diff --git a/DEPS b/DEPS
index ce42510..c7ffc60 100644
--- a/DEPS
+++ b/DEPS
@@ -253,15 +253,15 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
-  'skia_revision': '447845a906add31e196f9665fde406ab230c866b',
+  'skia_revision': '631e9e00fc341ce67e576cfed74cf5ed934cd44d',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'v8_revision': 'f6ce14f28a6a2a6e43c2eaacba19b14689e69eba',
+  'v8_revision': 'bbc18c9de614169da2a3d784177efeec33748538',
   # 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': '37bfc40d1bfc28a67f31200c71ff88a4aeece68c',
+  'angle_revision': '1fd544a4adc994d7e0b2f4e0d713d17c614e3fe6',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -269,7 +269,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
-  'pdfium_revision': '3fa9afc042f113c75e79d8d7966ef03ceea1fca5',
+  'pdfium_revision': '52cd2170b9ad1f550764629d5820e07438627213',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling BoringSSL
   # 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 devtools-frontend
   # and whatever else without interference from each other.
-  'devtools_frontend_revision': 'f98d5bdccf3be601afbcf9fdc3bd7e585b6b0239',
+  'devtools_frontend_revision': '776472583c6cc432c4f2c7028f361edbf033a900',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libprotobuf-mutator
   # and whatever else without interference from each other.
@@ -368,7 +368,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'dawn_revision': '5ba6ae779a8052202369962d67d5476ea4155fd9',
+  'dawn_revision': '44f039d3c209efc99315990938056f9157f1b42a',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -392,7 +392,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libavif
   # and whatever else without interference from each other.
-  'libavif_revision': '7a6d13be831da40859c6b61fb513b7a7a654a58b',
+  'libavif_revision': '6244bccbea5d96b08094ea1aed540c0d5a7cc31b',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling nearby
   # and whatever else without interference from each other.
@@ -704,7 +704,7 @@
     Var('chromium_git') + '/external/github.com/toji/webvr.info.git' + '@' + 'c58ae99b9ff9e2aa4c524633519570bf33536248',
 
   'src/docs/website': {
-    'url': Var('chromium_git') + '/website.git' + '@' + 'c5fc33d1a41991a0f64b42169220a57ffdaf7b54',
+    'url': Var('chromium_git') + '/website.git' + '@' + '45ff4c5d75b9182d81d8a1bea92005acd2178e14',
   },
 
   'src/ios/third_party/earl_grey2/src': {
@@ -1126,12 +1126,12 @@
 
   # For Linux and Chromium OS.
   'src/third_party/cros_system_api': {
-      'url': Var('chromium_git') + '/chromiumos/platform2/system_api.git' + '@' + 'bd903f09a68a2c4404c617014924e73db994a230',
+      'url': Var('chromium_git') + '/chromiumos/platform2/system_api.git' + '@' + '1af27b73d59bab46733bd0817be2008561c8288b',
       'condition': 'checkout_linux',
   },
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + 'a255e4064ae55e05e306aa595b12c047638e6b8c',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '1c22c511d02b4c4e02173297bcb8f2df3d3b4f66',
 
   'src/third_party/devtools-frontend/src':
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
@@ -1514,7 +1514,7 @@
   },
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' + '54ba566cdda23803c888afe5b664005be30d06e0',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' + 'd041e6e3ffe64223b6c9bbb3b066f8ab59873c02',
 
   'src/third_party/perl': {
       'url': Var('chromium_git') + '/chromium/deps/perl.git' + '@' + '6f3e5028eb65d0b4c5fdd792106ac4c84eee1eb3',
@@ -1592,7 +1592,7 @@
       'packages': [
           {
               'package': 'fuchsia/third_party/android/aemu/release/linux-amd64',
-              'version': '3qEVy-mxJWvizWcxmbgwzKDpK9LqsU4AiDZ1frAqUIIC'
+              'version': '16Xb5ZlqfZdoyBTPFYjV_SP1MSdQ7iYzIxZZT7_pHbkC'
           },
       ],
       'condition': 'host_os == "linux" and checkout_fuchsia',
@@ -1653,7 +1653,7 @@
     Var('chromium_git') + '/external/github.com/google/snappy.git' + '@' + '65dc7b383985eb4f63cd3e752136db8d9b4be8c0',
 
   'src/third_party/sqlite/src':
-    Var('chromium_git') + '/chromium/deps/sqlite.git' + '@' + 'bae060174325d9f8bbab105e9f807977bb1d3cf9',
+    Var('chromium_git') + '/chromium/deps/sqlite.git' + '@' + '803a31044a01ca3984f7c321c8821974e4100d07',
 
   'src/third_party/sqlite4java': {
       'packages': [
@@ -1735,7 +1735,7 @@
     Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + '008969e4d83211e112f83143bd7932f30e4ef549',
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + 'c1ed7ef491f83c83b90bafae2f1b72b0f68673a7',
+    Var('webrtc_git') + '/src.git' + '@' + '5823c55b17bb531bf0f5b11edcc747a189cc1922',
 
   'src/third_party/libgifcodec':
      Var('skia_git') + '/libgifcodec' + '@'+  Var('libgifcodec_revision'),
@@ -1805,7 +1805,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@db5ecce14d9077cf71fdbcc283f4f891f2e969f6',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@334e4232a3303239d0a505cd77c146207fa765e5',
     'condition': 'checkout_src_internal',
   },
 
diff --git a/ash/app_list/app_list_badge_controller.cc b/ash/app_list/app_list_badge_controller.cc
index bfc2754..9c927a1 100644
--- a/ash/app_list/app_list_badge_controller.cc
+++ b/ash/app_list/app_list_badge_controller.cc
@@ -47,8 +47,7 @@
     // Update the notification badge indicator for the newly added app list
     // item.
     cache_->ForOneApp(item->id(), [item](const apps::AppUpdate& update) {
-      item->UpdateNotificationBadge(update.HasBadge() ==
-                                    apps::mojom::OptionalBool::kTrue);
+      item->UpdateNotificationBadge(update.HasBadge().value_or(false));
     });
   }
 }
@@ -80,7 +79,8 @@
 void AppListBadgeController::OnAppUpdate(const apps::AppUpdate& update) {
   if (update.HasBadgeChanged() &&
       notification_badging_pref_enabled_.value_or(false)) {
-    UpdateItemNotificationBadge(update.AppId(), update.HasBadge());
+    UpdateItemNotificationBadge(update.AppId(),
+                                update.HasBadge().value_or(false));
   }
 }
 
@@ -91,13 +91,13 @@
 
 void AppListBadgeController::UpdateItemNotificationBadge(
     const std::string& app_id,
-    apps::mojom::OptionalBool has_badge) {
+    bool has_badge) {
   if (!model_)
     return;
   AppListItem* item = model_->FindItem(app_id);
   if (!item)
     return;
-  item->UpdateNotificationBadge(has_badge == apps::mojom::OptionalBool::kTrue);
+  item->UpdateNotificationBadge(has_badge);
 }
 
 void AppListBadgeController::UpdateAppNotificationBadging() {
@@ -115,11 +115,8 @@
   if (cache_) {
     cache_->ForEachApp([this](const apps::AppUpdate& update) {
       // Set the app notification badge hidden when the pref is disabled.
-      apps::mojom::OptionalBool has_badge =
-          notification_badging_pref_enabled_.value() &&
-                  (update.HasBadge() == apps::mojom::OptionalBool::kTrue)
-              ? apps::mojom::OptionalBool::kTrue
-              : apps::mojom::OptionalBool::kFalse;
+      bool has_badge = notification_badging_pref_enabled_.value() &&
+                       (update.HasBadge().value_or(false));
       UpdateItemNotificationBadge(update.AppId(), has_badge);
     });
   }
diff --git a/ash/app_list/app_list_badge_controller.h b/ash/app_list/app_list_badge_controller.h
index 1b6fb0ca..2861787a 100644
--- a/ash/app_list/app_list_badge_controller.h
+++ b/ash/app_list/app_list_badge_controller.h
@@ -55,8 +55,7 @@
  private:
   // Updates whether a notification badge is shown for the AppListItemView
   // corresponding with the |app_id|.
-  void UpdateItemNotificationBadge(const std::string& app_id,
-                                   apps::mojom::OptionalBool has_badge);
+  void UpdateItemNotificationBadge(const std::string& app_id, bool has_badge);
 
   // Checks the notification badging pref and then updates whether a
   // notification badge is shown for each AppListItem.
diff --git a/ash/app_list/app_list_bubble_presenter.cc b/ash/app_list/app_list_bubble_presenter.cc
index 4732631..fcdbdda 100644
--- a/ash/app_list/app_list_bubble_presenter.cc
+++ b/ash/app_list/app_list_bubble_presenter.cc
@@ -382,15 +382,9 @@
       bubble_widget_->GetNativeWindow()->parent();
 
   // If the bubble or one of its children (e.g. an uninstall dialog) gained
-  // focus, the bubble should stay open. Likewise, certain other containers are
-  // allowed to gain focus without closing the launcher (e.g. power menu).
-  // Allowing the other containers is a speculative fix for a bug where the
-  // launcher closes spontaneously.
-  if (gained_focus) {
-    aura::Window* container = ash::GetContainerForWindow(gained_focus);
-    if (container && !ShouldCloseAppListForFocusInContainer(container->GetId()))
-      return;
-  }
+  // focus, the bubble should stay open.
+  if (gained_focus && app_list_container->Contains(gained_focus))
+    return;
 
   // Otherwise, if the bubble or one of its children lost focus, the bubble
   // should close.
diff --git a/ash/app_list/app_list_bubble_presenter_unittest.cc b/ash/app_list/app_list_bubble_presenter_unittest.cc
index fbd04ac3..f41f3a9 100644
--- a/ash/app_list/app_list_bubble_presenter_unittest.cc
+++ b/ash/app_list/app_list_bubble_presenter_unittest.cc
@@ -330,21 +330,6 @@
   EXPECT_TRUE(presenter->bubble_widget_for_test());
 }
 
-TEST_F(AppListBubblePresenterTest, DismissOnFocusLoss) {
-  AppListBubblePresenter* presenter = GetBubblePresenter();
-  presenter->Show(GetPrimaryDisplay().id());
-
-  // Creating a window in these containers should not dismiss the launcher.
-  for (int id : kContainersThatWontHideAppListOnFocus) {
-    std::unique_ptr<views::Widget> widget = CreateTestWidget(nullptr, id);
-    EXPECT_TRUE(presenter->IsShowing());
-  }
-
-  // Creating a window in the default window container dismisses the launcher.
-  std::unique_ptr<views::Widget> widget = CreateTestWidget();
-  EXPECT_FALSE(presenter->IsShowing());
-}
-
 // Regression test for https://crbug.com/1275755
 TEST_F(AppListBubblePresenterTest, AssistantKeyOpensToAssistantPage) {
   // Simulate production behavior for animations, assistant, and zero-state
diff --git a/ash/app_list/app_list_presenter_impl.cc b/ash/app_list/app_list_presenter_impl.cc
index 2a5e649..a6867dea 100644
--- a/ash/app_list/app_list_presenter_impl.cc
+++ b/ash/app_list/app_list_presenter_impl.cc
@@ -26,6 +26,7 @@
 #include "ash/wm/container_finder.h"
 #include "base/bind.h"
 #include "base/callback_helpers.h"
+#include "base/containers/contains.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/metrics/user_metrics.h"
 #include "chromeos/services/assistant/public/cpp/assistant_enums.h"
@@ -125,6 +126,9 @@
 
 }  // namespace
 
+constexpr std::array<int, 7>
+    AppListPresenterImpl::kIdsOfContainersThatWontHideAppList;
+
 AppListPresenterImpl::AppListPresenterImpl(AppListControllerImpl* controller)
     : controller_(controller) {
   DCHECK(controller_);
@@ -549,7 +553,8 @@
   // change since the app list is still visible for the most part.
   const bool gained_focus_hides_app_list =
       gained_focus_container_id != kShellWindowId_Invalid &&
-      ShouldCloseAppListForFocusInContainer(gained_focus_container_id);
+      !base::Contains(kIdsOfContainersThatWontHideAppList,
+                      gained_focus_container_id);
 
   const bool app_list_gained_focus = applist_window->Contains(gained_focus) ||
                                      applist_container->Contains(gained_focus);
diff --git a/ash/app_list/app_list_presenter_impl.h b/ash/app_list/app_list_presenter_impl.h
index 125b3105..21af0018 100644
--- a/ash/app_list/app_list_presenter_impl.h
+++ b/ash/app_list/app_list_presenter_impl.h
@@ -14,6 +14,7 @@
 #include "ash/ash_export.h"
 #include "ash/public/cpp/pagination/pagination_model_observer.h"
 #include "ash/public/cpp/shelf_types.h"
+#include "ash/public/cpp/shell_window_ids.h"
 #include "ash/shelf/shelf_layout_manager.h"
 #include "ash/shelf/shelf_layout_manager_observer.h"
 #include "base/callback.h"
@@ -46,6 +47,15 @@
       public display::DisplayObserver,
       public ShelfLayoutManagerObserver {
  public:
+  static constexpr std::array<int, 7> kIdsOfContainersThatWontHideAppList = {
+      kShellWindowId_AppListContainer,
+      kShellWindowId_HomeScreenContainer,
+      kShellWindowId_MenuContainer,
+      kShellWindowId_PowerMenuContainer,
+      kShellWindowId_SettingBubbleContainer,
+      kShellWindowId_ShelfBubbleContainer,
+      kShellWindowId_ShelfContainer};
+
   // Callback which fills out the passed settings object. Used by
   // UpdateYPositionAndOpacityForHomeLauncher so different callers can do
   // similar animations with different settings.
diff --git a/ash/app_list/app_list_presenter_impl_unittest.cc b/ash/app_list/app_list_presenter_impl_unittest.cc
index 14549d0..8e2512a 100644
--- a/ash/app_list/app_list_presenter_impl_unittest.cc
+++ b/ash/app_list/app_list_presenter_impl_unittest.cc
@@ -177,7 +177,7 @@
             shelf_layout_manager->GetShelfBackgroundType());
   HotseatWidget* hotseat = GetPrimaryShelf()->hotseat_widget();
 
-  for (int id : kContainersThatWontHideAppListOnFocus) {
+  for (int id : AppListPresenterImpl::kIdsOfContainersThatWontHideAppList) {
     // Create a widget with a specific container id and make sure that the
     // kHomeLauncher background is still shown.
     std::unique_ptr<views::Widget> widget = CreateTestWidget(nullptr, id);
diff --git a/ash/app_list/app_list_test_api.cc b/ash/app_list/app_list_test_api.cc
index 1cd795b9..d4e3c53e 100644
--- a/ash/app_list/app_list_test_api.cc
+++ b/ash/app_list/app_list_test_api.cc
@@ -29,7 +29,6 @@
 #include "ash/app_list/views/paged_apps_grid_view.h"
 #include "ash/app_list/views/scrollable_apps_grid_view.h"
 #include "ash/constants/ash_features.h"
-#include "ash/public/cpp/shell_window_ids.h"
 #include "ash/shell.h"
 #include "ash/test/layer_animation_stopped_waiter.h"
 #include "base/callback.h"
diff --git a/ash/components/arc/mojom/file_system.mojom b/ash/components/arc/mojom/file_system.mojom
index e4a67617..d1ba40a 100644
--- a/ash/components/arc/mojom/file_system.mojom
+++ b/ash/components/arc/mojom/file_system.mojom
@@ -476,7 +476,8 @@
   [MinVersion=14] RequestFileRemovalScan@20(array<string> directory_paths);
 
   // DEPRECATED. Use OpenUrlsWithPermissionAndWindowInfo() instead.
-  [MinVersion=8] OpenUrlsWithPermission@11(OpenUrlsRequest request) => ();
+  [MinVersion=8] DEPRECATED_OpenUrlsWithPermission@11(OpenUrlsRequest request)
+      => ();
 
   // Opens URLs by sending an intent to the specified activity with a specific
   // launch window info.
diff --git a/ash/components/arc/test/fake_file_system_instance.cc b/ash/components/arc/test/fake_file_system_instance.cc
index f5c1f8a..1a3f80e 100644
--- a/ash/components/arc/test/fake_file_system_instance.cc
+++ b/ash/components/arc/test/fake_file_system_instance.cc
@@ -714,9 +714,9 @@
   RequestMediaScan(paths);
 }
 
-void FakeFileSystemInstance::OpenUrlsWithPermission(
+void FakeFileSystemInstance::DEPRECATED_OpenUrlsWithPermission(
     mojom::OpenUrlsRequestPtr request,
-    OpenUrlsWithPermissionCallback callback) {
+    DEPRECATED_OpenUrlsWithPermissionCallback callback) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   handled_url_requests_.emplace_back(std::move(request));
 }
@@ -724,7 +724,7 @@
 void FakeFileSystemInstance::OpenUrlsWithPermissionAndWindowInfo(
     mojom::OpenUrlsRequestPtr request,
     mojom::WindowInfoPtr window_info,
-    OpenUrlsWithPermissionCallback callback) {
+    DEPRECATED_OpenUrlsWithPermissionCallback callback) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   handled_url_requests_.emplace_back(std::move(request));
 }
diff --git a/ash/components/arc/test/fake_file_system_instance.h b/ash/components/arc/test/fake_file_system_instance.h
index e39b1ec..3d62231 100644
--- a/ash/components/arc/test/fake_file_system_instance.h
+++ b/ash/components/arc/test/fake_file_system_instance.h
@@ -354,12 +354,13 @@
   void RequestFileRemovalScan(
       const std::vector<std::string>& directory_paths) override;
   void ReindexDirectory(const std::string& directory_path) override;
-  void OpenUrlsWithPermission(mojom::OpenUrlsRequestPtr request,
-                              OpenUrlsWithPermissionCallback callback) override;
+  void DEPRECATED_OpenUrlsWithPermission(
+      mojom::OpenUrlsRequestPtr request,
+      DEPRECATED_OpenUrlsWithPermissionCallback callback) override;
   void OpenUrlsWithPermissionAndWindowInfo(
       mojom::OpenUrlsRequestPtr request,
       mojom::WindowInfoPtr window_info,
-      OpenUrlsWithPermissionCallback callback) override;
+      DEPRECATED_OpenUrlsWithPermissionCallback callback) override;
 
  private:
   // A pair of an authority and a document ID which identifies the location
diff --git a/ash/projector/projector_annotation_tray.cc b/ash/projector/projector_annotation_tray.cc
index 918c58d..6fd1b16 100644
--- a/ash/projector/projector_annotation_tray.cc
+++ b/ash/projector/projector_annotation_tray.cc
@@ -96,19 +96,29 @@
 ProjectorAnnotationTray::~ProjectorAnnotationTray() = default;
 
 bool ProjectorAnnotationTray::PerformAction(const ui::Event& event) {
-  if (bubble_) {
-    CloseBubble();
-  } else {
-    if (GetCurrentTool() == kToolNone) {
-      ShowBubble();
-    } else {
-      DeactivateActiveTool();
-    }
-  }
-
+  ToggleAnnotator();
   return true;
 }
 
+void ProjectorAnnotationTray::OnMouseEvent(ui::MouseEvent* event) {
+  if (event->type() != ui::ET_MOUSE_PRESSED) {
+    return;
+  }
+  if (event->IsRightMouseButton()) {
+    ShowBubble();
+  } else if (event->IsLeftMouseButton()) {
+    ToggleAnnotator();
+  }
+}
+
+void ProjectorAnnotationTray::OnGestureEvent(ui::GestureEvent* event) {
+  if (event->details().type() == ui::ET_GESTURE_LONG_PRESS) {
+    ShowBubble();
+  } else if (event->details().type() == ui::ET_GESTURE_TAP) {
+    ToggleAnnotator();
+  }
+}
+
 void ProjectorAnnotationTray::ClickedOutsideBubble() {
   CloseBubble();
 }
@@ -139,10 +149,6 @@
 
   DCHECK(tray_container());
 
-  // There may still be an active tool if show bubble was called from an
-  // accelerator.
-  DeactivateActiveTool();
-
   TrayBubbleView::InitParams init_params;
   init_params.delegate = this;
   init_params.parent_window = GetBubbleWindowContainer();
@@ -230,11 +236,28 @@
   UpdateIcon();
 }
 
+void ProjectorAnnotationTray::ToggleAnnotator() {
+  if (GetCurrentTool() == kToolNone) {
+    EnableAnnotatorTool();
+  } else {
+    DeactivateActiveTool();
+  }
+  if (bubble_) {
+    CloseBubble();
+  }
+  UpdateIcon();
+}
+
+void ProjectorAnnotationTray::EnableAnnotatorTool() {
+  auto* controller = Shell::Get()->projector_controller();
+  DCHECK(controller);
+  controller->OnMarkerPressed();
+}
+
 void ProjectorAnnotationTray::DeactivateActiveTool() {
   auto* controller = Shell::Get()->projector_controller();
   DCHECK(controller);
   controller->ResetTools();
-  UpdateIcon();
 }
 
 void ProjectorAnnotationTray::UpdateIcon() {
@@ -243,14 +266,13 @@
       GetIconForTool(tool),
       AshColorProvider::Get()->GetContentLayerColor(
           AshColorProvider::ContentLayerType::kIconColorPrimary)));
+  SetIsActive(GetCurrentTool() == kToolNone);
 }
 
 void ProjectorAnnotationTray::OnPenColorPressed(SkColor color) {
   // TODO(b/201664243) Pass the color for the marker.
-  auto* projector_controller = ProjectorControllerImpl::Get();
-  DCHECK(projector_controller);
-  projector_controller->OnMarkerPressed();
   CloseBubble();
+  UpdateIcon();
 }
 
 }  // namespace ash
diff --git a/ash/projector/projector_annotation_tray.h b/ash/projector/projector_annotation_tray.h
index cc32db7..2019b196 100644
--- a/ash/projector/projector_annotation_tray.h
+++ b/ash/projector/projector_annotation_tray.h
@@ -32,10 +32,15 @@
   void ShowBubble() override;
   TrayBubbleView* GetBubbleView() override;
   views::Widget* GetBubbleWidget() const override;
+  void OnMouseEvent(ui::MouseEvent* event) override;
+  void OnGestureEvent(ui::GestureEvent* event) override;
   void OnThemeChanged() override;
 
  private:
-  // Deactives any annotation tool that is currently enabled and update the UI.
+  void ToggleAnnotator();
+  void EnableAnnotatorTool();
+  // Deactivates any annotation tool that is currently enabled and updates the
+  // UI.
   void DeactivateActiveTool();
 
   // Updates the icon in the status area.
diff --git a/ash/public/cpp/shell_window_ids.cc b/ash/public/cpp/shell_window_ids.cc
index 8fa18c2..fb5a564 100644
--- a/ash/public/cpp/shell_window_ids.cc
+++ b/ash/public/cpp/shell_window_ids.cc
@@ -46,10 +46,6 @@
 
 }  // namespace
 
-bool ShouldCloseAppListForFocusInContainer(int id) {
-  return !base::Contains(kContainersThatWontHideAppListOnFocus, id);
-}
-
 std::vector<int> GetActivatableShellWindowIds() {
   std::vector<int> ids(kPreDesksActivatableContainersIds.begin(),
                        kPreDesksActivatableContainersIds.end());
diff --git a/ash/public/cpp/shell_window_ids.h b/ash/public/cpp/shell_window_ids.h
index b287371..a5c387b 100644
--- a/ash/public/cpp/shell_window_ids.h
+++ b/ash/public/cpp/shell_window_ids.h
@@ -220,22 +220,10 @@
 
 // A list of system modal container IDs. The order of the list is important that
 // the more restrictive container appears before the less restrictive ones.
-inline constexpr int kSystemModalContainerIds[] = {
+constexpr int kSystemModalContainerIds[] = {
     kShellWindowId_LockSystemModalContainer,
     kShellWindowId_SystemModalContainer};
 
-// Normally if a window gains focus the app list will be closed. Windows in
-// these containers are exceptions to that rule.
-inline constexpr int kContainersThatWontHideAppListOnFocus[] = {
-    kShellWindowId_AppListContainer,       kShellWindowId_HomeScreenContainer,
-    kShellWindowId_MenuContainer,          kShellWindowId_PowerMenuContainer,
-    kShellWindowId_SettingBubbleContainer, kShellWindowId_ShelfBubbleContainer,
-    kShellWindowId_ShelfContainer};
-
-// Returns true if `id` is a shell window container id such that a window in
-// that container gaining focus should close the app list.
-ASH_PUBLIC_EXPORT bool ShouldCloseAppListForFocusInContainer(int id);
-
 // Returns the list of container ids of containers which may contain windows
 // that need to be activated. this list is ordered by the activation order; that
 // is, windows in containers appearing earlier in the list are activated before
diff --git a/ash/search_box/search_box_view_base.cc b/ash/search_box/search_box_view_base.cc
index d1c96146..355609d 100644
--- a/ash/search_box/search_box_view_base.cc
+++ b/ash/search_box/search_box_view_base.cc
@@ -107,6 +107,13 @@
     // OnPaintBackground();
     SetInstallFocusRingOnFocus(false);
 
+    // Inkdrop only on click.
+    views::InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::ON);
+    SetHasInkDropActionOnClick(true);
+    views::InkDrop::UseInkDropForFloodFillRipple(views::InkDrop::Get(this),
+                                                 /*highlight_on_hover=*/false);
+    UpdateInkDropColors();
+
     SetPaintToLayer();
     layer()->SetFillsBoundsOpaquely(false);
 
@@ -144,6 +151,11 @@
     SchedulePaint();
   }
 
+  void OnThemeChanged() override {
+    views::View::OnThemeChanged();
+    UpdateInkDropColors();
+  }
+
   void set_is_showing(bool is_showing) { is_showing_ = is_showing; }
   bool is_showing() { return is_showing_; }
 
@@ -153,6 +165,18 @@
   // Whether the button is showing/shown or hiding/hidden.
   bool is_showing_ = false;
 
+  void UpdateInkDropColors() {
+    SkColor search_box_card_background_color =
+        AppListColorProvider::Get()->GetSearchBoxCardBackgroundColor();
+
+    views::InkDrop::Get(this)->SetBaseColor(
+        AppListColorProvider::Get()->GetInkDropBaseColor(
+            search_box_card_background_color));
+    views::InkDrop::Get(this)->SetVisibleOpacity(
+        AppListColorProvider::Get()->GetInkDropOpacity(
+            search_box_card_background_color));
+  }
+
   // views::View overrides:
   void OnPaintBackground(gfx::Canvas* canvas) override {
     if (HasFocus()) {
diff --git a/ash/shelf/shelf_controller.cc b/ash/shelf/shelf_controller.cc
index c3824b9..65c7481 100644
--- a/ash/shelf/shelf_controller.cc
+++ b/ash/shelf/shelf_controller.cc
@@ -238,7 +238,7 @@
 void ShelfController::OnAppUpdate(const apps::AppUpdate& update) {
   if (update.HasBadgeChanged() &&
       notification_badging_pref_enabled_.value_or(false)) {
-    bool has_badge = update.HasBadge() == apps::mojom::OptionalBool::kTrue;
+    bool has_badge = update.HasBadge().value_or(false);
     model_.UpdateItemNotification(update.AppId(), has_badge);
   }
 }
@@ -256,7 +256,7 @@
 
   // Update the notification badge indicator for the newly added shelf item.
   cache_->ForOneApp(app_id, [this](const apps::AppUpdate& update) {
-    bool has_badge = update.HasBadge() == apps::mojom::OptionalBool::kTrue;
+    bool has_badge = update.HasBadge().value_or(false);
     model_.UpdateItemNotification(update.AppId(), has_badge);
   });
 }
@@ -276,10 +276,9 @@
   if (cache_) {
     cache_->ForEachApp([this](const apps::AppUpdate& update) {
       // Set the app notification badge hidden when the pref is disabled.
-      bool has_badge =
-          notification_badging_pref_enabled_.value()
-              ? (update.HasBadge() == apps::mojom::OptionalBool::kTrue)
-              : false;
+      bool has_badge = notification_badging_pref_enabled_.value()
+                           ? update.HasBadge().value_or(false)
+                           : false;
 
       model_.UpdateItemNotification(update.AppId(), has_badge);
     });
diff --git a/ash/system/accessibility/tray_accessibility.cc b/ash/system/accessibility/tray_accessibility.cc
index e6d6fee5..84ea3e0 100644
--- a/ash/system/accessibility/tray_accessibility.cc
+++ b/ash/system/accessibility/tray_accessibility.cc
@@ -603,14 +603,15 @@
   }
 }
 
-void AccessibilityDetailedView::OnSodaInstallSucceeded() {
-  speech::SodaInstaller* soda_installer = speech::SodaInstaller::GetInstance();
-  if (!soda_installer->IsSodaInstalled(GetDictationLocale()))
+// SodaInstaller::Observer:
+void AccessibilityDetailedView::OnSodaInstalled(
+    speech::LanguageCode language_code) {
+  if (language_code != GetDictationLocale())
     return;
 
-  // Only show the success message if both the SODA binary and the language pack
+  // Show the success message if both the SODA binary and the language pack
   // matching the Dictation locale have been downloaded.
-  soda_installer->RemoveObserver(this);
+  speech::SodaInstaller::GetInstance()->RemoveObserver(this);
   AccessibilityControllerImpl* controller =
       Shell::Get()->accessibility_controller();
   if (dictation_view_ && controller->IsDictationSettingVisibleInTray()) {
@@ -619,16 +620,34 @@
   }
 }
 
-void AccessibilityDetailedView::OnSodaInstallProgress(
-    int progress,
+void AccessibilityDetailedView::OnSodaError(
     speech::LanguageCode language_code) {
-  // TODO(https://crbug.com/1266491): Ensure we use combined progress instead
-  // of just the language pack progress.
-  if (language_code != GetDictationLocale())
+  if (language_code != speech::LanguageCode::kNone &&
+      language_code != GetDictationLocale()) {
     return;
+  }
 
-  // Only show the progress message if this applies to the language pack
-  // matching the Dictation locale.
+  // Show the failed message if either the Dictation locale failed or the SODA
+  // binary failed (encoded by LanguageCode::kNone).
+  speech::SodaInstaller::GetInstance()->RemoveObserver(this);
+  AccessibilityControllerImpl* controller =
+      Shell::Get()->accessibility_controller();
+  if (dictation_view_ && controller->IsDictationSettingVisibleInTray()) {
+    dictation_view_->SetSubText(l10n_util::GetStringUTF16(
+        IDS_ASH_ACCESSIBILITY_DICTATION_SETTING_SUBTITLE_SODA_DOWNLOAD_ERROR));
+  }
+}
+
+void AccessibilityDetailedView::OnSodaProgress(
+    speech::LanguageCode language_code,
+    int progress) {
+  if (language_code != speech::LanguageCode::kNone &&
+      language_code != GetDictationLocale()) {
+    return;
+  }
+
+  // Only show the progress message if this applies to the SODA binary (encoded
+  // by LanguageCode::kNone) or the language pack matching the Dictation locale.
   AccessibilityControllerImpl* controller =
       Shell::Get()->accessibility_controller();
   if (dictation_view_ && controller->IsDictationSettingVisibleInTray()) {
@@ -638,47 +657,6 @@
   }
 }
 
-void AccessibilityDetailedView::OnSodaInstallFailed(
-    speech::LanguageCode language_code) {
-  if (language_code == speech::LanguageCode::kNone ||
-      language_code == GetDictationLocale()) {
-    // Show the failed message if either the Dictation locale failed or the SODA
-    // binary failed (encoded by LanguageCode::kNone).
-    speech::SodaInstaller::GetInstance()->RemoveObserver(this);
-    AccessibilityControllerImpl* controller =
-        Shell::Get()->accessibility_controller();
-    if (dictation_view_ && controller->IsDictationSettingVisibleInTray()) {
-      dictation_view_->SetSubText(l10n_util::GetStringUTF16(
-          IDS_ASH_ACCESSIBILITY_DICTATION_SETTING_SUBTITLE_SODA_DOWNLOAD_ERROR));
-    }
-  }
-}
-
-// SodaInstaller::Observer:
-void AccessibilityDetailedView::OnSodaInstalled() {
-  OnSodaInstallSucceeded();
-}
-
-void AccessibilityDetailedView::OnSodaLanguagePackInstalled(
-    speech::LanguageCode language_code) {
-  OnSodaInstallSucceeded();
-}
-
-void AccessibilityDetailedView::OnSodaError() {
-  OnSodaInstallFailed(speech::LanguageCode::kNone);
-}
-
-void AccessibilityDetailedView::OnSodaLanguagePackError(
-    speech::LanguageCode language_code) {
-  OnSodaInstallFailed(language_code);
-}
-
-void AccessibilityDetailedView::OnSodaLanguagePackProgress(
-    int language_progress,
-    speech::LanguageCode language_code) {
-  OnSodaInstallProgress(language_progress, language_code);
-}
-
 void AccessibilityDetailedView::SetDictationViewSubtitleTextForTesting(
     std::u16string text) {
   dictation_view_->SetSubText(text);
diff --git a/ash/system/accessibility/tray_accessibility.h b/ash/system/accessibility/tray_accessibility.h
index f5f4cb8e..b280c56 100644
--- a/ash/system/accessibility/tray_accessibility.h
+++ b/ash/system/accessibility/tray_accessibility.h
@@ -75,18 +75,12 @@
   void AppendAccessibilityList();
 
   void UpdateSodaInstallerObserverStatus();
-  void OnSodaInstallSucceeded();
-  void OnSodaInstallProgress(int progress, speech::LanguageCode language_code);
-  void OnSodaInstallFailed(speech::LanguageCode language_code);
 
   // SodaInstaller::Observer:
-  void OnSodaInstalled() override;
-  void OnSodaLanguagePackInstalled(speech::LanguageCode language_code) override;
-  void OnSodaError() override;
-  void OnSodaLanguagePackError(speech::LanguageCode language_code) override;
-  void OnSodaProgress(int combined_progress) override {}
-  void OnSodaLanguagePackProgress(int language_progress,
-                                  speech::LanguageCode language_code) override;
+  void OnSodaInstalled(speech::LanguageCode language_code) override;
+  void OnSodaError(speech::LanguageCode language_code) override;
+  void OnSodaProgress(speech::LanguageCode language_code,
+                      int combined_progress) override;
 
   void SetDictationViewSubtitleTextForTesting(std::u16string text);
   std::u16string GetDictationViewSubtitleTextForTesting();
diff --git a/ash/system/accessibility/tray_accessibility_unittest.cc b/ash/system/accessibility/tray_accessibility_unittest.cc
index 3fcdd27..8439b19 100644
--- a/ash/system/accessibility/tray_accessibility_unittest.cc
+++ b/ash/system/accessibility/tray_accessibility_unittest.cc
@@ -715,6 +715,7 @@
   }
 
   speech::LanguageCode en_us() { return speech::LanguageCode::kEnUs; }
+  speech::LanguageCode fr_fr() { return speech::LanguageCode::kFrFr; }
 
   void SetDictationViewSubtitleText(std::u16string text) {
     detailed_menu()->SetDictationViewSubtitleTextForTesting(text);
@@ -738,23 +739,20 @@
   // correct language pack before doing anything.
   soda_installer()->NotifySodaInstalledForTesting();
   EXPECT_EQ(kInitialDictationViewSubtitleText, GetDictationViewSubtitleText());
-  soda_installer()->NotifyOnSodaLanguagePackInstalledForTesting(en_us());
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   EXPECT_EQ(kInitialDictationViewSubtitleText, GetDictationViewSubtitleText());
-  soda_installer()->NotifyOnSodaLanguagePackInstalledForTesting(
-      speech::LanguageCode::kFrFr);
+  soda_installer()->NotifySodaInstalledForTesting(fr_fr());
   EXPECT_EQ(kSodaDownloaded, GetDictationViewSubtitleText());
 }
 
 // Ensures we only notify the user of progress for the language pack matching
 // the Dictation locale.
 TEST_F(TrayAccessibilitySodaTest, OnSodaProgressNotification) {
-  // Do not give updates for the SODA binary.
-  soda_installer()->NotifySodaDownloadProgressForTesting(50);
+  soda_installer()->NotifySodaProgressForTesting(50, fr_fr());
   EXPECT_EQ(kInitialDictationViewSubtitleText, GetDictationViewSubtitleText());
-  soda_installer()->NotifyOnSodaLanguagePackProgressForTesting(
-      50, speech::LanguageCode::kFrFr);
-  EXPECT_EQ(kInitialDictationViewSubtitleText, GetDictationViewSubtitleText());
-  soda_installer()->NotifyOnSodaLanguagePackProgressForTesting(50, en_us());
+  soda_installer()->NotifySodaProgressForTesting(50);
+  EXPECT_EQ(kSodaInProgress, GetDictationViewSubtitleText());
+  soda_installer()->NotifySodaProgressForTesting(50, en_us());
   EXPECT_EQ(kSodaInProgress, GetDictationViewSubtitleText());
 }
 
@@ -768,10 +766,9 @@
 TEST_F(TrayAccessibilitySodaTest, SodaLanguageErrorNotification) {
   // Do nothing if the failed language pack is different than the Dictation
   // locale.
-  soda_installer()->NotifyOnSodaLanguagePackErrorForTesting(
-      speech::LanguageCode::kFrFr);
+  soda_installer()->NotifySodaErrorForTesting(fr_fr());
   EXPECT_EQ(kInitialDictationViewSubtitleText, GetDictationViewSubtitleText());
-  soda_installer()->NotifyOnSodaLanguagePackErrorForTesting(en_us());
+  soda_installer()->NotifySodaErrorForTesting(en_us());
   EXPECT_EQ(kSodaFailed, GetDictationViewSubtitleText());
 }
 
diff --git a/ash/system/eche/eche_tray.cc b/ash/system/eche/eche_tray.cc
index 48d9477..c1fda179 100644
--- a/ash/system/eche/eche_tray.cc
+++ b/ash/system/eche/eche_tray.cc
@@ -102,44 +102,7 @@
     return;
   }
 
-  TrayBubbleView::InitParams init_params;
-  init_params.delegate = this;
-  init_params.parent_window = GetBubbleWindowContainer();
-  init_params.anchor_mode = TrayBubbleView::AnchorMode::kRect;
-  init_params.anchor_rect = GetBubbleAnchor()->GetAnchorBoundsInScreen();
-  init_params.insets = GetTrayBubbleInsets();
-  init_params.shelf_alignment = shelf()->alignment();
-  // TODO(nayebi): get the width relative to the screen size
-  init_params.preferred_width = 400;
-  init_params.close_on_deactivate = false;
-  init_params.has_shadow = false;
-  init_params.translucent = true;
-  init_params.reroute_event_handler = false;
-  init_params.corner_radius = kTrayItemCornerRadius;
-
-  auto bubble_view = std::make_unique<TrayBubbleView>(init_params);
-  bubble_view->SetCanActivate(true);
-  bubble_view->SetBorder(views::CreateEmptyBorder(kBubblePadding));
-
-  auto* header_view = bubble_view->AddChildView(CreateBubbleHeaderView());
-  // The layer is needed to draw the header non-opaquely that is needed to
-  // match the phone hub behavior.
-  header_view->SetPaintToLayer();
-  header_view->layer()->SetFillsBoundsOpaquely(false);
-
-  AshWebView::InitParams params;
-  auto web_view = AshWebViewFactory::Get()->Create(params);
-  // TODO(nayebi): Use GetDefaultBoundsForEche()
-  web_view->SetPreferredSize(gfx::Size(400, 600));
-  if (!url_.is_empty())
-    web_view->Navigate(url_);
-  bubble_view->AddChildView(std::move(web_view));
-
-  bubble_ = std::make_unique<TrayBubbleWrapper>(this, bubble_view.release(),
-                                                /*event_handling=*/false);
-
-  SetIsActive(true);
-  bubble_->GetBubbleView()->UpdateBubble();
+  InitBubble();
 
   // TODO(nayebi): Add metric updates.
 }
@@ -218,6 +181,46 @@
   bubble_->GetBubbleWidget()->Hide();
 }
 
+void EcheTray::InitBubble() {
+  TrayBubbleView::InitParams init_params;
+  init_params.delegate = this;
+  init_params.parent_window = GetBubbleWindowContainer();
+  init_params.anchor_mode = TrayBubbleView::AnchorMode::kRect;
+  init_params.anchor_rect = GetBubbleAnchor()->GetAnchorBoundsInScreen();
+  init_params.insets = GetTrayBubbleInsets();
+  init_params.shelf_alignment = shelf()->alignment();
+  // TODO(nayebi): get the width relative to the screen size
+  init_params.preferred_width = 400;
+  init_params.close_on_deactivate = false;
+  init_params.has_shadow = false;
+  init_params.translucent = true;
+  init_params.reroute_event_handler = false;
+  init_params.corner_radius = kTrayItemCornerRadius;
+
+  auto bubble_view = std::make_unique<TrayBubbleView>(init_params);
+  bubble_view->SetCanActivate(true);
+  bubble_view->SetBorder(views::CreateEmptyBorder(kBubblePadding));
+
+  auto* header_view = bubble_view->AddChildView(CreateBubbleHeaderView());
+  // The layer is needed to draw the header non-opaquely that is needed to
+  // match the phone hub behavior.
+  header_view->SetPaintToLayer();
+  header_view->layer()->SetFillsBoundsOpaquely(false);
+
+  auto web_view = AshWebViewFactory::Get()->Create(AshWebView::InitParams());
+  // TODO(nayebi): Use GetDefaultBoundsForEche()
+  web_view->SetPreferredSize(gfx::Size(400, 600));
+  if (!url_.is_empty())
+    web_view->Navigate(url_);
+  bubble_view->AddChildView(std::move(web_view));
+
+  bubble_ = std::make_unique<TrayBubbleWrapper>(this, bubble_view.release(),
+                                                /*event_handling=*/false);
+
+  SetIsActive(true);
+  bubble_->GetBubbleView()->UpdateBubble();
+}
+
 void EcheTray::UpdateVisibility() {
   SetVisiblePreferred(true);
 }
diff --git a/ash/system/eche/eche_tray.h b/ash/system/eche/eche_tray.h
index 394e710f..3151de0 100644
--- a/ash/system/eche/eche_tray.h
+++ b/ash/system/eche/eche_tray.h
@@ -75,6 +75,11 @@
 
   void HideBubble();
 
+  // Set up the params and init the bubble.
+  // Note: This function makes the bubble active and makes the
+  // TrayBackgroundView's background inkdrop activate.
+  void InitBubble();
+
   // Test helpers
   TrayBubbleWrapper* get_bubble_wrapper_for_test() { return bubble_.get(); }
 
diff --git a/ash/system/eche/eche_tray_unittest.cc b/ash/system/eche/eche_tray_unittest.cc
index 5d4b700e..b4d6660 100644
--- a/ash/system/eche/eche_tray_unittest.cc
+++ b/ash/system/eche/eche_tray_unittest.cc
@@ -95,4 +95,31 @@
       eche_tray()->get_bubble_wrapper_for_test()->bubble_view()->GetVisible());
 }
 
+TEST_F(EcheTrayTest, EcheTrayCreatesBubbleButHideFirst) {
+  // Verify the eche tray button is not active, and the eche tray bubble
+  // is not shown initially.
+  EXPECT_FALSE(eche_tray()->is_active());
+  EXPECT_FALSE(eche_tray()->get_bubble_wrapper_for_test());
+
+  // Allow us to create the bubble but it is not visible until we need this
+  // bubble to show up.
+  eche_tray()->SetUrl(GURL("http://google.com"));
+  eche_tray()->InitBubble();
+  eche_tray()->HideBubble();
+
+  EXPECT_FALSE(eche_tray()->is_active());
+  EXPECT_TRUE(eche_tray()->get_bubble_wrapper_for_test());
+  EXPECT_FALSE(
+      eche_tray()->get_bubble_wrapper_for_test()->bubble_view()->GetVisible());
+
+  // Request this bubble to show up.
+  eche_tray()->ShowBubble();
+  // Wait for the tray bubble widget to open.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_TRUE(eche_tray()->is_active());
+  EXPECT_TRUE(eche_tray()->get_bubble_wrapper_for_test());
+  EXPECT_TRUE(
+      eche_tray()->get_bubble_wrapper_for_test()->bubble_view()->GetVisible());
+}
+
 }  // namespace ash
diff --git a/ash/system/message_center/ash_notification_view_unittest.cc b/ash/system/message_center/ash_notification_view_unittest.cc
index 518f2ac..8ae1675d 100644
--- a/ash/system/message_center/ash_notification_view_unittest.cc
+++ b/ash/system/message_center/ash_notification_view_unittest.cc
@@ -20,6 +20,7 @@
 #include "ash/test/ash_test_base.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/time/time.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/compositor/layer.h"
 #include "ui/compositor/layer_animator.h"
@@ -192,10 +193,11 @@
       EXPECT_TRUE(ui::WaitForNextFrameToBePresented(compositor));
     }
 
-    // Ensure there is one more frame presented after animation finishes to
-    // allow animation throughput data to be passed from cc to ui.
-    std::ignore =
-        ui::WaitForNextFrameToBePresented(compositor, base::Milliseconds(200));
+    // Force a frame then wait, ensuring there is one more frame presented after
+    // animation finishes to allow animation throughput data to be passed from
+    // cc to ui.
+    compositor->ScheduleFullRedraw();
+    EXPECT_TRUE(ui::WaitForNextFrameToBePresented(compositor));
 
     // Smoothness should be recorded.
     histograms.ExpectTotalCount(animation_histogram_name, data_point_count);
@@ -665,8 +667,7 @@
       "Ash.NotificationView.ActionsRow.FadeIn.AnimationSmoothness");
 }
 
-TEST_F(AshNotificationViewTest,
-       DISABLED_ImageExpandCollapseAnimationsRecordSmoothness) {
+TEST_F(AshNotificationViewTest, ImageExpandCollapseAnimationsRecordSmoothness) {
   // Enable animations.
   ui::ScopedAnimationDurationScaleMode duration(
       ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
@@ -731,8 +732,7 @@
                           "ScaleAndTranslate.AnimationSmoothness");
 }
 
-TEST_F(AshNotificationViewTest,
-       DISABLED_GroupExpandCollapseAnimationsRecordSmoothness) {
+TEST_F(AshNotificationViewTest, GroupExpandCollapseAnimationsRecordSmoothness) {
   base::HistogramTester histograms;
 
   // Enable animations.
@@ -839,8 +839,7 @@
       "Ash.NotificationView.InlineReply.FadeOut.AnimationSmoothness");
 }
 
-TEST_F(AshNotificationViewTest,
-       DISABLED_InlineSettingsAnimationsRecordSmoothness) {
+TEST_F(AshNotificationViewTest, InlineSettingsAnimationsRecordSmoothness) {
   base::HistogramTester histograms;
 
   // Enable animations.
diff --git a/ash/webui/camera_app_ui/resources/.eslintrc.js b/ash/webui/camera_app_ui/resources/.eslintrc.js
index 5e2227b..fece873d 100644
--- a/ash/webui/camera_app_ui/resources/.eslintrc.js
+++ b/ash/webui/camera_app_ui/resources/.eslintrc.js
@@ -534,6 +534,11 @@
         message: 'Don\'t use "Interface" as identifier suffix. ' +
             '(go/tsstyle#naming-style)',
       },
+      // Disallow forEach. (go/tsstyle#iterating-containers)
+      {
+        selector: 'CallExpression[callee.property.name="forEach"]',
+        message: 'forEach are not allowed. (go/tsstyle#iterating-containers)',
+      },
     ],
 
     '@typescript-eslint/naming-convention': [
diff --git a/ash/webui/camera_app_ui/resources/js/animation.ts b/ash/webui/camera_app_ui/resources/js/animation.ts
index f762e86..2f32d14 100644
--- a/ash/webui/camera_app_ui/resources/js/animation.ts
+++ b/ash/webui/camera_app_ui/resources/js/animation.ts
@@ -39,7 +39,9 @@
  */
 async function doCancel({el, onChild}: {el: HTMLElement, onChild: boolean}):
     Promise<void> {
-  getAnimations({el, onChild}).forEach((a) => a.cancel());
+  for (const a of getAnimations({el, onChild})) {
+    a.cancel();
+  }
   await getQueueFor(el).flush();
 }
 
diff --git a/ash/webui/camera_app_ui/resources/js/device/camera3_device_info.ts b/ash/webui/camera_app_ui/resources/js/device/camera3_device_info.ts
index ff1f874..d47d6724 100644
--- a/ash/webui/camera_app_ui/resources/js/device/camera3_device_info.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/camera3_device_info.ts
@@ -18,16 +18,16 @@
 export class Camera3DeviceInfo {
   readonly deviceId: string;
 
-  readonly videoResols: ResolutionList = [];
+  readonly videoResolutions: ResolutionList = [];
 
   readonly videoMaxFps: MaxFpsInfo = {};
 
   /**
    * @param deviceInfo Information of the video device.
    * @param facing Camera facing of the video device.
-   * @param photoResols Supported available photo resolutions
+   * @param photoResolutions Supported available photo resolutions
    *     of the video device.
-   * @param videoResolFpses Supported available video
+   * @param videoResolutionFpses Supported available video
    *     resolutions and maximal capture fps of the video device.
    * @param fpsRanges Supported fps ranges of the video device.
    * @param supportPTZ Is supported PTZ controls.
@@ -35,18 +35,20 @@
   constructor(
       deviceInfo: MediaDeviceInfo,
       readonly facing: Facing,
-      readonly photoResols: ResolutionList,
-      videoResolFpses: VideoConfig[],
+      readonly photoResolutions: ResolutionList,
+      videoResolutionFpses: VideoConfig[],
       readonly fpsRanges: FpsRangeList,
       readonly supportPTZ: boolean,
   ) {
     this.deviceId = deviceInfo.deviceId;
-    videoResolFpses.filter(({maxFps}) => maxFps >= 24)
-        .forEach(({width, height, maxFps}) => {
-          const r = new Resolution(width, height);
-          this.videoResols.push(r);
-          this.videoMaxFps[r.toString()] = maxFps;
-        });
+    for (const {width, height, maxFps} of videoResolutionFpses) {
+      if (maxFps < 24) {
+        continue;
+      }
+      const r = new Resolution(width, height);
+      this.videoResolutions.push(r);
+      this.videoMaxFps[r.toString()] = maxFps;
+    }
   }
 
   /**
diff --git a/ash/webui/camera_app_ui/resources/js/device/camera_manager.ts b/ash/webui/camera_app_ui/resources/js/device/camera_manager.ts
index 1b662b98..4898e79 100644
--- a/ash/webui/camera_app_ui/resources/js/device/camera_manager.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/camera_manager.ts
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import {
+  assert,
   assertExists,
   assertInstanceof,
   assertString,
@@ -101,7 +102,7 @@
 
   constructor(
       private readonly perfLogger: PerfLogger,
-      defaultFacing: Facing,
+      defaultFacing: Facing|null,
       modeConstraints: ModeConstraints,
   ) {
     this.preview = new Preview(async () => {
@@ -143,7 +144,7 @@
   }
 
   private getDeviceId(): string {
-    return assertString(this.scheduler.reconfigurer.config.deviceId);
+    return assertString(this.scheduler.reconfigurer.config?.deviceId);
   }
 
   getPreviewVideo(): PreviewVideo {
@@ -299,6 +300,7 @@
       }
       if (devices.length > 0) {
         index = (index + 1) % devices.length;
+        assert(this.scheduler.reconfigurer.config !== null);
         this.scheduler.reconfigurer.config.deviceId = devices[index].deviceId;
       }
     });
@@ -312,6 +314,7 @@
 
   switchMode(mode: Mode): Promise<boolean>|null {
     return this.tryReconfigure(() => {
+      assert(this.scheduler.reconfigurer.config !== null);
       this.scheduler.reconfigurer.config.mode = mode;
     });
   }
@@ -326,6 +329,7 @@
       // Changing the configure of the camera not currently opened, thus no
       // reconfiguration are required.
       preferer.changePreferredResolution(deviceId, resolution);
+      assert(this.scheduler.reconfigurer.config !== null);
       return this.onUpdateConfig(this.scheduler.reconfigurer.config)
           .then(() => true);
     }
diff --git a/ash/webui/camera_app_ui/resources/js/device/camera_operation.ts b/ash/webui/camera_app_ui/resources/js/device/camera_operation.ts
index ac21311..ee0c1e4 100644
--- a/ash/webui/camera_app_ui/resources/js/device/camera_operation.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/camera_operation.ts
@@ -32,6 +32,7 @@
 import {StreamManager} from './stream_manager.js';
 import {
   CameraConfig,
+  CameraConfigCandidate,
   CameraInfo,
   CameraViewUI,
   ModeConstraints,
@@ -47,7 +48,7 @@
 }
 
 export interface EventListener {
-  onTryingNewConfig(config: CameraConfig): void;
+  onTryingNewConfig(config: CameraConfigCandidate): void;
   onUpdateConfig(config: CameraConfig): Promise<void>;
   onUpdateCapability(cameraInfo: CameraInfo): void;
 }
@@ -59,7 +60,11 @@
   /**
    * Preferred configuration.
    */
-  config: CameraConfig;
+  config: CameraConfig|null = null;
+
+  private readonly initialFacing: Facing|null;
+
+  private readonly initialMode: Mode;
 
   private shouldSuspend = false;
 
@@ -68,11 +73,11 @@
       private readonly modes: Modes,
       private readonly listener: EventListener,
       private readonly modeConstraints: ModeConstraints,
-      facing: Facing,
+      facing: Facing|null,
   ) {
-    const mode = util.assertEnumVariant(
+    this.initialMode = util.assertEnumVariant(
         Mode, this.modeConstraints.exact ?? this.modeConstraints.default);
-    this.config = {deviceId: null, facing, mode};
+    this.initialFacing = facing;
   }
 
   setShouldSuspend(value: boolean) {
@@ -100,17 +105,15 @@
       devices = cameraInfo.devicesInfo;
     }
 
-    const preferredFacing = this.config.facing === Facing.NOT_SET ?
-        util.getDefaultFacing() :
-        this.config.facing;
+    const preferredFacing =
+        this.config?.facing ?? this.initialFacing ?? util.getDefaultFacing();
     // Put the selected video device id first.
     const sorted = devices.map((device) => device.deviceId).sort((a, b) => {
       if (a === b) {
         return 0;
       }
-      if (this.config.deviceId !== null ?
-              a === this.config.deviceId :
-              (facings && facings[a] === preferredFacing)) {
+      if (this.config !== null ? a === this.config.deviceId :
+                                 (facings && facings[a] === preferredFacing)) {
         return -1;
       }
       return 1;
@@ -124,7 +127,8 @@
           await this.modes.isSupported(this.modeConstraints.exact, deviceId));
       return [this.modeConstraints.exact];
     }
-    return this.modes.getModeCandidates(deviceId, this.config.mode);
+    return this.modes.getModeCandidates(
+        deviceId, this.config?.mode ?? this.initialMode);
   }
 
   private async *
@@ -134,23 +138,24 @@
 
     for (const deviceId of this.getDeviceIdCandidates(cameraInfo)) {
       for (const mode of await this.getModeCandidates(deviceId)) {
-        let resolCandidates;
-        let photoRs;
+        let resolutionCandidates;
+        let photoResolutions;
         if (deviceOperator !== null) {
-          resolCandidates = this.modes.getResolutionCandidates(mode, deviceId);
-          photoRs = await deviceOperator.getPhotoResolutions(deviceId);
+          resolutionCandidates =
+              this.modes.getResolutionCandidates(mode, deviceId);
+          photoResolutions = await deviceOperator.getPhotoResolutions(deviceId);
         } else {
-          resolCandidates =
+          resolutionCandidates =
               this.modes.getFakeResolutionCandidates(mode, deviceId);
-          photoRs = resolCandidates.map((c) => c.resolution);
+          photoResolutions = resolutionCandidates.map((c) => c.resolution);
         }
-        const maxResolution = photoRs.reduce(
+        const maxResolution = photoResolutions.reduce(
             (maxR, r) =>
                 r !== null && (maxR === null || r.area > maxR.area) ? r : maxR);
         for (const {
                resolution: captureResolution,
                previewCandidates,
-             } of resolCandidates) {
+             } of resolutionCandidates) {
           const videoSnapshotResolution =
               state.get(state.State.ENABLE_FULL_SIZED_VIDEO_SNAPSHOT) ?
               maxResolution :
@@ -214,15 +219,13 @@
         return false;
       }
 
-      const nextConfig: CameraConfig = {
+      this.listener.onTryingNewConfig({
         deviceId: c.deviceId,
-        facing: (c.deviceId !== null ?
-                     cameraInfo.getCamera3DeviceInfo(c.deviceId)?.facing :
-                     null) ??
-            Facing.NOT_SET,
+        facing: c.deviceId !== null ?
+            cameraInfo.getCamera3DeviceInfo(c.deviceId)?.facing ?? null :
+            null,
         mode: c.mode,
-      };
-      this.listener.onTryingNewConfig(nextConfig);
+      });
       this.modes.setCaptureParams(
           c.mode, c.constraints, c.captureResolution,
           c.videoSnapshotResolution);
@@ -230,18 +233,20 @@
         await this.modes.prepareDevice();
         const factory = this.modes.getModeFactory(c.mode);
         const stream = await this.preview.open(c.constraints);
-        // For legacy linux VCD, the facing and device id can only be known
+        // For non-ChromeOS VCD, the facing and device id can only be known
         // after preview is actually opened.
         const facing = this.preview.getFacing();
-        nextConfig.facing = facing;
         const deviceId = assertString(this.preview.getDeviceId());
-        nextConfig.deviceId = deviceId;
 
         await this.checkEnablePTZ(c);
         factory.setPreviewVideo(this.preview.getVideo());
         factory.setFacing(facing);
         await this.modes.updateMode(factory, stream, facing, deviceId);
-        this.config = nextConfig;
+        this.config = {
+          deviceId,
+          facing,
+          mode: c.mode,
+        };
         await this.listener.onUpdateConfig(this.config);
 
         return true;
@@ -262,7 +267,7 @@
             // We cannot get the camera facing from stream since it might
             // not be successfully opened. Therefore, we asked the camera
             // facing via Mojo API.
-            let facing = Facing.NOT_SET;
+            let facing: Facing|null = null;
             if (deviceOperator !== null) {
               facing = await deviceOperator.getCameraFacing(c.deviceId);
             }
@@ -344,7 +349,7 @@
       private readonly infoUpdater: DeviceInfoUpdater,
       private readonly listener: EventListener,
       preview: Preview,
-      defaultFacing: Facing,
+      defaultFacing: Facing|null,
       modeConstraints: ModeConstraints,
   ) {
     this.modes = new Modes(this.photoPreferrer, this.videoPreferrer);
diff --git a/ash/webui/camera_app_ui/resources/js/device/constraints_preferrer.ts b/ash/webui/camera_app_ui/resources/js/device/constraints_preferrer.ts
index 27d180f..175f398 100644
--- a/ash/webui/camera_app_ui/resources/js/device/constraints_preferrer.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/constraints_preferrer.ts
@@ -177,11 +177,11 @@
   /**
    * Sorts prefer resolutions.
    *
-   * @param prefR Preferred resolution.
+   * @param prefResolution Preferred resolution.
    * @return Return compare function for comparing based on preferred
    *     resolution.
    */
-  protected getPreferResolutionSort(prefR: Resolution):
+  protected getPreferResolutionSort(prefResolution: Resolution):
       (c0: CaptureCandidate, c1: CaptureCandidate) => number {
     return ({resolution: r1}, {resolution: r2}) => {
       if (r1 === null || r2 === null) {
@@ -191,19 +191,19 @@
         return 0;
       }
       // Exactly the preferred resolution.
-      if (r1.equals(prefR)) {
+      if (r1.equals(prefResolution)) {
         return -1;
       }
-      if (r2.equals(prefR)) {
+      if (r2.equals(prefResolution)) {
         return 1;
       }
 
       // Aspect ratio same as preferred resolution.
       if (!r1.aspectRatioEquals(r2)) {
-        if (r1.aspectRatioEquals(prefR)) {
+        if (r1.aspectRatioEquals(prefResolution)) {
           return -1;
         }
-        if (r2.aspectRatioEquals(prefR)) {
+        if (r2.aspectRatioEquals(prefResolution)) {
           return 1;
         }
       }
@@ -216,7 +216,7 @@
    *
    * @return Ratio as key, all resolutions with that ratio as value.
    */
-  protected groupResolutionRatio(rs: ResolutionList):
+  protected groupResolutionRatio(resolutions: ResolutionList):
       Map<number, ResolutionList> {
     const toSupportedPreviewRatio = (r: Resolution): number => {
       // Special aspect ratio mapping rule, see http://b/147986763.
@@ -227,7 +227,7 @@
     };
 
     const result = new Map<number, ResolutionList>();
-    for (const r of rs) {
+    for (const r of resolutions) {
       const ratio = toSupportedPreviewRatio(r);
       const ratios = result.get(ratio) ?? [];
       ratios.push(r);
@@ -242,6 +242,11 @@
  */
 const SUPPORTED_CONSTANT_FPS = [30, 60];
 
+interface VideoPreviewResolutions {
+  videoResolutions: ResolutionList;
+  previewResolutions: ResolutionList;
+}
+
 /**
  * Controller for handling video resolution preference.
  */
@@ -269,8 +274,8 @@
    * Maps from device id as key to video and preview resolutions of
    * same aspect ratio supported by that video device as value.
    */
-  private deviceVideoPreviewResolutionMap = new Map<
-      string, Array<{videoRs: ResolutionList, previewRs: ResolutionList}>>();
+  private deviceVideoPreviewResolutionMap =
+      new Map<string, VideoPreviewResolutions[]>();
 
   constructor() {
     super();
@@ -311,8 +316,9 @@
     if (!SUPPORTED_CONSTANT_FPS.includes(prefFps)) {
       return;
     }
-    SUPPORTED_CONSTANT_FPS.forEach(
-        (fps) => state.set(state.assertState(`fps-${fps}`), fps === prefFps));
+    for (const fps of SUPPORTED_CONSTANT_FPS) {
+      state.set(state.assertState(`fps-${fps}`), fps === prefFps);
+    }
     const resolutionFpses = this.prefFpses[deviceId] || {};
     resolutionFpses[resolution.toString()] = prefFps;
     this.prefFpses[deviceId] = resolutionFpses;
@@ -343,35 +349,40 @@
     this.supportedResolutions = new Map();
     this.constFpsInfo = {};
 
-    for (const {deviceId, videoResols, videoMaxFps, fpsRanges} of devices) {
+    for (const {deviceId, videoResolutions, videoMaxFps, fpsRanges} of
+             devices) {
       this.supportedResolutions.set(
-          deviceId, [...videoResols].sort((r1, r2) => r2.area - r1.area));
+          deviceId, [...videoResolutions].sort((r1, r2) => r2.area - r1.area));
 
       // Filter out preview resolution greater than 1920x1080 and 1600x1200.
-      const previewRatios = this.groupResolutionRatio(videoResols.filter(
+      const previewRatios = this.groupResolutionRatio(videoResolutions.filter(
           ({width, height}) => width <= 1920 && height <= 1200));
-      const videoRatios = this.groupResolutionRatio(videoResols);
-      const pairedResolutions:
-          Array<{videoRs: ResolutionList, previewRs: ResolutionList}> = [];
-      for (const [ratio, videoRs] of videoRatios) {
-        const previewRs = previewRatios.get(ratio);
-        if (previewRs === undefined) {
+      const videoRatios = this.groupResolutionRatio(videoResolutions);
+      const pairedResolutions: VideoPreviewResolutions[] = [];
+      for (const [ratio, videoResolutions] of videoRatios) {
+        const previewResolutions = previewRatios.get(ratio);
+        if (previewResolutions === undefined) {
           continue;
         }
-        pairedResolutions.push({videoRs, previewRs});
+        pairedResolutions.push({videoResolutions, previewResolutions});
       }
       this.deviceVideoPreviewResolutionMap.set(deviceId, pairedResolutions);
 
-      const findResol = (width: number, height: number): Resolution|undefined =>
-          videoResols.find((r) => r.width === width && r.height === height);
-      let prefR = this.getPrefResolution(deviceId) ?? findResol(1920, 1080) ??
-          findResol(1280, 720) ?? new Resolution(0, -1);
-      if (findResol(prefR.width, prefR.height) === undefined) {
-        prefR = videoResols.reduce(
+      const findResolution = (width: number, height: number): Resolution|
+          undefined => videoResolutions.find(
+              (r) => r.width === width && r.height === height);
+
+      let prefResolution = this.getPrefResolution(deviceId) ??
+          findResolution(1920, 1080) ?? findResolution(1280, 720) ??
+          new Resolution(0, -1);
+
+      if (findResolution(prefResolution.width, prefResolution.height) ===
+          undefined) {
+        prefResolution = videoResolutions.reduce(
             (maxR, r) => (maxR.area < r.area ? r : maxR),
             new Resolution(0, -1));
       }
-      this.prefResolution.set(deviceId, prefR);
+      this.prefResolution.set(deviceId, prefResolution);
 
       const constFpses =
           fpsRanges.filter(({minFps, maxFps}) => minFps === maxFps)
@@ -408,7 +419,8 @@
   }
 
   private getMultiStreamSortedCandidates(deviceId: string): CaptureCandidate[] {
-    const prefR = this.getPrefResolution(deviceId) ?? new Resolution(0, -1);
+    const prefResolution =
+        this.getPrefResolution(deviceId) ?? new Resolution(0, -1);
 
     /**
      * Maps specified video resolution to object of resolution and all supported
@@ -436,36 +448,35 @@
         };
 
     const toVideoCandidate =
-        ({videoRs,
-          previewRs}: {videoRs: ResolutionList, previewRs: ResolutionList}):
-            CaptureCandidate[] => {
-              let videoR = prefR;
-              if (!videoRs.some((r) => r.equals(prefR))) {
-                videoR = videoRs.reduce(
-                    (videoR, r) => (r.width > videoR.width ? r : videoR));
-              }
+        ({videoResolutions,
+          previewResolutions}: VideoPreviewResolutions): CaptureCandidate[] => {
+          let videoResolution = prefResolution;
+          if (!videoResolutions.some((r) => r.equals(prefResolution))) {
+            videoResolution = videoResolutions.reduce(
+                (videoR, r) => (r.width > videoR.width ? r : videoR));
+          }
 
-              return getFpses(videoR).map(
-                  ({fps}) => ({
-                    resolution: videoR,
-                    previewCandidates:
-                        this.sortPreview(previewRs, videoR)
-                            .map(({width, height}) => ({
-                                   deviceId,
-                                   audio: true,
-                                   video: {
-                                     frameRate: fps ? {exact: fps} :
-                                                      {min: 20, ideal: 30},
-                                     width,
-                                     height,
-                                   },
-                                 })),
-                  }));
-            };
+          return getFpses(videoResolution)
+              .map(({fps}) => ({
+                     resolution: videoResolution,
+                     previewCandidates:
+                         this.sortPreview(previewResolutions, videoResolution)
+                             .map(({width, height}) => ({
+                                    deviceId,
+                                    audio: true,
+                                    video: {
+                                      frameRate: fps ? {exact: fps} :
+                                                       {min: 20, ideal: 30},
+                                      width,
+                                      height,
+                                    },
+                                  })),
+                   }));
+        };
 
     return assertExists(this.deviceVideoPreviewResolutionMap.get(deviceId))
         .flatMap(toVideoCandidate)
-        .sort(this.getPreferResolutionSort(prefR));
+        .sort(this.getPreferResolutionSort(prefResolution));
   }
 
   private getSortedCandidatesInternal(deviceId: string): CaptureCandidate[] {
@@ -505,7 +516,8 @@
           },
         });
 
-    const prefR = this.getPrefResolution(deviceId) ?? new Resolution(0, -1);
+    const prefResolution =
+        this.getPrefResolution(deviceId) ?? new Resolution(0, -1);
     return [...assertExists(this.supportedResolutions.get(deviceId))]
         .flatMap(getFpses)
         .map(({r, fps}) => ({
@@ -514,7 +526,7 @@
                // to do video recording.
                previewCandidates: [toPreivewConstraints(r, fps)],
              }))
-        .sort(this.getPreferResolutionSort(prefR));
+        .sort(this.getPreferResolutionSort(prefResolution));
   }
 
   getSortedCandidates(deviceId: string): CaptureCandidate[] {
@@ -525,6 +537,11 @@
   }
 }
 
+interface CapturePreviewResolutions {
+  captureResolutions: ResolutionList;
+  previewResolutions: ResolutionList;
+}
+
 /**
  * Controller for handling photo resolution preference.
  */
@@ -533,8 +550,8 @@
    * Maps from device id as key to capture and preview resolutions of
    * same aspect ratio supported by that video device as value.
    */
-  private deviceCapturePreviewResolutionMap = new Map<
-      string, Array<{captureRs: ResolutionList, previewRs: ResolutionList}>>();
+  private deviceCapturePreviewResolutionMap =
+      new Map<string, CapturePreviewResolutions[]>();
 
   /**
    * Maps from device id as key to whether PTZ is support from device level.
@@ -558,35 +575,40 @@
     this.devicePTZSupportMap = new Map(
         devices.map(({deviceId, supportPTZ}) => [deviceId, supportPTZ]));
 
-    devices.forEach(({deviceId, photoResols, videoResols: previewResols}) => {
-      const previewRatios = this.groupResolutionRatio(previewResols);
-      const captureRatios = this.groupResolutionRatio(photoResols);
-      const pairedResolutions:
-          Array<{captureRs: ResolutionList, previewRs: ResolutionList}> = [];
-      for (const [ratio, captureRs] of captureRatios) {
-        const previewRs = previewRatios.get(ratio);
-        if (previewRs === undefined) {
+    for (const {
+           deviceId,
+           photoResolutions,
+           videoResolutions: previewResolutions,
+         } of devices) {
+      const previewRatios = this.groupResolutionRatio(previewResolutions);
+      const captureRatios = this.groupResolutionRatio(photoResolutions);
+      const pairedResolutions: CapturePreviewResolutions[] = [];
+      for (const [ratio, captureResolutions] of captureRatios) {
+        const previewResolutions = previewRatios.get(ratio);
+        if (previewResolutions === undefined) {
           continue;
         }
-        pairedResolutions.push({captureRs, previewRs});
+        pairedResolutions.push({captureResolutions, previewResolutions});
       }
 
       this.deviceCapturePreviewResolutionMap.set(deviceId, pairedResolutions);
       this.supportedResolutions.set(
           deviceId,
-          pairedResolutions.flatMap(({captureRs}) => captureRs)
+          pairedResolutions
+              .flatMap(({captureResolutions}) => captureResolutions)
               .sort((r1, r2) => r2.area - r1.area));
 
-      let prefR = this.getPrefResolution(deviceId) ?? new Resolution(0, -1);
-      const captureRs = this.supportedResolutions.get(deviceId);
-      assert(captureRs !== undefined);
-      if (!captureRs.some((r) => r.equals(prefR))) {
-        prefR = captureRs.reduce(
+      let prefResolution =
+          this.getPrefResolution(deviceId) ?? new Resolution(0, -1);
+      const captureResolutions = this.supportedResolutions.get(deviceId);
+      assert(captureResolutions !== undefined);
+      if (!captureResolutions.some((r) => r.equals(prefResolution))) {
+        prefResolution = captureResolutions.reduce(
             (maxR, r) => (maxR.area < r.area ? r : maxR),
             new Resolution(0, -1));
       }
-      this.prefResolution.set(deviceId, prefR);
-    });
+      this.prefResolution.set(deviceId, prefResolution);
+    }
     this.saveResolutionPreference('devicePhotoResolution');
   }
 
@@ -599,40 +621,42 @@
   }
 
   getSortedCandidates(deviceId: string): CaptureCandidate[] {
-    const prefR = this.getPrefResolution(deviceId) ?? new Resolution(0, -1);
+    const prefResolution =
+        this.getPrefResolution(deviceId) ?? new Resolution(0, -1);
     const supportPTZ = this.devicePTZSupportMap.get(deviceId) ?? false;
 
     const toCaptureCandidate =
-        ({captureRs, previewRs}:
-             {captureRs: ResolutionList,
-              previewRs: ResolutionList}): CaptureCandidate => {
-          let captureR = prefR;
-          if (!captureRs.some((r) => r.equals(prefR))) {
-            captureR = captureRs.reduce(
+        ({captureResolutions,
+          previewResolutions}: CapturePreviewResolutions): CaptureCandidate => {
+          let captureResolution = prefResolution;
+          if (!captureResolutions.some((r) => r.equals(prefResolution))) {
+            captureResolution = captureResolutions.reduce(
                 (captureR, r) => (r.width > captureR.width ? r : captureR));
           }
 
           // Use workaround for b/184089334 on PTZ camera to use preview frame
           // as photo result.
           if (supportPTZ &&
-              previewRs.find((r) => captureR.equals(r)) !== undefined) {
-            previewRs = [captureR];
+              previewResolutions.find((r) => captureResolution.equals(r)) !==
+                  undefined) {
+            previewResolutions = [captureResolution];
           }
 
           const previewCandidates =
-              this.sortPreview(previewRs, captureR).map(({width, height}) => ({
-                                                          deviceId,
-                                                          audio: false,
-                                                          video: {
-                                                            width,
-                                                            height,
-                                                          },
-                                                        }));
-          return {resolution: captureR, previewCandidates};
+              this.sortPreview(previewResolutions, captureResolution)
+                  .map(({width, height}) => ({
+                         deviceId,
+                         audio: false,
+                         video: {
+                           width,
+                           height,
+                         },
+                       }));
+          return {resolution: captureResolution, previewCandidates};
         };
 
     return assertExists(this.deviceCapturePreviewResolutionMap.get(deviceId))
         .map(toCaptureCandidate)
-        .sort(this.getPreferResolutionSort(prefR));
+        .sort(this.getPreferResolutionSort(prefResolution));
   }
 }
diff --git a/ash/webui/camera_app_ui/resources/js/device/device_info_updater.ts b/ash/webui/camera_app_ui/resources/js/device/device_info_updater.ts
index aa7e8a90..920abc2 100644
--- a/ash/webui/camera_app_ui/resources/js/device/device_info_updater.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/device_info_updater.ts
@@ -106,7 +106,9 @@
     } else {
       this.camera3DevicesInfo = null;
     }
-    this.deviceChangeListeners.forEach((l) => l(this));
+    for (const listener of this.deviceChangeListeners) {
+      listener(this);
+    }
   }
 
   /**
diff --git a/ash/webui/camera_app_ui/resources/js/device/mode/index.ts b/ash/webui/camera_app_ui/resources/js/device/mode/index.ts
index 95171d5b..37681ad 100644
--- a/ash/webui/camera_app_ui/resources/js/device/mode/index.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/mode/index.ts
@@ -320,11 +320,11 @@
       },
     };
 
-    [state.State.EXPERT, state.State.SAVE_METADATA].forEach((s) => {
+    for (const s of [state.State.EXPERT, state.State.SAVE_METADATA]) {
       state.addObserver(s, () => {
         this.updateSaveMetadata();
       });
-    });
+    }
   }
 
   initialize(handler: CaptureHandler): void {
diff --git a/ash/webui/camera_app_ui/resources/js/device/mode/mode_base.ts b/ash/webui/camera_app_ui/resources/js/device/mode/mode_base.ts
index 7407dce..d1dc421 100644
--- a/ash/webui/camera_app_ui/resources/js/device/mode/mode_base.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/mode/mode_base.ts
@@ -138,7 +138,7 @@
   /**
    * Camera facing of current mode.
    */
-  protected facing = Facing.NOT_SET;
+  protected facing: Facing|null = null;
 
   /**
    * @param constraints Constraints for preview stream.
diff --git a/ash/webui/camera_app_ui/resources/js/device/mode/photo.ts b/ash/webui/camera_app_ui/resources/js/device/mode/photo.ts
index 0e1a5373..c2c98ad0 100644
--- a/ash/webui/camera_app_ui/resources/js/device/mode/photo.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/mode/photo.ts
@@ -166,6 +166,7 @@
 
   produce(): ModeBase {
     assert(this.previewVideo !== null);
+    assert(this.facing !== null);
     return new Photo(
         this.previewVideo, this.facing, this.captureResolution, this.handler);
   }
diff --git a/ash/webui/camera_app_ui/resources/js/device/mode/portrait.ts b/ash/webui/camera_app_ui/resources/js/device/mode/portrait.ts
index 3cdad97..7f2b657 100644
--- a/ash/webui/camera_app_ui/resources/js/device/mode/portrait.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/mode/portrait.ts
@@ -102,6 +102,7 @@
 
   produce(): ModeBase {
     assert(this.previewVideo !== null);
+    assert(this.facing !== null);
     return new Portrait(
         this.previewVideo, this.facing, this.captureResolution,
         this.portraitHandler);
diff --git a/ash/webui/camera_app_ui/resources/js/device/mode/scan.ts b/ash/webui/camera_app_ui/resources/js/device/mode/scan.ts
index c3b7c7d..145be327 100644
--- a/ash/webui/camera_app_ui/resources/js/device/mode/scan.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/mode/scan.ts
@@ -91,6 +91,7 @@
 
   produce(): ModeBase {
     assert(this.previewVideo !== null);
+    assert(this.facing !== null);
     return new Scan(
         this.previewVideo,
         this.facing,
diff --git a/ash/webui/camera_app_ui/resources/js/device/mode/square.ts b/ash/webui/camera_app_ui/resources/js/device/mode/square.ts
index a4c11c7..bd748a82 100644
--- a/ash/webui/camera_app_ui/resources/js/device/mode/square.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/mode/square.ts
@@ -88,6 +88,7 @@
 export class SquareFactory extends PhotoFactory {
   produce(): ModeBase {
     assert(this.previewVideo !== null);
+    assert(this.facing !== null);
     return new Square(
         this.previewVideo, this.facing, this.captureResolution, this.handler);
   }
diff --git a/ash/webui/camera_app_ui/resources/js/device/mode/video.ts b/ash/webui/camera_app_ui/resources/js/device/mode/video.ts
index fb2e127f..fef84b32 100644
--- a/ash/webui/camera_app_ui/resources/js/device/mode/video.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/mode/video.ts
@@ -649,6 +649,7 @@
       };
     }
     assert(this.previewVideo !== null);
+    assert(this.facing !== null);
     return new Video(
         this.previewVideo, captureConstraints, this.captureResolution,
         this.snapshotResolution, this.facing, this.handler);
diff --git a/ash/webui/camera_app_ui/resources/js/device/preview.ts b/ash/webui/camera_app_ui/resources/js/device/preview.ts
index 37448d27..37e2107 100644
--- a/ash/webui/camera_app_ui/resources/js/device/preview.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/preview.ts
@@ -79,7 +79,7 @@
    */
   private focusMarker: symbol|null = null;
 
-  private facing = Facing.NOT_SET;
+  private facing: Facing|null = null;
 
   private deviceId: string|null = null;
 
@@ -133,7 +133,7 @@
   }
 
   getFacing(): Facing {
-    return this.facing;
+    return util.assertEnumVariant(Facing, this.facing);
   }
 
   getDeviceId(): string|null {
@@ -155,15 +155,7 @@
   }
 
   private async updateFacing() {
-    if (!(await DeviceOperator.isSupported())) {
-      this.facing = Facing.NOT_SET;
-      return;
-    }
     const {facingMode} = this.getVideoTrack().getSettings();
-    if (facingMode === undefined) {
-      this.facing = Facing.EXTERNAL;
-      return;
-    }
     switch (facingMode) {
       case 'user':
         this.facing = Facing.USER;
@@ -172,7 +164,8 @@
         this.facing = Facing.ENVIRONMENT;
         return;
       default:
-        throw new Error('Unknown facing: ' + facingMode);
+        this.facing = Facing.EXTERNAL;
+        return;
     }
   }
 
@@ -416,9 +409,9 @@
       return;
     }
 
-    dom.getAll('.metadata.value', HTMLElement).forEach((element) => {
+    for (const element of dom.getAll('.metadata.value', HTMLElement)) {
       element.style.display = 'none';
-    });
+    }
 
     const displayCategory = (selector: string, enabled: boolean) => {
       dom.get(selector, HTMLElement).classList.toggle('mode-on', enabled);
diff --git a/ash/webui/camera_app_ui/resources/js/device/type.ts b/ash/webui/camera_app_ui/resources/js/device/type.ts
index a092f87..5af41c2 100644
--- a/ash/webui/camera_app_ui/resources/js/device/type.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/type.ts
@@ -53,21 +53,31 @@
  * camera will be opened with.
  */
 export interface CameraConfig {
+  deviceId: string;
+  facing: Facing;
+  mode: Mode;
+}
+
+/**
+ * The next |CameraConfig| to be tried.
+ */
+export interface CameraConfigCandidate {
   /**
-   * May be null for device using legacy linux VCD.
+   * The only null case is for opening the default facing camera on non-ChromeOS
+   * VCD.
    */
   deviceId: string|null;
-
   /**
-   * May be Facing.NOT_SET for device using legacy linux VCD.
+   * On device using non-ChromeOS VCD, camera facing is unknown before opening
+   * the camera.
    */
-  facing: Facing;
+  facing: Facing|null;
   mode: Mode;
 }
 
 export interface CameraUI {
   onUpdateCapability?(cameraInfo: CameraInfo): void;
-  onTryingNewConfig?(config: CameraConfig): void;
+  onTryingNewConfig?(config: CameraConfigCandidate): void;
   onUpdateConfig?(config: CameraConfig): Promise<void>|void;
   onCameraUnavailable?(): void;
   onCameraAvailble?(): void;
diff --git a/ash/webui/camera_app_ui/resources/js/focus_ring.ts b/ash/webui/camera_app_ui/resources/js/focus_ring.ts
index f6e0c70d..3605410 100644
--- a/ash/webui/camera_app_ui/resources/js/focus_ring.ts
+++ b/ash/webui/camera_app_ui/resources/js/focus_ring.ts
@@ -2,10 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {
-  assert,
-  assertInstanceof,
-} from './assert.js';
+import {assert} from './assert.js';
 import {cssStyle} from './css.js';
 import * as dom from './dom.js';
 import {getStyleValueInPx} from './util.js';
@@ -77,9 +74,11 @@
     }
   };
 
-  dom.getAll('[tabindex]', HTMLElement).forEach(setup);
+  for (const el of dom.getAll('[tabindex]', HTMLElement)) {
+    setup(el);
+  }
   const observer = new MutationObserver((mutationList) => {
-    mutationList.forEach((mutation) => {
+    for (const mutation of mutationList) {
       assert(mutation.type === 'childList');
       // Only the newly added nodes with [tabindex] are considered here. So
       // simply adding class attribute on existing element will not work.
@@ -87,12 +86,11 @@
         if (!(node instanceof HTMLElement)) {
           continue;
         }
-        const el = assertInstanceof(node, HTMLElement);
-        if (el.hasAttribute('tabindex')) {
-          setup(el);
+        if (node.hasAttribute('tabindex')) {
+          setup(node);
         }
       }
-    });
+    }
   });
   observer.observe(document.body, {
     subtree: true,
diff --git a/ash/webui/camera_app_ui/resources/js/main.ts b/ash/webui/camera_app_ui/resources/js/main.ts
index 4940f6203..b69c41d 100644
--- a/ash/webui/camera_app_ui/resources/js/main.ts
+++ b/ash/webui/camera_app_ui/resources/js/main.ts
@@ -65,7 +65,7 @@
   constructor({perfLogger, intent, facing, mode: defaultMode}: {
     perfLogger: PerfLogger,
     intent: Intent|null,
-    facing: Facing,
+    facing: Facing|null,
     mode: Mode|null,
   }) {
     this.perfLogger = perfLogger;
@@ -125,7 +125,7 @@
    * Sets up toggles (checkbox and radio) by data attributes.
    */
   private setupToggles() {
-    dom.getAll('input', HTMLInputElement).forEach((element) => {
+    for (const element of dom.getAll('input', HTMLInputElement)) {
       element.addEventListener('keypress', (event) => {
         const e = assertInstanceof(event, KeyboardEvent);
         if (util.getShortcutIdentifier(e) === 'Enter') {
@@ -171,18 +171,19 @@
             localStorage.getBool(element.dataset['key'], element.checked);
         util.toggleChecked(element, value);
       }
-    });
+    }
   }
 
   /**
    * Sets up visual effect for all applicable elements.
    */
   private setupEffect() {
-    dom.getAll('.inkdrop', HTMLElement)
-        .forEach((el) => util.setInkdropEffect(el));
+    for (const el of dom.getAll('.inkdrop', HTMLElement)) {
+      util.setInkdropEffect(el);
+    }
 
     const observer = new MutationObserver((mutationList) => {
-      mutationList.forEach((mutation) => {
+      for (const mutation of mutationList) {
         assert(mutation.type === 'childList');
         // Only the newly added nodes with inkdrop class are considered here. So
         // simply adding class attribute on existing element will not work.
@@ -190,12 +191,11 @@
           if (!(node instanceof HTMLElement)) {
             continue;
           }
-          const el = assertInstanceof(node, HTMLElement);
-          if (el.classList.contains('inkdrop')) {
-            util.setInkdropEffect(el);
+          if (node.classList.contains('inkdrop')) {
+            util.setInkdropEffect(node);
           }
         }
-      });
+      }
     });
     observer.observe(document.body, {
       subtree: true,
@@ -350,7 +350,7 @@
  */
 function parseSearchParams(): {
   intent: Intent|null,
-  facing: Facing,
+  facing: Facing|null,
   mode: Mode|null,
   openFrom: string|null,
   autoTake: boolean,
@@ -358,8 +358,7 @@
   const url = new URL(window.location.href);
   const params = url.searchParams;
 
-  const facing =
-      checkEnumVariant(Facing, params.get('facing')) ?? Facing.NOT_SET;
+  const facing = checkEnumVariant(Facing, params.get('facing'));
 
   const mode = checkEnumVariant(Mode, params.get('mode'));
 
diff --git a/ash/webui/camera_app_ui/resources/js/mojo/image_capture.ts b/ash/webui/camera_app_ui/resources/js/mojo/image_capture.ts
index 2a22344..3f18a42 100644
--- a/ash/webui/camera_app_ui/resources/js/mojo/image_capture.ts
+++ b/ash/webui/camera_app_ui/resources/js/mojo/image_capture.ts
@@ -154,12 +154,12 @@
     }
 
     const cameraMetadataTagInverseLookup: Record<number, string> = {};
-    Object.entries(CameraMetadataTag).forEach(([key, value]) => {
+    for (const [key, value] of Object.entries(CameraMetadataTag)) {
       if (key === 'MIN_VALUE' || key === 'MAX_VALUE') {
         return;
       }
       cameraMetadataTagInverseLookup[value] = key;
-    });
+    }
 
     const callback = (metadata: CameraMetadata) => {
       const parsedMetadata: Record<string, unknown> = {};
diff --git a/ash/webui/camera_app_ui/resources/js/nav.ts b/ash/webui/camera_app_ui/resources/js/nav.ts
index e350560..a4e8ce08 100644
--- a/ash/webui/camera_app_ui/resources/js/nav.ts
+++ b/ash/webui/camera_app_ui/resources/js/nav.ts
@@ -60,14 +60,14 @@
   // Restore the view's child elements' tabindex and then focus the view.
   const view = allViews[index];
   view.root.setAttribute('aria-hidden', 'false');
-  dom.getAllFrom(view.root, '[tabindex]', HTMLElement).forEach((element) => {
+  for (const element of dom.getAllFrom(view.root, '[tabindex]', HTMLElement)) {
     if (element.dataset['tabindex'] === undefined) {
       // First activation, no need to restore tabindex from data-tabindex.
-      return;
+      continue;
     }
     element.setAttribute('tabindex', element.dataset['tabindex']);
     element.removeAttribute('data-tabindex');
-  });
+  }
   view.focus();
 }
 
@@ -79,11 +79,11 @@
 function deactivate(index: number) {
   const view = allViews[index];
   view.root.setAttribute('aria-hidden', 'true');
-  dom.getAllFrom(view.root, '[tabindex]', HTMLElement).forEach((element) => {
+  for (const element of dom.getAllFrom(view.root, '[tabindex]', HTMLElement)) {
     element.dataset['tabindex'] =
         assertExists(element.getAttribute('tabindex'));
     element.setAttribute('tabindex', '-1');
-  });
+  }
   const activeElement = document.activeElement;
   if (activeElement instanceof HTMLElement) {
     activeElement.blur();
diff --git a/ash/webui/camera_app_ui/resources/js/perf.ts b/ash/webui/camera_app_ui/resources/js/perf.ts
index 534f53d..f1d58433 100644
--- a/ash/webui/camera_app_ui/resources/js/perf.ts
+++ b/ash/webui/camera_app_ui/resources/js/perf.ts
@@ -101,7 +101,9 @@
 
     const duration = performance.now() - startTime;
     ChromeHelper.getInstance().stopTracing(event);
-    this.listeners.forEach((listener) => listener({event, duration, perfInfo}));
+    for (const listener of this.listeners) {
+      listener({event, duration, perfInfo});
+    }
   }
 
   /**
diff --git a/ash/webui/camera_app_ui/resources/js/tooltip.ts b/ash/webui/camera_app_ui/resources/js/tooltip.ts
index 56dab01..7a5bc01 100644
--- a/ash/webui/camera_app_ui/resources/js/tooltip.ts
+++ b/ash/webui/camera_app_ui/resources/js/tooltip.ts
@@ -94,7 +94,7 @@
 export function setup(elements: NodeListOf<HTMLElement>):
     NodeListOf<HTMLElement> {
   wrapper = dom.get('#tooltip', HTMLElement);
-  elements.forEach((el) => {
+  for (const el of elements) {
     const handler = () => {
       // Handler hides tooltip only when it's for the element.
       if (el === hovered) {
@@ -104,6 +104,6 @@
     el.addEventListener('mouseout', handler);
     el.addEventListener('click', handler);
     el.addEventListener('mouseover', () => show(el));
-  });
+  }
   return elements;
 }
diff --git a/ash/webui/camera_app_ui/resources/js/type.ts b/ash/webui/camera_app_ui/resources/js/type.ts
index b4076c2..e8a965c 100644
--- a/ash/webui/camera_app_ui/resources/js/type.ts
+++ b/ash/webui/camera_app_ui/resources/js/type.ts
@@ -93,7 +93,7 @@
   VIRTUAL_USER = 'virtual_user',
   VIRTUAL_ENV = 'virtual_environment',
   VIRTUAL_EXT = 'virtual_external',
-  NOT_SET = '(not set)',
+  UNKNOWN = 'unknown',
 }
 
 export enum ViewName {
diff --git a/ash/webui/camera_app_ui/resources/js/util.ts b/ash/webui/camera_app_ui/resources/js/util.ts
index e473cc51..e9e9ee9 100644
--- a/ash/webui/camera_app_ui/resources/js/util.ts
+++ b/ash/webui/camera_app_ui/resources/js/util.ts
@@ -109,21 +109,23 @@
   const setAriaLabel = (element: HTMLElement, attr: string) =>
       element.setAttribute('aria-label', getMessage(element, attr));
 
-  getElements('i18n-text')
-      .forEach(
-          (element) => element.textContent = getMessage(element, 'i18n-text'));
-  getElements('i18n-tooltip-true')
-      .forEach(
-          (element) => element.setAttribute(
-              'tooltip-true', getMessage(element, 'i18n-tooltip-true')));
-  getElements('i18n-tooltip-false')
-      .forEach(
-          (element) => element.setAttribute(
-              'tooltip-false', getMessage(element, 'i18n-tooltip-false')));
-  getElements('i18n-aria')
-      .forEach((element) => setAriaLabel(element, 'i18n-aria'));
-  tooltip.setup(getElements('i18n-label'))
-      .forEach((element) => setAriaLabel(element, 'i18n-label'));
+  for (const element of getElements('i18n-text')) {
+    element.textContent = getMessage(element, 'i18n-text');
+  }
+  for (const element of getElements('i18n-tooltip-true')) {
+    element.setAttribute(
+        'tooltip-true', getMessage(element, 'i18n-tooltip-true'));
+  }
+  for (const element of getElements('i18n-tooltip-false')) {
+    element.setAttribute(
+        'tooltip-false', getMessage(element, 'i18n-tooltip-false'));
+  }
+  for (const element of getElements('i18n-aria')) {
+    setAriaLabel(element, 'i18n-aria');
+  }
+  for (const element of tooltip.setup(getElements('i18n-label'))) {
+    setAriaLabel(element, 'i18n-label');
+  }
 }
 
 /**
diff --git a/ash/webui/camera_app_ui/resources/js/views/camera.ts b/ash/webui/camera_app_ui/resources/js/views/camera.ts
index 26568de5..8546027 100644
--- a/ash/webui/camera_app_ui/resources/js/views/camera.ts
+++ b/ash/webui/camera_app_ui/resources/js/views/camera.ts
@@ -102,7 +102,7 @@
 
   protected readonly review = new review.Review();
 
-  protected facing = Facing.NOT_SET;
+  protected facing: Facing|null = null;
 
   protected shutterType = metrics.ShutterType.UNKNOWN;
 
@@ -275,6 +275,13 @@
     await this.initScanMode();
   }
 
+  /**
+   * Gets current facing after |initialize()|.
+   */
+  protected getFacing(): Facing {
+    return util.assertEnumVariant(Facing, this.facing);
+  }
+
   private updateModeUI(mode: Mode) {
     for (const m of Object.values(Mode)) {
       state.set(m, m === mode);
@@ -397,8 +404,11 @@
       if (!state.get(state.State.CAMERA_CONFIGURING) && state.get(Mode.SCAN) &&
           this.scanOptions.isDocumentModeEanbled() &&
           nav.isTopMostView(this.name)) {
-        dom.getAll('button.shutter', HTMLButtonElement)
-            .forEach((btn) => btn.offsetParent && btn.focus());
+        for (const btn of dom.getAll('button.shutter', HTMLButtonElement)) {
+          if (btn.offsetParent !== null) {
+            btn.focus();
+          }
+        }
       }
     };
     state.addObserver(state.State.CAMERA_CONFIGURING, checkRefocus);
@@ -424,8 +434,11 @@
       }
 
       // Avoid focusing invisible shutters.
-      dom.getAll('button.shutter', HTMLButtonElement)
-          .forEach((btn) => btn.offsetParent && btn.focus());
+      for (const btn of dom.getAll('button.shutter', HTMLButtonElement)) {
+        if (btn.offsetParent !== null) {
+          btn.focus();
+        }
+      }
     })();
   }
 
@@ -474,7 +487,10 @@
             assertInstanceof(e, Error));
       } finally {
         this.take = null;
-        state.set(state.State.TAKING, false, {hasError, facing: this.facing});
+        state.set(state.State.TAKING, false, {
+          hasError,
+          facing: this.getFacing(),
+        });
         this.focus();  // Refocus the visible shutter button for ChromeVox.
       }
     })();
@@ -505,7 +521,7 @@
   async handleVideoSnapshot({resolution, blob, timestamp, metadata}:
                                 PhotoResult): Promise<void> {
     metrics.sendCaptureEvent({
-      facing: this.facing,
+      facing: this.getFacing(),
       resolution,
       shutterType: this.shutterType,
       isVideoSnapshot: true,
@@ -535,7 +551,7 @@
           await this.checkPhotoResult(pendingPhotoResult);
 
       metrics.sendCaptureEvent({
-        facing: this.facing,
+        facing: this.getFacing(),
         resolution,
         shutterType: this.shutterType,
         isVideoSnapshot: false,
@@ -550,7 +566,7 @@
       }
       state.set(
           PerfEvent.PHOTO_CAPTURE_POST_PROCESSING, false,
-          {resolution, facing: this.facing});
+          {resolution, facing: this.getFacing()});
     } catch (e) {
       state.set(
           PerfEvent.PHOTO_CAPTURE_POST_PROCESSING, false, {hasError: true});
@@ -568,7 +584,7 @@
           await this.checkPhotoResult(pendingReference);
 
       metrics.sendCaptureEvent({
-        facing: this.facing,
+        facing: this.getFacing(),
         resolution,
         shutterType: this.shutterType,
         isVideoSnapshot: false,
@@ -607,7 +623,7 @@
     } finally {
       state.set(
           PerfEvent.PORTRAIT_MODE_CAPTURE_POST_PROCESSING, false,
-          {hasError, facing: this.facing});
+          {hasError, facing: this.getFacing()});
     }
   }
 
@@ -685,7 +701,7 @@
         let fixType = metrics.DocFixType.NONE;
         const sendEvent = (docResult: metrics.DocResultType) => {
           metrics.sendCaptureEvent({
-            facing: this.facing,
+            facing: this.getFacing(),
             resolution: originImage.resolution,
             shutterType: this.shutterType,
             docResult,
@@ -818,7 +834,7 @@
     const sendEvent = (gifResult: metrics.GifResultType) => {
       metrics.sendCaptureEvent({
         recordType: metrics.RecordType.GIF,
-        facing: this.facing,
+        facing: this.getFacing(),
         resolution,
         duration,
         shutterType: this.shutterType,
@@ -863,7 +879,7 @@
     try {
       metrics.sendCaptureEvent({
         recordType: metrics.RecordType.NORMAL_VIDEO,
-        facing: this.facing,
+        facing: this.getFacing(),
         duration,
         resolution,
         shutterType: this.shutterType,
@@ -872,7 +888,7 @@
       await this.resultSaver.finishSaveVideo(videoSaver);
       state.set(
           PerfEvent.VIDEO_CAPTURE_POST_PROCESSING, false,
-          {resolution, facing: this.facing});
+          {resolution, facing: this.getFacing()});
     } catch (e) {
       state.set(
           PerfEvent.VIDEO_CAPTURE_POST_PROCESSING, false, {hasError: true});
diff --git a/ash/webui/camera_app_ui/resources/js/views/camera/document_corner_overlay.ts b/ash/webui/camera_app_ui/resources/js/views/camera/document_corner_overlay.ts
index 44597e1e..76b65f6 100644
--- a/ash/webui/camera_app_ui/resources/js/views/camera/document_corner_overlay.ts
+++ b/ash/webui/camera_app_ui/resources/js/views/camera/document_corner_overlay.ts
@@ -356,14 +356,14 @@
     });
 
     // Set start of dot transition.
-    starts.forEach((corn, idx) => {
+    for (const [idx, corn] of starts.entries()) {
       const prevIdx = (idx + 3) % 4;
       const nextIdx = (idx + 1) % 4;
       this.corners[idx].place(corn, starts[prevIdx], starts[nextIdx]);
-    });
+    }
 
     // Set start of line transition.
-    this.sides.forEach((line, i) => {
+    for (const [i, line] of this.sides.entries()) {
       const startCorn = starts[i];
       const startCorn2 = starts[(i + 1) % 4];
       const startSide = vectorFromPoints(startCorn2, startCorn);
@@ -372,18 +372,18 @@
         angle: startSide.cssRotateAngle(),
         length: startSide.length(),
       });
-    });
+    }
 
     void this.cornerContainer.offsetParent;  // Force start state of transition.
 
     // Set end of dot transition.
-    corners.forEach((corn, i) => {
+    for (const [i, corn] of corners.entries()) {
       const prevIdx = (i + 3) % 4;
       const nextIdx = (i + 1) % 4;
       this.corners[i].place(corn, corners[prevIdx], corners[nextIdx]);
-    });
+    }
 
-    this.sides.forEach((line, i) => {
+    for (const [i, line] of this.sides.entries()) {
       const endCorn = corners[i];
       const endCorn2 = corners[(i + 1) % 4];
       const endSide = vectorFromPoints(endCorn2, endCorn);
@@ -392,19 +392,19 @@
         angle: endSide.cssRotateAngle(),
         length: endSide.length(),
       });
-    });
+    }
   }
 
   /**
    * Place first 4 corners on the overlay and play settle animation.
    */
   private updateCorners(corners: Point[]) {
-    corners.forEach((corn, i) => {
+    for (const [i, corn] of corners.entries()) {
       const prevIdx = (i + 3) % 4;
       const nextIdx = (i + 1) % 4;
       this.corners[i].place(corn, corners[prevIdx], corners[nextIdx]);
-    });
-    this.sides.forEach((line, i) => {
+    }
+    for (const [i, line] of this.sides.entries()) {
       const corn = corners[i];
       const corn2 = corners[(i + 1) % 4];
       const side = vectorFromPoints(corn2, corn);
@@ -413,7 +413,7 @@
         angle: side.cssRotateAngle(),
         length: side.length(),
       });
-    });
+    }
   }
 
   /**
diff --git a/ash/webui/camera_app_ui/resources/js/views/camera/options.ts b/ash/webui/camera_app_ui/resources/js/views/camera/options.ts
index 123ce81..b6402e6 100644
--- a/ash/webui/camera_app_ui/resources/js/views/camera/options.ts
+++ b/ash/webui/camera_app_ui/resources/js/views/camera/options.ts
@@ -110,8 +110,9 @@
 
   private updateVideoConstFpsOption(prefFps: number|null) {
     this.toggleFps.checked = prefFps === 60;
-    SUPPORTED_CONSTANT_FPS.forEach(
-        (fps) => state.set(state.assertState(`fps-${fps}`), fps === prefFps));
+    for (const fps of SUPPORTED_CONSTANT_FPS) {
+      state.set(state.assertState(`fps-${fps}`), fps === prefFps);
+    }
   }
 
   onUpdateCapability(cameraInfo: CameraInfo): void {
diff --git a/ash/webui/camera_app_ui/resources/js/views/camera/scan_options.ts b/ash/webui/camera_app_ui/resources/js/views/camera/scan_options.ts
index a85a090..28b71f7 100644
--- a/ash/webui/camera_app_ui/resources/js/views/camera/scan_options.ts
+++ b/ash/webui/camera_app_ui/resources/js/views/camera/scan_options.ts
@@ -75,24 +75,24 @@
     this.documentCornerOverylay = new DocumentCornerOverlay(
         (p) => this.cameraManager.setPointOfInterest(p));
 
-    [this.photoBarcodeOption, ...this.scanOptions].forEach((opt) => {
-      opt.addEventListener('click', (evt) => {
+    for (const option of [this.photoBarcodeOption, ...this.scanOptions]) {
+      option.addEventListener('click', (evt) => {
         if (state.get(state.State.CAMERA_CONFIGURING)) {
           evt.preventDefault();
         }
       });
-    });
+    }
     this.photoBarcodeOption.addEventListener('change', () => {
       this.updateOption(
           this.photoBarcodeOption.checked ? ScanType.BARCODE : null);
     });
-    this.scanOptions.forEach((opt) => {
-      opt.addEventListener('change', () => {
-        if (opt.checked) {
+    for (const option of this.scanOptions) {
+      option.addEventListener('change', () => {
+        if (option.checked) {
           this.updateOption(this.getToggledScanOption());
         }
       });
-    });
+    }
   }
 
   /**
diff --git a/ash/webui/camera_app_ui/resources/js/views/camera_intent.ts b/ash/webui/camera_app_ui/resources/js/views/camera_intent.ts
index d2fb8ac7..adcdc40 100644
--- a/ash/webui/camera_app_ui/resources/js/views/camera_intent.ts
+++ b/ash/webui/camera_app_ui/resources/js/views/camera_intent.ts
@@ -87,7 +87,7 @@
         ],
       }));
       metrics.sendCaptureEvent({
-        facing: this.facing,
+        facing: this.getFacing(),
         ...metricArgs,
         intentResult: confirmed ? metrics.IntentResultType.CONFIRMED :
                                   metrics.IntentResultType.CANCELED,
diff --git a/ash/webui/camera_app_ui/resources/js/views/crop_document.ts b/ash/webui/camera_app_ui/resources/js/views/crop_document.ts
index 08348984..e241704 100644
--- a/ash/webui/camera_app_ui/resources/js/views/crop_document.ts
+++ b/ash/webui/camera_app_ui/resources/js/views/crop_document.ts
@@ -204,7 +204,7 @@
       return new Size(width, height);
     })();
 
-    this.corners.forEach((corn) => {
+    for (const corn of this.corners) {
       // Start dragging on one corner.
       corn.el.addEventListener('pointerdown', (e) => {
         e.preventDefault();
@@ -286,7 +286,7 @@
           clearKeydown();
         }
       });
-    });
+    }
 
     // Stop dragging.
     for (const eventName of ['pointerup', 'pointerleave', 'pointercancel']) {
@@ -486,23 +486,24 @@
   private updateCornerEl() {
     const cords = this.corners.map(({pt: {x, y}}) => `${x},${y}`).join(' ');
     this.cropArea.setAttribute('points', cords);
-    this.corners.forEach((corn) => {
+    for (const corn of this.corners) {
       const style = corn.el.attributeStyleMap;
       style.set('left', CSS.px(corn.pt.x));
       style.set('top', CSS.px(corn.pt.y));
-    });
+    }
   }
 
   private updateCornerElAriaLabel() {
-    [I18nString.LABEL_DOCUMENT_TOP_LEFT_CORNER,
-     I18nString.LABEL_DOCUMENT_BOTTOM_LEFT_CORNER,
-     I18nString.LABEL_DOCUMENT_BOTTOM_RIGHT_CORNER,
-     I18nString.LABEL_DOCUMENT_TOP_RIGHT_CORNER,
-    ].forEach((label, index) => {
+    for (const [index, label] of
+             [I18nString.LABEL_DOCUMENT_TOP_LEFT_CORNER,
+              I18nString.LABEL_DOCUMENT_BOTTOM_LEFT_CORNER,
+              I18nString.LABEL_DOCUMENT_BOTTOM_RIGHT_CORNER,
+              I18nString.LABEL_DOCUMENT_TOP_RIGHT_CORNER,
+    ].entries()) {
       const cornEl =
           this.corners[(this.rotation + index) % this.corners.length].el;
       cornEl.setAttribute('i18n-aria', label);
-    });
+    }
     util.setupI18nElements(this.root);
   }
 
@@ -532,18 +533,18 @@
 
     // Update corner space.
     if (this.cornerSpaceSize === null) {
-      this.initialCorners.forEach(({x, y}, idx) => {
+      for (const [idx, {x, y}] of this.initialCorners.entries()) {
         this.corners[idx].pt = new Point(x * newImageW, y * newImageH);
-      });
+      }
       this.initialCorners = [];
     } else {
       const oldImageW = this.cornerSpaceSize?.width || newImageW;
       const oldImageH = this.cornerSpaceSize?.height || newImageH;
-      this.corners.forEach((corn) => {
+      for (const corn of this.corners) {
         corn.pt = new Point(
             corn.pt.x / oldImageW * newImageW,
             corn.pt.y / oldImageH * newImageH);
-      });
+      }
     }
     this.cornerSpaceSize = new Size(newImageW, newImageH);
 
diff --git a/ash/webui/camera_app_ui/resources/js/views/settings.ts b/ash/webui/camera_app_ui/resources/js/views/settings.ts
index e93f20e..b02cf59 100644
--- a/ash/webui/camera_app_ui/resources/js/views/settings.ts
+++ b/ash/webui/camera_app_ui/resources/js/views/settings.ts
@@ -30,8 +30,8 @@
  * available resolutions for a particular video device.
  */
 interface ResolutionConfig {
-  prefResol: Resolution;
-  resols: ResolutionList;
+  prefResolution: Resolution;
+  resolutions: ResolutionList;
 }
 
 /**
@@ -69,12 +69,13 @@
 
     dom.getFrom(this.root, '.menu-header button', HTMLButtonElement)
         .addEventListener('click', () => this.leave());
-    dom.getAllFrom(this.root, '.menu-item', HTMLElement).forEach((element) => {
+    for (const element of dom.getAllFrom(
+             this.root, '.menu-item', HTMLElement)) {
       const handler = itemHandlers[element.id];
       if (handler !== undefined) {
         element.addEventListener('click', handler);
       }
-    });
+    }
 
     this.defaultFocus = dom.getFrom(this.root, '[tabindex]', HTMLElement);
 
@@ -223,11 +224,11 @@
   readonly videoResolutionSettings =
       new BaseSettings(ViewName.VIDEO_RESOLUTION_SETTINGS);
 
-  private readonly resMenu: HTMLDivElement;
+  private readonly resolutionMenu: HTMLDivElement;
 
-  private readonly videoResMenu: HTMLDivElement;
+  private readonly videoResolutionMenu: HTMLDivElement;
 
-  private readonly photoResMenu: HTMLDivElement;
+  private readonly photoResolutionMenu: HTMLDivElement;
 
   private cameraAvailble = false;
 
@@ -281,12 +282,12 @@
         })(),
     );
 
-    this.resMenu = dom.getFrom(this.root, 'div.menu', HTMLDivElement);
+    this.resolutionMenu = dom.getFrom(this.root, 'div.menu', HTMLDivElement);
 
-    this.videoResMenu = dom.getFrom(
+    this.videoResolutionMenu = dom.getFrom(
         this.videoResolutionSettings.root, 'div.menu', HTMLDivElement);
 
-    this.photoResMenu = dom.getFrom(
+    this.photoResolutionMenu = dom.getFrom(
         this.photoResolutionSettings.root, 'div.menu', HTMLDivElement);
 
     state.addObserver(state.State.TAKING, () => {
@@ -318,21 +319,22 @@
         this.frontSetting = this.backSetting = null;
         this.externalSettings = [];
 
-        devices.forEach(({deviceId, facing, photoResols, videoResols}) => {
+        for (const {deviceId, facing, photoResolutions, videoResolutions} of
+                 devices) {
           const deviceSetting = {
             deviceId,
             photo: {
-              prefResol: assertInstanceof(
+              prefResolution: assertInstanceof(
                   cameraManager.getPrefPhotoResolution(deviceId), Resolution),
-              resols:
-                  /* Filter out resolutions of megapixels < 0.1 i.e. megapixels
-                   * 0.0 */
-                  photoResols.filter((r) => r.area >= 100000),
+              resolutions:
+                  /* Filter out resolutions of megapixels < 0.1 i.e.
+                   * megapixels 0.0 */
+                  photoResolutions.filter((r) => r.area >= 100000),
             },
             video: {
-              prefResol: assertInstanceof(
+              prefResolution: assertInstanceof(
                   cameraManager.getPrefVideoResolution(deviceId), Resolution),
-              resols: videoResols,
+              resolutions: videoResolutions,
             },
           };
           switch (facing) {
@@ -350,7 +352,7 @@
                   ErrorType.UNKNOWN_FACING, ErrorLevel.ERROR,
                   new Error(`Ignore device of unknown facing: ${facing}`));
           }
-        });
+        }
         this.updateResolutions();
       },
       onUpdateConfig: (config: CameraConfig) => {
@@ -362,14 +364,14 @@
           return;
         }
         if (config.mode === Mode.VIDEO) {
-          const prefResol = cameraManager.getPrefVideoResolution(deviceId);
-          if (prefResol !== null) {
-            this.updateSelectedVideoResolution(deviceId, prefResol);
+          const prefResolution = cameraManager.getPrefVideoResolution(deviceId);
+          if (prefResolution !== null) {
+            this.updateSelectedVideoResolution(deviceId, prefResolution);
           }
         } else {
-          const prefResol = cameraManager.getPrefPhotoResolution(deviceId);
-          if (prefResol !== null) {
-            this.updateSelectedPhotoResolution(deviceId, prefResol);
+          const prefResolution = cameraManager.getPrefPhotoResolution(deviceId);
+          if (prefResolution !== null) {
+            this.updateSelectedPhotoResolution(deviceId, prefResolution);
           }
         }
       },
@@ -377,9 +379,9 @@
   }
 
   private updateOptionAvailability(): void {
-    dom.getAll('.resolution-option>input', HTMLInputElement).forEach((e) => {
+    for (const e of dom.getAll('.resolution-option>input', HTMLInputElement)) {
       e.disabled = !this.cameraAvailble || state.get(state.State.TAKING);
-    });
+    }
   }
 
 
@@ -393,38 +395,40 @@
   /**
    * Template for generating option text from photo resolution width and height.
    *
-   * @param r Resolution of text to be generated.
+   * @param resolution Resolution of text to be generated.
    * @param resolutions All available resolutions.
    * @return Text shown on resolution option item.
    */
-  private photoOptTextTempl(r: Resolution, resolutions: ResolutionList):
-      string {
+  private photoOptionTextTemplate(
+      resolution: Resolution, resolutions: ResolutionList): string {
     const gcd = (a: number, b: number): number => (a === 0 ? b : gcd(b % a, a));
     const toMegapixel = ({area}: Resolution): number =>
         area >= 1e6 ? Math.round(area / 1e6) : Math.round(area / 1e5) / 10;
-    const d = gcd(r.width, r.height);
+    const d = gcd(resolution.width, resolution.height);
+
     if (resolutions.some(
-            (findR) => !findR.equals(r) && r.aspectRatioEquals(findR) &&
-                toMegapixel(r) === toMegapixel(findR))) {
+            (r) => !r.equals(resolution) && resolution.aspectRatioEquals(r) &&
+                toMegapixel(resolution) === toMegapixel(r))) {
       return loadTimeData.getI18nMessage(
-          I18nString.LABEL_DETAIL_PHOTO_RESOLUTION, r.width / d, r.height / d,
-          r.width, r.height, toMegapixel(r));
+          I18nString.LABEL_DETAIL_PHOTO_RESOLUTION, resolution.width / d,
+          resolution.height / d, resolution.width, resolution.height,
+          toMegapixel(resolution));
     } else {
       return loadTimeData.getI18nMessage(
-          I18nString.LABEL_PHOTO_RESOLUTION, r.width / d, r.height / d,
-          toMegapixel(r));
+          I18nString.LABEL_PHOTO_RESOLUTION, resolution.width / d,
+          resolution.height / d, toMegapixel(resolution));
     }
   }
 
   /**
    * Template for generating option text from video resolution width and height.
    *
-   * @param r Resolution of text to be generated.
+   * @param resolution Resolution of text to be generated.
    * @return Text shown on resolution option item.
    */
-  private videoOptTextTempl(r: Resolution): string {
+  private videoOptionTextTemplate(resolution: Resolution): string {
     return loadTimeData.getI18nMessage(
-        I18nString.LABEL_VIDEO_RESOLUTION, r.height, r.width);
+        I18nString.LABEL_VIDEO_RESOLUTION, resolution.height, resolution.width);
   }
 
   /**
@@ -444,30 +448,36 @@
    * Updates resolution information of front, back camera and external cameras.
    */
   private updateResolutions() {
-    const prepItem =
-        (item: HTMLElement, id: string, {prefResol, resols}: ResolutionConfig,
-         optTextTempl: (prefResol: Resolution, resols: ResolutionList) =>
-             string) => {
+    const prepareItem =
+        (item: HTMLElement, id: string,
+         {prefResolution, resolutions}: ResolutionConfig,
+         optionTextTemplate:
+             (prefResolutions: Resolution, resolutions: ResolutionList) =>
+                 string) => {
           item.dataset['deviceId'] = id;
-          item.classList.toggle('multi-option', resols.length > 1);
+          item.classList.toggle('multi-option', resolutions.length > 1);
           dom.getFrom(item, '.description>span', HTMLSpanElement).textContent =
-              optTextTempl(prefResol, resols);
+              optionTextTemplate(prefResolution, resolutions);
         };
 
     // Update front camera setting
     state.set(state.State.HAS_FRONT_CAMERA, this.frontSetting !== null);
     if (this.frontSetting) {
       const {deviceId, photo, video} = this.frontSetting;
-      prepItem(this.frontPhotoItem, deviceId, photo, this.photoOptTextTempl);
-      prepItem(this.frontVideoItem, deviceId, video, this.videoOptTextTempl);
+      prepareItem(
+          this.frontPhotoItem, deviceId, photo, this.photoOptionTextTemplate);
+      prepareItem(
+          this.frontVideoItem, deviceId, video, this.videoOptionTextTemplate);
     }
 
     // Update back camera setting
     state.set(state.State.HAS_BACK_CAMERA, this.backSetting !== null);
     if (this.backSetting) {
       const {deviceId, photo, video} = this.backSetting;
-      prepItem(this.backPhotoItem, deviceId, photo, this.photoOptTextTempl);
-      prepItem(this.backVideoItem, deviceId, video, this.videoOptTextTempl);
+      prepareItem(
+          this.backPhotoItem, deviceId, photo, this.photoOptionTextTemplate);
+      prepareItem(
+          this.backVideoItem, deviceId, video, this.videoOptionTextTemplate);
     }
 
     // Update external camera settings
@@ -475,23 +485,23 @@
     // focused item in both previous and current list, pop out all items in
     // previous list except those having same deviceId as focused one and
     // recreate all other items from current list.
-    const prevFocus = this.resMenu.querySelector<HTMLElement>(
+    const prevFocused = this.resolutionMenu.querySelector<HTMLElement>(
         '.menu-item.external-camera:focus');
-    const prevFId = prevFocus?.dataset['deviceId'] ?? null;
-    const focusIdx =
-        this.externalSettings.findIndex(({deviceId}) => deviceId === prevFId);
-    const fTitle = this.resMenu.querySelector<HTMLElement>(
-        `.external-camera.title-item[data-device-id="${prevFId}"]`);
-    const focusedId = focusIdx === -1 ? null : prevFId;
+    const prevFocusedId = prevFocused?.dataset['deviceId'] ?? null;
+    const focusedIdx = this.externalSettings.findIndex(
+        ({deviceId}) => deviceId === prevFocusedId);
+    const prevFocusedTitle = this.resolutionMenu.querySelector<HTMLElement>(
+        `.external-camera.title-item[data-device-id="${prevFocusedId}"]`);
+    const focusedId = focusedIdx === -1 ? null : prevFocusedId;
 
     for (const element of dom.getAllFrom(
-             this.resMenu, '.menu-item.external-camera', HTMLElement)) {
+             this.resolutionMenu, '.menu-item.external-camera', HTMLElement)) {
       if (element.dataset['deviceId'] !== focusedId) {
         assertExists(element.parentNode).removeChild(element);
       }
     }
 
-    this.externalSettings.forEach((config, index) => {
+    for (const [index, config] of this.externalSettings.entries()) {
       const {deviceId} = config;
       let titleItem: HTMLElement;
       let photoItem: HTMLElement;
@@ -518,21 +528,24 @@
         videoItem.setAttribute('aria-describedby', `${deviceId}-videores-desc`);
         dom.getFrom(videoItem, '.description', HTMLElement).id =
             `${deviceId}-videores-desc`;
-        if (index < focusIdx) {
-          this.resMenu.insertBefore(extItem, fTitle);
+        if (index < focusedIdx) {
+          this.resolutionMenu.insertBefore(extItem, prevFocusedTitle);
         } else {
-          this.resMenu.appendChild(extItem);
+          this.resolutionMenu.appendChild(extItem);
         }
       } else {
-        assert(fTitle !== null);
-        titleItem = fTitle;
-        photoItem = assertInstanceof(fTitle.nextElementSibling, HTMLElement);
+        assert(prevFocusedTitle !== null);
+        titleItem = prevFocusedTitle;
+        photoItem =
+            assertInstanceof(prevFocusedTitle.nextElementSibling, HTMLElement);
         videoItem = assertInstanceof(photoItem.nextElementSibling, HTMLElement);
       }
       titleItem.dataset['deviceId'] = deviceId;
-      prepItem(photoItem, deviceId, config.photo, this.photoOptTextTempl);
-      prepItem(videoItem, deviceId, config.video, this.videoOptTextTempl);
-    });
+      prepareItem(
+          photoItem, deviceId, config.photo, this.photoOptionTextTemplate);
+      prepareItem(
+          videoItem, deviceId, config.video, this.videoOptionTextTemplate);
+    }
     // Force closing opened setting of unplugged device.
     if ((state.get(ViewName.PHOTO_RESOLUTION_SETTINGS) ||
          state.get(ViewName.VIDEO_RESOLUTION_SETTINGS)) &&
@@ -554,7 +567,7 @@
   private updateSelectedPhotoResolution(
       deviceId: string, resolution: Resolution) {
     const {photo} = assertExists(this.getDeviceSetting(deviceId));
-    photo.prefResol = resolution;
+    photo.prefResolution = resolution;
     let photoItem: HTMLElement;
     if (this.frontSetting && this.frontSetting.deviceId === deviceId) {
       photoItem = this.frontPhotoItem;
@@ -562,17 +575,17 @@
       photoItem = this.backPhotoItem;
     } else {
       photoItem = dom.getFrom(
-          this.resMenu, `.menu-item.photo-item[data-device-id="${deviceId}"]`,
-          HTMLElement);
+          this.resolutionMenu,
+          `.menu-item.photo-item[data-device-id="${deviceId}"]`, HTMLElement);
     }
     dom.getFrom(photoItem, '.description>span', HTMLSpanElement).textContent =
-        this.photoOptTextTempl(photo.prefResol, photo.resols);
+        this.photoOptionTextTemplate(photo.prefResolution, photo.resolutions);
 
     // Update setting option if it's opened.
     if (state.get(ViewName.PHOTO_RESOLUTION_SETTINGS) &&
         this.openedSettingDeviceId === deviceId) {
       const input = dom.getFrom(
-          this.photoResMenu,
+          this.photoResolutionMenu,
           'input' +
               `[data-width="${resolution.width}"]` +
               `[data-height="${resolution.height}"]`,
@@ -590,7 +603,7 @@
   private updateSelectedVideoResolution(
       deviceId: string, resolution: Resolution) {
     const {video} = assertExists(this.getDeviceSetting(deviceId));
-    video.prefResol = resolution;
+    video.prefResolution = resolution;
     let videoItem: HTMLElement;
     if (this.frontSetting && this.frontSetting.deviceId === deviceId) {
       videoItem = this.frontVideoItem;
@@ -598,17 +611,17 @@
       videoItem = this.backVideoItem;
     } else {
       videoItem = dom.getFrom(
-          this.resMenu, `.menu-item.video-item[data-device-id="${deviceId}"]`,
-          HTMLElement);
+          this.resolutionMenu,
+          `.menu-item.video-item[data-device-id="${deviceId}"]`, HTMLElement);
     }
     dom.getFrom(videoItem, '.description>span', HTMLSpanElement).textContent =
-        this.videoOptTextTempl(video.prefResol);
+        this.videoOptionTextTemplate(video.prefResolution);
 
     // Update setting option if it's opened.
     if (state.get(ViewName.VIDEO_RESOLUTION_SETTINGS) &&
         this.openedSettingDeviceId === deviceId) {
       const input = dom.getFrom(
-          this.videoResMenu,
+          this.videoResolutionMenu,
           'input' +
               `[data-width="${resolution.width}"]` +
               `[data-height="${resolution.height}"]`,
@@ -621,83 +634,85 @@
    * Opens photo resolution setting view.
    *
    * @param setting Setting of video device to be opened.
-   * @param resolItem Dom element from upper layer menu item showing title of
-   *     the selected resolution.
+   * @param resolutionItem Dom element from upper layer menu item showing title
+   *     of the selected resolution.
    */
-  private openPhotoResSettings(setting: DeviceSetting, resolItem: HTMLElement) {
+  private openPhotoResSettings(
+      setting: DeviceSetting, resolutionItem: HTMLElement) {
     const {deviceId, photo} = setting;
     this.openedSettingDeviceId = deviceId;
     this.updateMenu(
-        resolItem, this.photoResMenu, this.photoOptTextTempl,
+        resolutionItem, this.photoResolutionMenu, this.photoOptionTextTemplate,
         (r) => this.cameraManager.setPrefPhotoResolution(deviceId, r),
-        photo.resols, photo.prefResol);
-    this.openSubSettings(resolItem, ViewName.PHOTO_RESOLUTION_SETTINGS);
+        photo.resolutions, photo.prefResolution);
+    this.openSubSettings(resolutionItem, ViewName.PHOTO_RESOLUTION_SETTINGS);
   }
 
   /**
    * Opens video resolution setting view.
    *
    * @param setting Setting of video device to be opened.
-   * @param resolItem Dom element from upper layer menu item showing title of
-   *     the selected resolution.
+   * @param resolutionItem Dom element from upper layer menu item showing title
+   *     of the selected resolution.
    */
-  private openVideoResSettings(setting: DeviceSetting, resolItem: HTMLElement) {
+  private openVideoResSettings(
+      setting: DeviceSetting, resolutionItem: HTMLElement) {
     const {deviceId, video} = setting;
     this.openedSettingDeviceId = deviceId;
     this.updateMenu(
-        resolItem, this.videoResMenu, this.videoOptTextTempl,
+        resolutionItem, this.videoResolutionMenu, this.videoOptionTextTemplate,
         (r) => this.cameraManager.setPrefVideoResolution(deviceId, r),
-        video.resols, video.prefResol);
-    this.openSubSettings(resolItem, ViewName.VIDEO_RESOLUTION_SETTINGS);
+        video.resolutions, video.prefResolution);
+    this.openSubSettings(resolutionItem, ViewName.VIDEO_RESOLUTION_SETTINGS);
   }
 
   /**
    * Updates resolution menu with specified resolutions.
    *
-   * @param resolItem DOM element holding selected resolution.
+   * @param resolutionItem DOM element holding selected resolution.
    * @param menu Menu holding all resolution option elements.
-   * @param optTextTempl Template generating text content for each resolution
-   *     option from its width and height.
+   * @param optionTextTemplate Template generating text content for each
+   *     resolution option from its width and height.
    * @param onChange Called when selected option changed with resolution of
    *     newly selected option.
    * @param resolutions Resolutions of its width and height to be updated with.
-   * @param selectedR Selected resolution.
+   * @param selectedResolution Selected resolution.
    */
   private updateMenu(
-      resolItem: HTMLElement,
+      resolutionItem: HTMLElement,
       menu: HTMLElement,
-      optTextTempl:
+      optionTextTemplate:
           (resolution: Resolution, resolutions: ResolutionList) => string,
       onChange: (resolution: Resolution) => void,
       resolutions: ResolutionList,
-      selectedR: Resolution,
+      selectedResolution: Resolution,
   ) {
     const captionText =
-        dom.getFrom(resolItem, '.description>span', HTMLSpanElement);
+        dom.getFrom(resolutionItem, '.description>span', HTMLSpanElement);
     captionText.textContent = '';
     for (const element of dom.getAllFrom(
              menu, '.menu-item', HTMLLabelElement)) {
       assertExists(element.parentNode).removeChild(element);
     }
 
-    for (const r of resolutions) {
+    for (const resolution of resolutions) {
       const item = util.instantiateTemplate('#resolution-item-template');
       const input = dom.getFrom(item, 'input', HTMLInputElement);
       dom.getFrom(item, 'span', HTMLSpanElement).textContent =
-          optTextTempl(r, resolutions);
+          optionTextTemplate(resolution, resolutions);
       input.name = assertExists(menu.dataset[I18nString.NAME]);
-      input.dataset['width'] = r.width.toString();
-      input.dataset['height'] = r.height.toString();
-      if (r.equals(selectedR)) {
-        captionText.textContent = optTextTempl(r, resolutions);
+      input.dataset['width'] = resolution.width.toString();
+      input.dataset['height'] = resolution.height.toString();
+      if (resolution.equals(selectedResolution)) {
+        captionText.textContent = optionTextTemplate(resolution, resolutions);
         input.checked = true;
       }
       input.disabled = state.get(state.State.CAMERA_CONFIGURING) ||
           state.get(state.State.TAKING);
       input.addEventListener('change', () => {
         if (input.checked) {
-          captionText.textContent = optTextTempl(r, resolutions);
-          onChange(r);
+          captionText.textContent = optionTextTemplate(resolution, resolutions);
+          onChange(resolution);
         }
       });
       menu.appendChild(item);
diff --git a/ash/webui/camera_app_ui/resources/js/window_controller.ts b/ash/webui/camera_app_ui/resources/js/window_controller.ts
index 37a62eeb..eb22763 100644
--- a/ash/webui/camera_app_ui/resources/js/window_controller.ts
+++ b/ash/webui/camera_app_ui/resources/js/window_controller.ts
@@ -42,7 +42,9 @@
     windowMonitorCallbackRouter.onWindowStateChanged.addListener(
         (states: WindowStateType[]) => {
           this.windowStates = states;
-          this.listeners.forEach((listener) => listener(states));
+          for (const listener of this.listeners) {
+            listener(states);
+          }
         });
     const {states} = await this.windowStateController.addMonitor(
         windowMonitorCallbackRouter.$.bindNewPipeAndPassRemote());
diff --git a/ash/webui/eche_app_ui/BUILD.gn b/ash/webui/eche_app_ui/BUILD.gn
index 4b223329..d5f6319 100644
--- a/ash/webui/eche_app_ui/BUILD.gn
+++ b/ash/webui/eche_app_ui/BUILD.gn
@@ -28,6 +28,8 @@
     "eche_connector.h",
     "eche_connector_impl.cc",
     "eche_connector_impl.h",
+    "eche_display_stream_handler.cc",
+    "eche_display_stream_handler.h",
     "eche_feature_status_provider.cc",
     "eche_feature_status_provider.h",
     "eche_message_receiver.cc",
@@ -114,10 +116,12 @@
     "apps_access_setup_operation_unittest.cc",
     "eche_app_manager_unittest.cc",
     "eche_connector_impl_unittest.cc",
+    "eche_display_stream_handler_unittest.cc",
     "eche_feature_status_provider_unittest.cc",
     "eche_message_receiver_impl_unittest.cc",
     "eche_notification_click_handler_unittest.cc",
     "eche_notification_generator_unittest.cc",
+    "eche_presence_manager_unittest.cc",
     "eche_recent_app_click_handler_unittest.cc",
     "eche_signaler_unittest.cc",
     "eche_uid_provider_unittest.cc",
diff --git a/ash/webui/eche_app_ui/eche_app_manager.cc b/ash/webui/eche_app_ui/eche_app_manager.cc
index 397a5bbc..142b2dc 100644
--- a/ash/webui/eche_app_ui/eche_app_manager.cc
+++ b/ash/webui/eche_app_ui/eche_app_manager.cc
@@ -9,6 +9,7 @@
 #include "ash/services/secure_channel/public/cpp/client/connection_manager_impl.h"
 #include "ash/webui/eche_app_ui/apps_access_manager_impl.h"
 #include "ash/webui/eche_app_ui/eche_connector_impl.h"
+#include "ash/webui/eche_app_ui/eche_display_stream_handler.h"
 #include "ash/webui/eche_app_ui/eche_message_receiver_impl.h"
 #include "ash/webui/eche_app_ui/eche_notification_generator.h"
 #include "ash/webui/eche_app_ui/eche_presence_manager.h"
@@ -59,11 +60,13 @@
                                             launch_eche_app_function,
                                             close_eche_app_function,
                                             launch_notification_function)),
+      display_stream_handler_(std::make_unique<EcheDisplayStreamHandler>()),
       eche_notification_click_handler_(
           std::make_unique<EcheNotificationClickHandler>(
               phone_hub_manager,
               feature_status_provider_.get(),
-              launch_app_helper_.get())),
+              launch_app_helper_.get(),
+              display_stream_handler_.get())),
       eche_connector_(
           std::make_unique<EcheConnectorImpl>(feature_status_provider_.get(),
                                               connection_manager_.get())),
@@ -83,7 +86,8 @@
           std::make_unique<EcheRecentAppClickHandler>(
               phone_hub_manager,
               feature_status_provider_.get(),
-              launch_app_helper_.get())),
+              launch_app_helper_.get(),
+              display_stream_handler_.get())),
       notification_generator_(std::make_unique<EcheNotificationGenerator>(
           launch_app_helper_.get())),
       apps_access_manager_(std::make_unique<AppsAccessManagerImpl>(
@@ -119,6 +123,11 @@
   notification_generator_->Bind(std::move(receiver));
 }
 
+void EcheAppManager::BindDisplayStreamHandlerInterface(
+    mojo::PendingReceiver<mojom::DisplayStreamHandler> receiver) {
+  display_stream_handler_->Bind(std::move(receiver));
+}
+
 AppsAccessManager* EcheAppManager::GetAppsAccessManager() {
   return apps_access_manager_.get();
 }
@@ -136,6 +145,7 @@
   signaler_.reset();
   eche_connector_.reset();
   eche_notification_click_handler_.reset();
+  display_stream_handler_.reset();
   launch_app_helper_.reset();
   feature_status_provider_.reset();
   connection_manager_.reset();
diff --git a/ash/webui/eche_app_ui/eche_app_manager.h b/ash/webui/eche_app_ui/eche_app_manager.h
index f911b64..a839bf5 100644
--- a/ash/webui/eche_app_ui/eche_app_manager.h
+++ b/ash/webui/eche_app_ui/eche_app_manager.h
@@ -41,6 +41,7 @@
 class SystemInfo;
 class SystemInfoProvider;
 class AppsAccessManager;
+class EcheDisplayStreamHandler;
 
 // Implements the core logic of the EcheApp and exposes interfaces via its
 // public API. Implemented as a KeyedService since it depends on other
@@ -75,6 +76,9 @@
   void BindNotificationGeneratorInterface(
       mojo::PendingReceiver<mojom::NotificationGenerator> receiver);
 
+  void BindDisplayStreamHandlerInterface(
+      mojo::PendingReceiver<mojom::DisplayStreamHandler> receiver);
+
   AppsAccessManager* GetAppsAccessManager();
 
   // KeyedService:
@@ -84,6 +88,7 @@
   std::unique_ptr<secure_channel::ConnectionManager> connection_manager_;
   std::unique_ptr<EcheFeatureStatusProvider> feature_status_provider_;
   std::unique_ptr<LaunchAppHelper> launch_app_helper_;
+  std::unique_ptr<EcheDisplayStreamHandler> display_stream_handler_;
   std::unique_ptr<EcheNotificationClickHandler>
       eche_notification_click_handler_;
   std::unique_ptr<EcheConnector> eche_connector_;
diff --git a/ash/webui/eche_app_ui/eche_app_manager_unittest.cc b/ash/webui/eche_app_ui/eche_app_manager_unittest.cc
index 332ddd01..ddff94f 100644
--- a/ash/webui/eche_app_ui/eche_app_manager_unittest.cc
+++ b/ash/webui/eche_app_ui/eche_app_manager_unittest.cc
@@ -12,6 +12,7 @@
 #include "ash/services/secure_channel/public/cpp/client/fake_secure_channel_client.h"
 #include "ash/services/secure_channel/public/cpp/client/presence_monitor_client.h"
 #include "ash/services/secure_channel/public/cpp/client/presence_monitor_client_impl.h"
+#include "ash/webui/eche_app_ui/eche_display_stream_handler.h"
 #include "ash/webui/eche_app_ui/launch_app_helper.h"
 #include "ash/webui/eche_app_ui/system_info.h"
 #include "base/bind.h"
@@ -134,6 +135,10 @@
     return notification_generator_remote_;
   }
 
+  mojo::Remote<mojom::DisplayStreamHandler>& display_stream_handler_remote() {
+    return display_stream_handler_remote_;
+  }
+
   void Bind() {
     manager_->BindSignalingMessageExchangerInterface(
         signaling_message_exchanger_remote_.BindNewPipeAndPassReceiver());
@@ -143,6 +148,8 @@
         uid_generator_remote_.BindNewPipeAndPassReceiver());
     manager_->BindNotificationGeneratorInterface(
         notification_generator_remote_.BindNewPipeAndPassReceiver());
+    manager_->BindDisplayStreamHandlerInterface(
+        display_stream_handler_remote_.BindNewPipeAndPassReceiver());
   }
 
  private:
@@ -163,6 +170,7 @@
   mojo::Remote<mojom::SystemInfoProvider> system_info_provider_remote_;
   mojo::Remote<mojom::UidGenerator> uid_generator_remote_;
   mojo::Remote<mojom::NotificationGenerator> notification_generator_remote_;
+  mojo::Remote<mojom::DisplayStreamHandler> display_stream_handler_remote_;
 };
 
 TEST_F(EcheAppManagerTest, BindCheck) {
@@ -170,6 +178,7 @@
   EXPECT_FALSE(system_info_provider_remote());
   EXPECT_FALSE(uid_generator_remote());
   EXPECT_FALSE(notification_generator_remote());
+  EXPECT_FALSE(display_stream_handler_remote());
 
   Bind();
 
@@ -177,6 +186,7 @@
   EXPECT_TRUE(system_info_provider_remote());
   EXPECT_TRUE(uid_generator_remote());
   EXPECT_TRUE(notification_generator_remote());
+  EXPECT_TRUE(display_stream_handler_remote());
 }
 
 }  // namespace eche_app
diff --git a/ash/webui/eche_app_ui/eche_app_ui.cc b/ash/webui/eche_app_ui/eche_app_ui.cc
index 32d39a4..fab04df 100644
--- a/ash/webui/eche_app_ui/eche_app_ui.cc
+++ b/ash/webui/eche_app_ui/eche_app_ui.cc
@@ -24,12 +24,14 @@
                      BindSignalingMessageExchangerCallback exchanger_callback,
                      BindSystemInfoProviderCallback system_info_callback,
                      BindUidGeneratorCallback generator_callback,
-                     BindNotificationGeneratorCallback notification_callback)
+                     BindNotificationGeneratorCallback notification_callback,
+                     BindDisplayStreamHandlerCallback stream_handler_callback)
     : ui::MojoWebUIController(web_ui),
       bind_exchanger_callback_(std::move(exchanger_callback)),
       bind_system_info_callback_(std::move(system_info_callback)),
       bind_generator_callback_(std::move(generator_callback)),
-      bind_notification_callback_(std::move(notification_callback)) {
+      bind_notification_callback_(std::move(notification_callback)),
+      bind_stream_handler_callback_(std::move(stream_handler_callback)) {
   auto* browser_context = web_ui->GetWebContents()->GetBrowserContext();
   content::WebUIDataSource* html_source =
       content::WebUIDataSource::CreateAndAdd(browser_context,
@@ -111,6 +113,11 @@
   bind_notification_callback_.Run(std::move(receiver));
 }
 
+void EcheAppUI::BindInterface(
+    mojo::PendingReceiver<mojom::DisplayStreamHandler> receiver) {
+  bind_stream_handler_callback_.Run(std::move(receiver));
+}
+
 WEB_UI_CONTROLLER_TYPE_IMPL(EcheAppUI)
 
 }  // namespace eche_app
diff --git a/ash/webui/eche_app_ui/eche_app_ui.h b/ash/webui/eche_app_ui/eche_app_ui.h
index f06b3117..30387785 100644
--- a/ash/webui/eche_app_ui/eche_app_ui.h
+++ b/ash/webui/eche_app_ui/eche_app_ui.h
@@ -22,12 +22,15 @@
       base::RepeatingCallback<void(mojo::PendingReceiver<mojom::UidGenerator>)>;
   using BindNotificationGeneratorCallback = base::RepeatingCallback<void(
       mojo::PendingReceiver<mojom::NotificationGenerator>)>;
+  using BindDisplayStreamHandlerCallback = base::RepeatingCallback<void(
+      mojo::PendingReceiver<mojom::DisplayStreamHandler>)>;
 
   EcheAppUI(content::WebUI* web_ui,
             BindSignalingMessageExchangerCallback exchanger_callback,
             BindSystemInfoProviderCallback system_info_callback,
             BindUidGeneratorCallback generator_callback,
-            BindNotificationGeneratorCallback notification_callback);
+            BindNotificationGeneratorCallback notification_callback,
+            BindDisplayStreamHandlerCallback stream_handler_callback);
   EcheAppUI(const EcheAppUI&) = delete;
   EcheAppUI& operator=(const EcheAppUI&) = delete;
   ~EcheAppUI() override;
@@ -42,11 +45,15 @@
   void BindInterface(
       mojo::PendingReceiver<mojom::NotificationGenerator> receiver);
 
+  void BindInterface(
+      mojo::PendingReceiver<mojom::DisplayStreamHandler> receiver);
+
  private:
   const BindSignalingMessageExchangerCallback bind_exchanger_callback_;
   const BindSystemInfoProviderCallback bind_system_info_callback_;
   const BindUidGeneratorCallback bind_generator_callback_;
   const BindNotificationGeneratorCallback bind_notification_callback_;
+  const BindDisplayStreamHandlerCallback bind_stream_handler_callback_;
 
   WEB_UI_CONTROLLER_TYPE_DECL();
 };
diff --git a/ash/webui/eche_app_ui/eche_display_stream_handler.cc b/ash/webui/eche_app_ui/eche_display_stream_handler.cc
new file mode 100644
index 0000000..d1b9f86a9
--- /dev/null
+++ b/ash/webui/eche_app_ui/eche_display_stream_handler.cc
@@ -0,0 +1,42 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/webui/eche_app_ui/eche_display_stream_handler.h"
+
+#include "ash/webui/eche_app_ui/launch_app_helper.h"
+#include "chromeos/components/multidevice/logging/logging.h"
+
+namespace ash {
+namespace eche_app {
+
+EcheDisplayStreamHandler::EcheDisplayStreamHandler() = default;
+
+EcheDisplayStreamHandler::~EcheDisplayStreamHandler() = default;
+
+void EcheDisplayStreamHandler::StartStreaming() {
+  PA_LOG(INFO) << "echeapi EcheDisplayStreamHandler StartStreaming";
+  NotifyStartStreaming();
+}
+
+void EcheDisplayStreamHandler::Bind(
+    mojo::PendingReceiver<mojom::DisplayStreamHandler> receiver) {
+  display_stream_receiver_.reset();
+  display_stream_receiver_.Bind(std::move(receiver));
+}
+
+void EcheDisplayStreamHandler::AddObserver(Observer* observer) {
+  observer_list_.AddObserver(observer);
+}
+
+void EcheDisplayStreamHandler::RemoveObserver(Observer* observer) {
+  observer_list_.RemoveObserver(observer);
+}
+
+void EcheDisplayStreamHandler::NotifyStartStreaming() {
+  for (auto& observer : observer_list_)
+    observer.OnStartStreaming();
+}
+
+}  // namespace eche_app
+}  // namespace ash
diff --git a/ash/webui/eche_app_ui/eche_display_stream_handler.h b/ash/webui/eche_app_ui/eche_display_stream_handler.h
new file mode 100644
index 0000000..339b1b2
--- /dev/null
+++ b/ash/webui/eche_app_ui/eche_display_stream_handler.h
@@ -0,0 +1,60 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ASH_WEBUI_ECHE_APP_UI_ECHE_DISPLAY_STREAM_HANDLER_H_
+#define ASH_WEBUI_ECHE_APP_UI_ECHE_DISPLAY_STREAM_HANDLER_H_
+
+#include "ash/webui/eche_app_ui/mojom/eche_app.mojom.h"
+#include "base/observer_list.h"
+#include "base/observer_list_types.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+
+namespace ash {
+namespace eche_app {
+
+// Implements the EcheDisplayStreamHandler interface to allow the WebUI to sync
+// the status of the display stream for Eche, e.g. When the display stream is
+// started  in the Eche Web, we can register `Observer` and get this status via
+// `OnStartStreaming` event.
+// TODO(paulzchen): Consider using `DisplayStreamEventHandler` to replace
+// `DisplayStreamHandler`.
+class EcheDisplayStreamHandler : public mojom::DisplayStreamHandler {
+ public:
+  class Observer : public base::CheckedObserver {
+   public:
+    ~Observer() override = default;
+
+    //  Called when the streaming is ready. About another status:
+    //  OnStopStreaming, we prefer to listen to the stop signal when the bubble
+    //  is really closed.
+    // TODO(paulzchen): Using generic method `OnStreamStatusChanged`.
+    virtual void OnStartStreaming() = 0;
+  };
+
+  EcheDisplayStreamHandler();
+  ~EcheDisplayStreamHandler() override;
+
+  EcheDisplayStreamHandler(const EcheDisplayStreamHandler&) = delete;
+  EcheDisplayStreamHandler& operator=(const EcheDisplayStreamHandler&) = delete;
+
+  // mojom::DisplayStreamHandler:
+  void StartStreaming() override;
+
+  void AddObserver(Observer* observer);
+  void RemoveObserver(Observer* observer);
+
+  void Bind(mojo::PendingReceiver<mojom::DisplayStreamHandler> receiver);
+
+ protected:
+  void NotifyStartStreaming();
+
+ private:
+  mojo::Receiver<mojom::DisplayStreamHandler> display_stream_receiver_{this};
+  base::ObserverList<Observer> observer_list_;
+};
+
+}  // namespace eche_app
+}  // namespace ash
+
+#endif  // ASH_WEBUI_ECHE_APP_UI_ECHE_DISPLAY_STREAM_HANDLER_H_
diff --git a/ash/webui/eche_app_ui/eche_display_stream_handler_unittest.cc b/ash/webui/eche_app_ui/eche_display_stream_handler_unittest.cc
new file mode 100644
index 0000000..fec38a0
--- /dev/null
+++ b/ash/webui/eche_app_ui/eche_display_stream_handler_unittest.cc
@@ -0,0 +1,63 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/webui/eche_app_ui/eche_display_stream_handler.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace ash {
+namespace eche_app {
+namespace {
+
+class FakeObserver : public EcheDisplayStreamHandler::Observer {
+ public:
+  FakeObserver() = default;
+  ~FakeObserver() override = default;
+
+  size_t num_calls() const { return num_calls_; }
+
+  // EcheDisplayStreamHandler::Observer:
+  void OnStartStreaming() override { ++num_calls_; }
+
+ private:
+  size_t num_calls_ = 0;
+};
+
+}  // namespace
+
+class EcheDisplayStreamHandlerTest : public testing::Test {
+ protected:
+  EcheDisplayStreamHandlerTest() = default;
+  EcheDisplayStreamHandlerTest(const EcheDisplayStreamHandlerTest&) = delete;
+  EcheDisplayStreamHandlerTest& operator=(const EcheDisplayStreamHandlerTest&) =
+      delete;
+  ~EcheDisplayStreamHandlerTest() override = default;
+
+  // testing::Test:
+  void SetUp() override {
+    handler_ = std::make_unique<EcheDisplayStreamHandler>();
+    handler_->AddObserver(&fake_observer_);
+  }
+
+  void TearDown() override {
+    handler_->RemoveObserver(&fake_observer_);
+    handler_.reset();
+  }
+
+  void StartStreaming() { handler_->StartStreaming(); }
+
+  size_t GetNumObserverCalls() const { return fake_observer_.num_calls(); }
+
+ private:
+  FakeObserver fake_observer_;
+  std::unique_ptr<EcheDisplayStreamHandler> handler_;
+};
+
+TEST_F(EcheDisplayStreamHandlerTest, StartStreaming) {
+  StartStreaming();
+  EXPECT_EQ(1u, GetNumObserverCalls());
+}
+
+}  // namespace eche_app
+}  // namespace ash
\ No newline at end of file
diff --git a/ash/webui/eche_app_ui/eche_notification_click_handler.cc b/ash/webui/eche_app_ui/eche_notification_click_handler.cc
index 3a81369a..2f8a4e9d 100644
--- a/ash/webui/eche_app_ui/eche_notification_click_handler.cc
+++ b/ash/webui/eche_app_ui/eche_notification_click_handler.cc
@@ -6,6 +6,9 @@
 
 #include "ash/components/phonehub/phone_hub_manager.h"
 #include "ash/constants/ash_features.h"
+#include "ash/root_window_controller.h"
+#include "ash/shell.h"
+#include "ash/system/eche/eche_tray.h"
 #include "ash/webui/eche_app_ui/launch_app_helper.h"
 #include "chromeos/components/multidevice/logging/logging.h"
 
@@ -15,9 +18,11 @@
 EcheNotificationClickHandler::EcheNotificationClickHandler(
     phonehub::PhoneHubManager* phone_hub_manager,
     FeatureStatusProvider* feature_status_provider,
-    LaunchAppHelper* launch_app_helper)
+    LaunchAppHelper* launch_app_helper,
+    EcheDisplayStreamHandler* display_stream_handler)
     : feature_status_provider_(feature_status_provider),
-      launch_app_helper_(launch_app_helper) {
+      launch_app_helper_(launch_app_helper),
+      display_stream_handler_(display_stream_handler) {
   handler_ = phone_hub_manager->GetNotificationInteractionHandler();
   feature_status_provider_->AddObserver(this);
   if (handler_ && IsClickable(feature_status_provider_->GetStatus())) {
@@ -27,12 +32,18 @@
     PA_LOG(INFO)
         << "No Phone Hub interaction handler to set Eche click handler";
   }
+
+  if (features::IsEcheSWAInBackgroundEnabled())
+    display_stream_handler_->AddObserver(this);
 }
 
 EcheNotificationClickHandler::~EcheNotificationClickHandler() {
   feature_status_provider_->RemoveObserver(this);
   if (is_click_handler_set_ && handler_)
     handler_->RemoveNotificationClickHandler(this);
+
+  if (features::IsEcheSWAInBackgroundEnabled())
+    display_stream_handler_->RemoveObserver(this);
 }
 
 void EcheNotificationClickHandler::HandleNotificationClick(
@@ -46,6 +57,7 @@
       launch_app_helper_->LaunchEcheApp(
           notification_id, app_metadata.package_name,
           app_metadata.visible_app_name, app_metadata.user_id);
+      is_waiting_for_streaming_to_show_ = true;
       break;
     case LaunchAppHelper::AppLaunchProhibitedReason::kDisabledByScreenLock:
       launch_app_helper_->ShowNotification(
@@ -80,15 +92,30 @@
   } else if (is_click_handler_set_ && !clickable) {
     handler_->RemoveNotificationClickHandler(this);
     is_click_handler_set_ = false;
+    is_waiting_for_streaming_to_show_ = false;
   }
 
   if (NeedClose(feature_status_provider_->GetStatus()) &&
       !base::FeatureList::IsEnabled(features::kEcheSWADebugMode)) {
     PA_LOG(INFO) << "Close Eche app window";
+    is_waiting_for_streaming_to_show_ = false;
     launch_app_helper_->CloseEcheApp();
   }
 }
 
+void EcheNotificationClickHandler::OnStartStreaming() {
+  if (features::IsEcheCustomWidgetEnabled()) {
+    // TODO(paulzchen): Move the eche tray control to factory.
+    auto* eche_tray = Shell::GetPrimaryRootWindowController()
+                          ->GetStatusAreaWidget()
+                          ->eche_tray();
+    if (eche_tray && is_waiting_for_streaming_to_show_) {
+      eche_tray->ShowBubble();
+      is_waiting_for_streaming_to_show_ = false;
+    }
+  }
+}
+
 bool EcheNotificationClickHandler::IsClickable(FeatureStatus status) {
   return status == FeatureStatus::kDisconnected ||
          status == FeatureStatus::kConnecting ||
diff --git a/ash/webui/eche_app_ui/eche_notification_click_handler.h b/ash/webui/eche_app_ui/eche_notification_click_handler.h
index 5fe0c234..c46cc41 100644
--- a/ash/webui/eche_app_ui/eche_notification_click_handler.h
+++ b/ash/webui/eche_app_ui/eche_notification_click_handler.h
@@ -10,6 +10,7 @@
 #include "ash/components/phonehub/notification_interaction_handler.h"
 // TODO(https://crbug.com/1164001): move to forward declaration.
 #include "ash/components/phonehub/phone_hub_manager.h"
+#include "ash/webui/eche_app_ui/eche_display_stream_handler.h"
 #include "ash/webui/eche_app_ui/feature_status_provider.h"
 #include "base/callback.h"
 
@@ -20,11 +21,14 @@
 
 // Handles notification clicks originating from Phone Hub notifications.
 class EcheNotificationClickHandler : public phonehub::NotificationClickHandler,
-                                     public FeatureStatusProvider::Observer {
+                                     public FeatureStatusProvider::Observer,
+                                     public EcheDisplayStreamHandler::Observer {
  public:
-  EcheNotificationClickHandler(phonehub::PhoneHubManager* phone_hub_manager,
-                               FeatureStatusProvider* feature_status_provider,
-                               LaunchAppHelper* launch_app_helper);
+  EcheNotificationClickHandler(
+      phonehub::PhoneHubManager* phone_hub_manager,
+      FeatureStatusProvider* feature_status_provider,
+      LaunchAppHelper* launch_app_helper,
+      EcheDisplayStreamHandler* display_stream_handler);
   ~EcheNotificationClickHandler() override;
 
   EcheNotificationClickHandler(const EcheNotificationClickHandler&) = delete;
@@ -39,6 +43,14 @@
   // FeatureStatusProvider::Observer:
   void OnFeatureStatusChanged() override;
 
+  // EcheDisplayStreamHandler::Observer:
+  void OnStartStreaming() override;
+
+  // Test helpers, we need this to confirm the streaming will work as expected.
+  bool waiting_for_streaming_to_show() {
+    return is_waiting_for_streaming_to_show_;
+  }
+
  private:
   bool IsClickable(FeatureStatus status);
 
@@ -47,7 +59,9 @@
   phonehub::NotificationInteractionHandler* handler_;
   FeatureStatusProvider* feature_status_provider_;
   LaunchAppHelper* launch_app_helper_;
+  EcheDisplayStreamHandler* display_stream_handler_;
   bool is_click_handler_set_ = false;
+  bool is_waiting_for_streaming_to_show_ = false;
 };
 
 }  // namespace eche_app
diff --git a/ash/webui/eche_app_ui/eche_notification_click_handler_unittest.cc b/ash/webui/eche_app_ui/eche_notification_click_handler_unittest.cc
index fd49284..33be6b5 100644
--- a/ash/webui/eche_app_ui/eche_notification_click_handler_unittest.cc
+++ b/ash/webui/eche_app_ui/eche_notification_click_handler_unittest.cc
@@ -8,6 +8,12 @@
 
 #include "ash/components/phonehub/fake_phone_hub_manager.h"
 #include "ash/constants/ash_features.h"
+#include "ash/system/eche/eche_tray.h"
+#include "ash/system/status_area_widget_test_helper.h"
+#include "ash/system/tray/tray_bubble_wrapper.h"
+#include "ash/test/ash_test_base.h"
+#include "ash/test/ash_test_suite.h"
+#include "ash/test/test_ash_web_view_factory.h"
 #include "ash/webui/eche_app_ui/fake_feature_status_provider.h"
 #include "ash/webui/eche_app_ui/fake_launch_app_helper.h"
 #include "ash/webui/eche_app_ui/launch_app_helper.h"
@@ -15,11 +21,12 @@
 #include "base/test/scoped_feature_list.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
+#include "ui/base/resource/resource_bundle.h"
 
 namespace ash {
 namespace eche_app {
 
-class EcheNotificationClickHandlerTest : public testing::Test {
+class EcheNotificationClickHandlerTest : public AshTestBase {
  protected:
   EcheNotificationClickHandlerTest() = default;
   EcheNotificationClickHandlerTest(const EcheNotificationClickHandlerTest&) =
@@ -28,12 +35,23 @@
       const EcheNotificationClickHandlerTest&) = delete;
   ~EcheNotificationClickHandlerTest() override = default;
 
-  // testing::Test:
+  // AshTestBase::Test:
   void SetUp() override {
+    scoped_feature_list_.InitWithFeatures(
+        /*enabled_features=*/{features::kEcheSWA, features::kEcheCustomWidget},
+        /*disabled_features=*/{});
+
+    DCHECK(test_web_view_factory_.get());
+
+    ui::ResourceBundle::CleanupSharedInstance();
+    AshTestSuite::LoadTestResources();
+    AshTestBase::SetUp();
+    eche_tray_ =
+        ash::StatusAreaWidgetTestHelper::GetStatusAreaWidget()->eche_tray();
+
     fake_phone_hub_manager_.fake_feature_status_provider()->SetStatus(
         phonehub::FeatureStatus::kEnabledAndConnected);
     fake_feature_status_provider_.SetStatus(FeatureStatus::kIneligible);
-    scoped_feature_list_.InitWithFeatures({features::kEcheSWA}, {});
     launch_app_helper_ = std::make_unique<FakeLaunchAppHelper>(
         &fake_phone_hub_manager_,
         base::BindRepeating(
@@ -45,13 +63,16 @@
         base::BindRepeating(
             &EcheNotificationClickHandlerTest::FakeLaunchNotificationFunction,
             base::Unretained(this)));
+    display_stream_handler_ = std::make_unique<EcheDisplayStreamHandler>();
     handler_ = std::make_unique<EcheNotificationClickHandler>(
         &fake_phone_hub_manager_, &fake_feature_status_provider_,
-        launch_app_helper_.get());
+        launch_app_helper_.get(), display_stream_handler_.get());
   }
 
   void TearDown() override {
+    AshTestBase::TearDown();
     launch_app_helper_.reset();
+    display_stream_handler_.reset();
     handler_.reset();
   }
 
@@ -91,12 +112,20 @@
         ->notification_click_handler_count();
   }
 
+  void StartStreaming() { handler_->OnStartStreaming(); }
+
   bool close_eche_is_called() { return close_eche_is_called_; }
 
   size_t num_notifications_shown() { return num_notifications_shown_; }
 
   size_t num_app_launch() { return num_app_launch_; }
 
+  bool waiting_for_streaming_to_show() {
+    return handler_->waiting_for_streaming_to_show();
+  }
+
+  EcheTray* eche_tray() { return eche_tray_; }
+
   void reset() {
     close_eche_is_called_ = false;
     num_notifications_shown_ = 0;
@@ -110,9 +139,15 @@
   base::test::ScopedFeatureList scoped_feature_list_;
   FakeFeatureStatusProvider fake_feature_status_provider_;
   std::unique_ptr<FakeLaunchAppHelper> launch_app_helper_;
+  std::unique_ptr<EcheDisplayStreamHandler> display_stream_handler_;
   bool close_eche_is_called_;
   size_t num_notifications_shown_ = 0;
   size_t num_app_launch_ = 0;
+  EcheTray* eche_tray_ = nullptr;  // Not owned
+
+  // Calling the factory constructor is enough to set it up.
+  std::unique_ptr<TestAshWebViewFactory> test_web_view_factory_ =
+      std::make_unique<TestAshWebViewFactory>();
 };
 
 TEST_F(EcheNotificationClickHandlerTest, StatusChangeTransitions) {
@@ -199,5 +234,28 @@
   EXPECT_EQ(num_notifications_shown(), 1u);
 }
 
+TEST_F(EcheNotificationClickHandlerTest, StartStreaming) {
+  EXPECT_FALSE(waiting_for_streaming_to_show());
+
+  const int64_t notification_id = 0;
+  const char16_t app_name[] = u"Test App";
+  const char package_name[] = "com.google.testapp";
+  const int64_t user_id = 0;
+  phonehub::Notification::AppMetadata app_meta_data =
+      phonehub::Notification::AppMetadata(app_name, package_name,
+                                          /*icon=*/gfx::Image(),
+                                          /*icon_color=*/absl::nullopt,
+                                          /*icon_is_monochrome=*/true, user_id);
+  HandleNotificationClick(notification_id, app_meta_data);
+
+  EXPECT_TRUE(waiting_for_streaming_to_show());
+
+  StartStreaming();
+
+  EXPECT_TRUE(
+      eche_tray()->get_bubble_wrapper_for_test()->bubble_view()->GetVisible());
+  EXPECT_FALSE(waiting_for_streaming_to_show());
+}
+
 }  // namespace eche_app
 }  // namespace ash
diff --git a/ash/webui/eche_app_ui/eche_presence_manager.cc b/ash/webui/eche_app_ui/eche_presence_manager.cc
index f305aa47..14882050 100644
--- a/ash/webui/eche_app_ui/eche_presence_manager.cc
+++ b/ash/webui/eche_app_ui/eche_presence_manager.cc
@@ -25,7 +25,7 @@
 }  // namespace
 
 EchePresenceManager::EchePresenceManager(
-    EcheFeatureStatusProvider* eche_feature_status_provider,
+    FeatureStatusProvider* eche_feature_status_provider,
     device_sync::DeviceSyncClient* device_sync_client,
     multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client,
     std::unique_ptr<secure_channel::PresenceMonitorClient>
diff --git a/ash/webui/eche_app_ui/eche_presence_manager.h b/ash/webui/eche_app_ui/eche_presence_manager.h
index 99e447f6..93dfb9d 100644
--- a/ash/webui/eche_app_ui/eche_presence_manager.h
+++ b/ash/webui/eche_app_ui/eche_presence_manager.h
@@ -29,7 +29,7 @@
                             public EcheMessageReceiver::Observer {
  public:
   EchePresenceManager(
-      EcheFeatureStatusProvider* eche_feature_status_provider,
+      FeatureStatusProvider* eche_feature_status_provider,
       device_sync::DeviceSyncClient* device_sync_client,
       multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client,
       std::unique_ptr<secure_channel::PresenceMonitorClient>
@@ -59,7 +59,7 @@
   void StopMonitoring();
   void OnTimerExpired();
 
-  EcheFeatureStatusProvider* eche_feature_status_provider_;
+  FeatureStatusProvider* eche_feature_status_provider_;
   device_sync::DeviceSyncClient* device_sync_client_;
   multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client_;
   std::unique_ptr<secure_channel::PresenceMonitorClient>
diff --git a/ash/webui/eche_app_ui/eche_presence_manager_unittest.cc b/ash/webui/eche_app_ui/eche_presence_manager_unittest.cc
new file mode 100644
index 0000000..0f80bd0
--- /dev/null
+++ b/ash/webui/eche_app_ui/eche_presence_manager_unittest.cc
@@ -0,0 +1,196 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/webui/eche_app_ui/eche_presence_manager.h"
+
+#include "ash/constants/ash_features.h"
+#include "ash/services/multidevice_setup/public/cpp/fake_multidevice_setup_client.h"
+#include "ash/services/secure_channel/public/cpp/client/presence_monitor_client_impl.h"
+#include "ash/webui/eche_app_ui/fake_eche_connector.h"
+#include "ash/webui/eche_app_ui/fake_eche_message_receiver.h"
+#include "ash/webui/eche_app_ui/fake_feature_status_provider.h"
+#include "ash/webui/eche_app_ui/proto/exo_messages.pb.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
+#include "chromeos/components/multidevice/remote_device_test_util.h"
+#include "chromeos/services/device_sync/public/cpp/fake_device_sync_client.h"
+#include "components/prefs/testing_pref_service.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace ash {
+namespace eche_app {
+
+static size_t num_start_monitor_calls_ = 0;
+static size_t num_stop_monitor_calls_ = 0;
+
+namespace {
+
+class FakePresenceMonitorClient : public secure_channel::PresenceMonitorClient {
+ public:
+  FakePresenceMonitorClient() = default;
+  ~FakePresenceMonitorClient() override = default;
+
+ private:
+  // secure_channel::PresenceMonitorClient:
+  void SetPresenceMonitorCallbacks(
+      chromeos::secure_channel::PresenceMonitor::ReadyCallback ready_callback,
+      chromeos::secure_channel::PresenceMonitor::DeviceSeenCallback
+          device_seen_callback) override {}
+  void StartMonitoring(
+      const multidevice::RemoteDeviceRef& remote_device_ref,
+      const multidevice::RemoteDeviceRef& local_device_ref) override {
+    num_start_monitor_calls_++;
+  }
+  void StopMonitoring() override { num_stop_monitor_calls_++; }
+};
+}  // namespace
+
+class EchePresenceManagerTest : public testing::Test {
+ protected:
+  EchePresenceManagerTest()
+      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
+        test_remote_device_(
+            chromeos::multidevice::CreateRemoteDeviceRefForTest()),
+        test_devices_(
+            chromeos::multidevice::CreateRemoteDeviceRefListForTest(1)) {}
+  EchePresenceManagerTest(const EchePresenceManagerTest&) = delete;
+  EchePresenceManagerTest& operator=(const EchePresenceManagerTest&) = delete;
+  ~EchePresenceManagerTest() override = default;
+
+  void SetUp() override {
+    scoped_feature_list_.InitWithFeatures(
+        /*enabled_features=*/{chromeos::features::kEcheSWA},
+        /*disabled_features=*/{});
+    fake_multidevice_setup_client_.SetHostStatusWithDevice(std::make_pair(
+        chromeos::multidevice_setup::mojom::HostStatus::kHostVerified,
+        test_remote_device_));
+    fake_device_sync_client_.set_local_device_metadata(test_devices_[0]);
+    fake_device_sync_client_.NotifyReady();
+    fake_eche_connector_ = std::make_unique<FakeEcheConnector>();
+    fake_eche_message_receiver_ = std::make_unique<FakeEcheMessageReceiver>();
+    fake_feature_status_provider_ = std::make_unique<FakeFeatureStatusProvider>(
+        FeatureStatus::kDependentFeature);
+    fake_presence_monitor_client_ =
+        std::make_unique<FakePresenceMonitorClient>();
+    eche_presence_manager_ = std::make_unique<EchePresenceManager>(
+        fake_feature_status_provider_.get(), &fake_device_sync_client_,
+        &fake_multidevice_setup_client_,
+        std::move(fake_presence_monitor_client_), fake_eche_connector_.get(),
+        fake_eche_message_receiver_.get());
+  }
+
+  void TearDown() override {
+    eche_presence_manager_.reset();
+    fake_eche_connector_.reset();
+    fake_eche_message_receiver_.reset();
+    fake_feature_status_provider_.reset();
+    fake_presence_monitor_client_.reset();
+  }
+
+  void SetFeatureStatus(FeatureStatus status) {
+    fake_feature_status_provider_->SetStatus(status);
+  }
+
+  FeatureStatus GetFeatureStatus() {
+    return fake_feature_status_provider_->GetStatus();
+  }
+
+  void SetStreamStatus(proto::StatusChangeType type) {
+    fake_eche_message_receiver_->FakeStatusChange(type);
+  }
+
+  void Reset() {
+    num_start_monitor_calls_ = 0;
+    num_stop_monitor_calls_ = 0;
+  }
+
+  base::test::TaskEnvironment task_environment_;
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+  std::unique_ptr<FakeEcheConnector> fake_eche_connector_;
+  std::unique_ptr<FakeEcheMessageReceiver> fake_eche_message_receiver_;
+  std::unique_ptr<FakeFeatureStatusProvider> fake_feature_status_provider_;
+  const chromeos::multidevice::RemoteDeviceRef test_remote_device_;
+  multidevice_setup::FakeMultiDeviceSetupClient fake_multidevice_setup_client_;
+  device_sync::FakeDeviceSyncClient fake_device_sync_client_;
+  const multidevice::RemoteDeviceRefList test_devices_;
+  std::unique_ptr<FakePresenceMonitorClient> fake_presence_monitor_client_;
+  std::unique_ptr<EchePresenceManager> eche_presence_manager_;
+};
+
+TEST_F(EchePresenceManagerTest, StopMonitoring) {
+  // Test feature status change to kNotEnabledByPhone
+  Reset();
+  SetFeatureStatus(FeatureStatus::kConnected);
+  SetStreamStatus(proto::StatusChangeType::TYPE_STREAM_START);
+  SetFeatureStatus(FeatureStatus::kNotEnabledByPhone);
+  EXPECT_EQ(1u, num_stop_monitor_calls_);
+
+  // Test feature status change to kIneligible
+  Reset();
+  SetFeatureStatus(FeatureStatus::kConnected);
+  SetStreamStatus(proto::StatusChangeType::TYPE_STREAM_START);
+  SetFeatureStatus(FeatureStatus::kIneligible);
+  EXPECT_EQ(1u, num_stop_monitor_calls_);
+
+  // Test feature status change to kDisabled
+  Reset();
+  SetFeatureStatus(FeatureStatus::kConnected);
+  SetStreamStatus(proto::StatusChangeType::TYPE_STREAM_START);
+  SetFeatureStatus(FeatureStatus::kDisabled);
+  EXPECT_EQ(1u, num_stop_monitor_calls_);
+
+  // Test feature status change to kDependentFeature
+  Reset();
+  SetFeatureStatus(FeatureStatus::kConnected);
+  SetStreamStatus(proto::StatusChangeType::TYPE_STREAM_START);
+  SetFeatureStatus(FeatureStatus::kDependentFeature);
+  EXPECT_EQ(1u, num_stop_monitor_calls_);
+
+  // Test feature status change to kDependentFeaturePending
+  Reset();
+  SetFeatureStatus(FeatureStatus::kConnected);
+  SetStreamStatus(proto::StatusChangeType::TYPE_STREAM_START);
+  SetFeatureStatus(FeatureStatus::kDependentFeaturePending);
+  EXPECT_EQ(1u, num_stop_monitor_calls_);
+
+  // Test feature status change to kDisconnected.
+  Reset();
+  SetFeatureStatus(FeatureStatus::kConnected);
+  SetStreamStatus(proto::StatusChangeType::TYPE_STREAM_START);
+  SetFeatureStatus(FeatureStatus::kDisconnected);
+  EXPECT_EQ(1u, num_stop_monitor_calls_);
+
+  // Test feature status change to kConnecting.
+  Reset();
+  SetFeatureStatus(FeatureStatus::kConnected);
+  SetStreamStatus(proto::StatusChangeType::TYPE_STREAM_START);
+  SetFeatureStatus(FeatureStatus::kConnecting);
+  EXPECT_EQ(1u, num_stop_monitor_calls_);
+
+  // Test stream status change to stop.
+  Reset();
+  SetFeatureStatus(FeatureStatus::kConnected);
+  SetStreamStatus(proto::StatusChangeType::TYPE_STREAM_START);
+  SetStreamStatus(proto::StatusChangeType::TYPE_STREAM_STOP);
+  EXPECT_EQ(1u, num_stop_monitor_calls_);
+
+  // Test 5 minutes not see device.
+  Reset();
+  SetFeatureStatus(FeatureStatus::kConnected);
+  SetStreamStatus(proto::StatusChangeType::TYPE_STREAM_START);
+  task_environment_.FastForwardBy(base::Minutes(5));
+  EXPECT_EQ(1u, num_stop_monitor_calls_);
+}
+
+TEST_F(EchePresenceManagerTest, StartMonitoring) {
+  Reset();
+  SetFeatureStatus(FeatureStatus::kConnected);
+  SetStreamStatus(proto::StatusChangeType::TYPE_STREAM_START);
+  EXPECT_EQ(1u, num_start_monitor_calls_);
+}
+
+}  // namespace eche_app
+}  // namespace ash
diff --git a/ash/webui/eche_app_ui/eche_recent_app_click_handler.cc b/ash/webui/eche_app_ui/eche_recent_app_click_handler.cc
index b23adca..42cca0e 100644
--- a/ash/webui/eche_app_ui/eche_recent_app_click_handler.cc
+++ b/ash/webui/eche_app_ui/eche_recent_app_click_handler.cc
@@ -5,6 +5,9 @@
 #include "ash/webui/eche_app_ui/eche_recent_app_click_handler.h"
 
 #include "ash/components/phonehub/phone_hub_manager.h"
+#include "ash/root_window_controller.h"
+#include "ash/shell.h"
+#include "ash/system/eche/eche_tray.h"
 #include "ash/webui/eche_app_ui/launch_app_helper.h"
 #include "chromeos/components/multidevice/logging/logging.h"
 
@@ -14,9 +17,11 @@
 EcheRecentAppClickHandler::EcheRecentAppClickHandler(
     phonehub::PhoneHubManager* phone_hub_manager,
     FeatureStatusProvider* feature_status_provider,
-    LaunchAppHelper* launch_app_helper)
+    LaunchAppHelper* launch_app_helper,
+    EcheDisplayStreamHandler* display_stream_handler)
     : feature_status_provider_(feature_status_provider),
-      launch_app_helper_(launch_app_helper) {
+      launch_app_helper_(launch_app_helper),
+      display_stream_handler_(display_stream_handler) {
   notification_handler_ =
       phone_hub_manager->GetNotificationInteractionHandler();
   recent_apps_handler_ = phone_hub_manager->GetRecentAppsInteractionHandler();
@@ -28,6 +33,9 @@
     recent_apps_handler_->AddRecentAppClickObserver(this);
     is_click_handler_set_ = true;
   }
+
+  if (features::IsEcheSWAInBackgroundEnabled())
+    display_stream_handler_->AddObserver(this);
 }
 
 EcheRecentAppClickHandler::~EcheRecentAppClickHandler() {
@@ -36,6 +44,9 @@
     notification_handler_->RemoveNotificationClickHandler(this);
   if (recent_apps_handler_)
     recent_apps_handler_->RemoveRecentAppClickObserver(this);
+
+  if (features::IsEcheSWAInBackgroundEnabled())
+    display_stream_handler_->RemoveObserver(this);
 }
 
 void EcheRecentAppClickHandler::HandleNotificationClick(
@@ -62,6 +73,7 @@
       launch_app_helper_->LaunchEcheApp(
           /*notification_id=*/absl::nullopt, app_metadata.package_name,
           app_metadata.visible_app_name, app_metadata.user_id);
+      is_waiting_for_streaming_to_show_ = true;
       break;
     case LaunchAppHelper::AppLaunchProhibitedReason::kDisabledByScreenLock:
       launch_app_helper_->ShowNotification(
@@ -97,6 +109,20 @@
     notification_handler_->RemoveNotificationClickHandler(this);
     recent_apps_handler_->RemoveRecentAppClickObserver(this);
     is_click_handler_set_ = false;
+    is_waiting_for_streaming_to_show_ = false;
+  }
+}
+
+void EcheRecentAppClickHandler::OnStartStreaming() {
+  if (features::IsEcheCustomWidgetEnabled()) {
+    // TODO(paulzchen): Move the eche tray control to factory.
+    auto* eche_tray = Shell::GetPrimaryRootWindowController()
+                          ->GetStatusAreaWidget()
+                          ->eche_tray();
+    if (eche_tray && is_waiting_for_streaming_to_show_) {
+      eche_tray->ShowBubble();
+      is_waiting_for_streaming_to_show_ = false;
+    }
   }
 }
 
diff --git a/ash/webui/eche_app_ui/eche_recent_app_click_handler.h b/ash/webui/eche_app_ui/eche_recent_app_click_handler.h
index f5297a1..efdb6aae 100644
--- a/ash/webui/eche_app_ui/eche_recent_app_click_handler.h
+++ b/ash/webui/eche_app_ui/eche_recent_app_click_handler.h
@@ -12,6 +12,7 @@
 #include "ash/components/phonehub/phone_hub_manager.h"
 #include "ash/components/phonehub/recent_app_click_observer.h"
 #include "ash/components/phonehub/recent_apps_interaction_handler.h"
+#include "ash/webui/eche_app_ui/eche_display_stream_handler.h"
 #include "ash/webui/eche_app_ui/feature_status_provider.h"
 #include "base/callback.h"
 
@@ -23,11 +24,13 @@
 // Handles recent app clicks originating from Phone Hub recent apps.
 class EcheRecentAppClickHandler : public phonehub::NotificationClickHandler,
                                   public FeatureStatusProvider::Observer,
-                                  public phonehub::RecentAppClickObserver {
+                                  public phonehub::RecentAppClickObserver,
+                                  public EcheDisplayStreamHandler::Observer {
  public:
   EcheRecentAppClickHandler(phonehub::PhoneHubManager* phone_hub_manager,
                             FeatureStatusProvider* feature_status_provider,
-                            LaunchAppHelper* launch_app_helper);
+                            LaunchAppHelper* launch_app_helper,
+                            EcheDisplayStreamHandler* display_stream_handler);
   ~EcheRecentAppClickHandler() override;
 
   EcheRecentAppClickHandler(const EcheRecentAppClickHandler&) = delete;
@@ -46,6 +49,14 @@
   // FeatureStatusProvider::Observer:
   void OnFeatureStatusChanged() override;
 
+  // EcheDisplayStreamHandler::Observer:
+  void OnStartStreaming() override;
+
+  // Test helpers
+  bool waiting_for_streaming_to_show() {
+    return is_waiting_for_streaming_to_show_;
+  }
+
  private:
   bool IsClickable(FeatureStatus status);
 
@@ -53,7 +64,9 @@
   phonehub::RecentAppsInteractionHandler* recent_apps_handler_;
   FeatureStatusProvider* feature_status_provider_;
   LaunchAppHelper* launch_app_helper_;
+  EcheDisplayStreamHandler* display_stream_handler_;
   bool is_click_handler_set_ = false;
+  bool is_waiting_for_streaming_to_show_ = false;
 };
 
 }  // namespace eche_app
diff --git a/ash/webui/eche_app_ui/eche_recent_app_click_handler_unittest.cc b/ash/webui/eche_app_ui/eche_recent_app_click_handler_unittest.cc
index 62c24678..1f4f7d4 100644
--- a/ash/webui/eche_app_ui/eche_recent_app_click_handler_unittest.cc
+++ b/ash/webui/eche_app_ui/eche_recent_app_click_handler_unittest.cc
@@ -8,6 +8,12 @@
 
 #include "ash/components/phonehub/fake_phone_hub_manager.h"
 #include "ash/constants/ash_features.h"
+#include "ash/system/eche/eche_tray.h"
+#include "ash/system/status_area_widget_test_helper.h"
+#include "ash/system/tray/tray_bubble_wrapper.h"
+#include "ash/test/ash_test_base.h"
+#include "ash/test/ash_test_suite.h"
+#include "ash/test/test_ash_web_view_factory.h"
 #include "ash/webui/eche_app_ui/fake_feature_status_provider.h"
 #include "ash/webui/eche_app_ui/fake_launch_app_helper.h"
 #include "ash/webui/eche_app_ui/launch_app_helper.h"
@@ -15,11 +21,12 @@
 #include "base/test/scoped_feature_list.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
+#include "ui/base/resource/resource_bundle.h"
 
 namespace ash {
 namespace eche_app {
 
-class EcheRecentAppClickHandlerTest : public testing::Test {
+class EcheRecentAppClickHandlerTest : public AshTestBase {
  protected:
   EcheRecentAppClickHandlerTest() = default;
   EcheRecentAppClickHandlerTest(const EcheRecentAppClickHandlerTest&) = delete;
@@ -27,15 +34,25 @@
       const EcheRecentAppClickHandlerTest&) = delete;
   ~EcheRecentAppClickHandlerTest() override = default;
 
-  // testing::Test:
+  // AshTestBase::Test:
   void SetUp() override {
+    scoped_feature_list_.InitWithFeatures(
+        /*enabled_features=*/{features::kEcheSWA, features::kPhoneHubRecentApps,
+                              features::kEcheCustomWidget},
+        /*disabled_features=*/{});
+
+    DCHECK(test_web_view_factory_.get());
+
+    ui::ResourceBundle::CleanupSharedInstance();
+    AshTestSuite::LoadTestResources();
+    AshTestBase::SetUp();
+    eche_tray_ =
+        ash::StatusAreaWidgetTestHelper::GetStatusAreaWidget()->eche_tray();
+
     fake_phone_hub_manager_.fake_feature_status_provider()->SetStatus(
         phonehub::FeatureStatus::kEnabledAndConnected);
     fake_feature_status_provider_.SetStatus(FeatureStatus::kIneligible);
-    scoped_feature_list_.InitWithFeatures(
-        /*enabled_features=*/{features::kEcheSWA,
-                              features::kPhoneHubRecentApps},
-        /*disabled_features=*/{});
+
     launch_app_helper_ = std::make_unique<FakeLaunchAppHelper>(
         &fake_phone_hub_manager_,
         base::BindRepeating(
@@ -47,13 +64,16 @@
         base::BindRepeating(
             &EcheRecentAppClickHandlerTest::FakeLaunchNotificationFunction,
             base::Unretained(this)));
+    display_stream_handler_ = std::make_unique<EcheDisplayStreamHandler>();
     handler_ = std::make_unique<EcheRecentAppClickHandler>(
         &fake_phone_hub_manager_, &fake_feature_status_provider_,
-        launch_app_helper_.get());
+        launch_app_helper_.get(), display_stream_handler_.get());
   }
 
   void TearDown() override {
+    AshTestBase::TearDown();
     launch_app_helper_.reset();
+    display_stream_handler_.reset();
     handler_.reset();
   }
 
@@ -103,21 +123,35 @@
         ->FetchRecentAppMetadataList();
   }
 
+  void StartStreaming() { handler_->OnStartStreaming(); }
+
   const std::string& get_package_name() { return package_name_; }
 
   const std::u16string& get_visible_name() { return visible_name_; }
 
   int64_t get_user_id() { return user_id_; }
 
+  bool waiting_for_streaming_to_show() {
+    return handler_->waiting_for_streaming_to_show();
+  }
+
+  EcheTray* eche_tray() { return eche_tray_; }
+
  private:
   phonehub::FakePhoneHubManager fake_phone_hub_manager_;
   base::test::ScopedFeatureList scoped_feature_list_;
   FakeFeatureStatusProvider fake_feature_status_provider_;
   std::unique_ptr<LaunchAppHelper> launch_app_helper_;
   std::unique_ptr<EcheRecentAppClickHandler> handler_;
+  std::unique_ptr<EcheDisplayStreamHandler> display_stream_handler_;
   std::string package_name_;
   std::u16string visible_name_;
   int64_t user_id_;
+  EcheTray* eche_tray_ = nullptr;  // Not owned
+
+  // Calling the factory constructor is enough to set it up.
+  std::unique_ptr<TestAshWebViewFactory> test_web_view_factory_ =
+      std::make_unique<TestAshWebViewFactory>();
 };
 
 TEST_F(EcheRecentAppClickHandlerTest, StatusChangeTransitions) {
@@ -182,5 +216,25 @@
   EXPECT_EQ(fake_app_metadata.user_id, app_metadata[0].user_id);
 }
 
+TEST_F(EcheRecentAppClickHandlerTest, StartStreaming) {
+  EXPECT_FALSE(waiting_for_streaming_to_show());
+
+  const int64_t user_id = 1;
+  const char16_t app_visible_name[] = u"Fake App";
+  const char package_name[] = "com.fakeapp";
+  auto fake_app_metadata = phonehub::Notification::AppMetadata(
+      app_visible_name, package_name, gfx::Image(),
+      /*icon_color=*/absl::nullopt, /*icon_is_monochrome=*/true, user_id);
+  RecentAppClicked(fake_app_metadata);
+
+  EXPECT_TRUE(waiting_for_streaming_to_show());
+
+  StartStreaming();
+
+  EXPECT_TRUE(
+      eche_tray()->get_bubble_wrapper_for_test()->bubble_view()->GetVisible());
+  EXPECT_FALSE(waiting_for_streaming_to_show());
+}
+
 }  // namespace eche_app
 }  // namespace ash
diff --git a/ash/webui/eche_app_ui/eche_uid_provider.h b/ash/webui/eche_app_ui/eche_uid_provider.h
index b01264b..5c3ba34 100644
--- a/ash/webui/eche_app_ui/eche_uid_provider.h
+++ b/ash/webui/eche_app_ui/eche_uid_provider.h
@@ -37,6 +37,8 @@
   void Bind(mojo::PendingReceiver<mojom::UidGenerator> receiver);
 
  private:
+  friend class EcheUidProviderTest;
+
   std::string ConvertBinaryToString(base::span<const uint8_t> src);
   absl::optional<std::vector<uint8_t>> ConvertStringToBinary(
       base::StringPiece str,
diff --git a/ash/webui/eche_app_ui/eche_uid_provider_unittest.cc b/ash/webui/eche_app_ui/eche_uid_provider_unittest.cc
index 0eff021..e2f2a0dd 100644
--- a/ash/webui/eche_app_ui/eche_uid_provider_unittest.cc
+++ b/ash/webui/eche_app_ui/eche_uid_provider_unittest.cc
@@ -5,7 +5,8 @@
 #include "ash/webui/eche_app_ui/eche_uid_provider.h"
 
 #include <base/base64.h>
-
+#include "base/task/single_thread_task_runner.h"
+#include "base/test/task_environment.h"
 #include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/testing_pref_service.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -13,16 +14,62 @@
 namespace ash {
 namespace eche_app {
 
+class TaskRunner {
+ public:
+  TaskRunner() = default;
+  ~TaskRunner() = default;
+
+  void WaitForResult() { run_loop_.Run(); }
+
+  void Finish() { run_loop_.Quit(); }
+
+ private:
+  base::test::SingleThreadTaskEnvironment task_environment_;
+  base::RunLoop run_loop_;
+};
+
+class EcheUidProviderTest;
+
 class Callback {
  public:
-  static void GetUidCallback(const std::string& uid) { uid_ = uid; }
+  static void GetUidCallback(const std::string& uid) {
+    uid_ = uid;
+    if (task_runner_) {
+      task_runner_->Finish();
+    }
+  }
+
+  static void setTaskRunner(TaskRunner* task_runner) {
+    task_runner_ = task_runner;
+  }
+
   static std::string GetUid() { return uid_; }
   static void ResetUid() { uid_ = ""; }
 
  private:
+  static TaskRunner* task_runner_;
   static std::string uid_;
 };
 
+class FakeExchangerClient : public mojom::UidGenerator {
+ public:
+  FakeExchangerClient() = default;
+  ~FakeExchangerClient() override = default;
+
+  mojo::PendingReceiver<mojom::UidGenerator> CreatePendingReceiver() {
+    return remote_.BindNewPipeAndPassReceiver();
+  }
+
+  // mojom::UidGenerator:
+  void GetUid(base::OnceCallback<void(const std::string&)> callback) override {
+    remote_->GetUid(base::BindOnce(std::move(callback)));
+  }
+
+ private:
+  mojo::Remote<mojom::UidGenerator> remote_;
+};
+
+ash::eche_app::TaskRunner* ash::eche_app::Callback::task_runner_ = nullptr;
 std::string ash::eche_app::Callback::uid_ = "";
 
 class EcheUidProviderTest : public testing::Test {
@@ -53,10 +100,17 @@
   void GetUid() {
     uid_provider_->GetUid(base::BindOnce(&Callback::GetUidCallback));
   }
+  absl::optional<std::vector<uint8_t>> DecodeStringWithSeed(
+      size_t expected_len) {
+    std::string pref_seed = pref_service_.GetString(kEcheAppSeedPref);
+    return uid_provider_->ConvertStringToBinary(pref_seed, expected_len);
+  }
+
+  TaskRunner task_runner_;
+  std::unique_ptr<EcheUidProvider> uid_provider_;
 
  private:
   TestingPrefServiceSimple pref_service_;
-  std::unique_ptr<EcheUidProvider> uid_provider_;
 };
 
 TEST_F(EcheUidProviderTest, GetUidHasValue) {
@@ -87,5 +141,30 @@
   EXPECT_NE(Callback::GetUid(), uid);
 }
 
+TEST_F(EcheUidProviderTest, BindPendingReceiverCanGetUid) {
+  Callback::setTaskRunner(&task_runner_);
+  FakeExchangerClient fake_exchanger_client;
+  uid_provider_->Bind(fake_exchanger_client.CreatePendingReceiver());
+
+  fake_exchanger_client.GetUid(base::BindOnce(&Callback::GetUidCallback));
+  task_runner_.WaitForResult();
+
+  EXPECT_NE(Callback::GetUid(), "");
+}
+
+TEST_F(EcheUidProviderTest, GetBinaryWhenSeedSizeCorrect) {
+  GetUid();
+  ResetUidProvider();
+
+  EXPECT_NE(DecodeStringWithSeed(kSeedSizeInByte), absl::nullopt);
+}
+
+TEST_F(EcheUidProviderTest, GetNulloptWhenSeedSizeIncorrect) {
+  GetUid();
+  ResetUidProvider();
+
+  EXPECT_EQ(DecodeStringWithSeed(kSeedSizeInByte - 1), absl::nullopt);
+}
+
 }  // namespace eche_app
 }  // namespace ash
diff --git a/ash/webui/eche_app_ui/mojom/eche_app.mojom b/ash/webui/eche_app_ui/mojom/eche_app.mojom
index c05d75c..3bfbaa1 100644
--- a/ash/webui/eche_app_ui/mojom/eche_app.mojom
+++ b/ash/webui/eche_app_ui/mojom/eche_app.mojom
@@ -88,3 +88,10 @@
   ShowNotification(mojo_base.mojom.String16 title,
           mojo_base.mojom.String16 message, WebNotificationType type);
 };
+
+// Interface for streaming a display video with which the connection is
+// established. TODO(paulzchen): Using generic method `OnStreamStatusChanged`.
+interface DisplayStreamHandler {
+  // Stream a display video for Eche.
+  StartStreaming();
+};
diff --git a/ash/webui/eche_app_ui/resources/browser_proxy.js b/ash/webui/eche_app_ui/resources/browser_proxy.js
index d8bd8e4..f1ede4e 100644
--- a/ash/webui/eche_app_ui/resources/browser_proxy.js
+++ b/ash/webui/eche_app_ui/resources/browser_proxy.js
@@ -43,6 +43,8 @@
 const notificationGenerator =
     ash.echeApp.mojom.NotificationGenerator.getRemote();
 
+const displayStreamHandler = ash.echeApp.mojom.DisplayStreamHandler.getRemote();
+
 /**
  * A pipe through which we can send messages to the guest frame.
  * Use an undefined `target` to find the <iframe> automatically.
@@ -148,6 +150,12 @@
            histogramData.maxValue);
      });
 
+ // Register START_STREAMING pipes.
+ guestMessagePipe.registerHandler(Message.START_STREAMING, async () => {
+   console.log('echeapi browser_proxy.js startStreaming');
+   displayStreamHandler.startStreaming();
+ });
+
  // We can't access hash change event inside iframe so parse the notification
  // info from the anchor part of the url when hash is changed and send them to
  // untrusted section via message pipes.
diff --git a/ash/webui/eche_app_ui/resources/message_types.js b/ash/webui/eche_app_ui/resources/message_types.js
index ef67b8b..73259ea 100644
--- a/ash/webui/eche_app_ui/resources/message_types.js
+++ b/ash/webui/eche_app_ui/resources/message_types.js
@@ -89,4 +89,6 @@
   TIME_HISTOGRAM_MESSAGE: 'time_histagram_message',
   // Message for sending metrics data for recording enum histogram.
   ENUM_HISTOGRAM_MESSAGE: 'enum_histagram_message',
+  // Message for starting the display video of Eche.
+  START_STREAMING: 'start_streaming',
 };
diff --git a/ash/webui/eche_app_ui/resources/receiver.js b/ash/webui/eche_app_ui/resources/receiver.js
index fd17229..f598738 100644
--- a/ash/webui/eche_app_ui/resources/receiver.js
+++ b/ash/webui/eche_app_ui/resources/receiver.js
@@ -93,6 +93,11 @@
         Message.SHOW_NOTIFICATION, {title, message, notificationType});
   }
 
+  startStreaming() {
+    console.log('echeapi receiver.js startStreaming');
+    parentMessagePipe.sendMessage(Message.START_STREAMING);
+  }
+
   sendTimeHistogram(histogram, value) {
     console.log('echeapi receiver.js sendTimeHistogram');
     parentMessagePipe.sendMessage(
@@ -131,6 +136,8 @@
     EcheApiBindingImpl.onReceivedNotification.bind(EcheApiBindingImpl);
 echeapi.system.showCrOSNotification =
     EcheApiBindingImpl.showNotification.bind(EcheApiBindingImpl);
+echeapi.system.startStreaming =
+    EcheApiBindingImpl.startStreaming.bind(EcheApiBindingImpl);
 echeapi.system.sendTimeHistogram =
     EcheApiBindingImpl.sendTimeHistogram.bind(EcheApiBindingImpl);
 echeapi.system.sendEnumHistogram =
diff --git a/ash/webui/system_extensions_internals_ui/BUILD.gn b/ash/webui/system_extensions_internals_ui/BUILD.gn
index d260625a..7b0a72a 100644
--- a/ash/webui/system_extensions_internals_ui/BUILD.gn
+++ b/ash/webui/system_extensions_internals_ui/BUILD.gn
@@ -19,6 +19,7 @@
 
   deps = [
     "//ash/webui/resources:system_extensions_internals_resources",
+    "//ash/webui/system_extensions_internals_ui/mojom",
     "//content/public/browser",
     "//ui/webui",
   ]
@@ -30,7 +31,13 @@
 }
 
 js_library("system_extensions_internals") {
-  sources = [ "resources/index.js" ]
+  sources = [
+    "resources/index.js",
+    "resources/page_handler.js",
+  ]
+  externs_list =
+      [ "//ash/webui/web_applications/externs/file_handling.externs.js" ]
+  deps = [ "//ash/webui/system_extensions_internals_ui/mojom:mojom_webui_js" ]
 }
 
 js2gtest("browser_tests_js") {
@@ -43,13 +50,32 @@
 
 grd_prefix = "ash_system_extensions_internals"
 
+mojo_grdp = "$target_gen_dir/system_extensions_internals_mojo_resources.grdp"
+
+generate_grd("build_mojo_grdp") {
+  grd_prefix = grd_prefix
+  out_grd = mojo_grdp
+
+  deps = [ "//ash/webui/system_extensions_internals_ui/mojom:mojom_webui_js" ]
+
+  # Flatten out the dependency tree of your mojom and add generated bindings
+  # file here.
+  input_files = [ "ash/webui/system_extensions_internals_ui/mojom/system_extensions_internals_ui.mojom-webui.js" ]
+
+  input_files_base_dir =
+      rebase_path("$root_gen_dir/mojom-webui", "$root_build_dir")
+}
+
 generate_grd("build_grd") {
   input_files_base_dir = rebase_path("resources", "//")
   input_files = [
     "index.html",
     "index.js",
+    "page_handler.js",
   ]
 
   grd_prefix = grd_prefix
   out_grd = "$target_gen_dir/${grd_prefix}_resources.grd"
+  deps = [ ":build_mojo_grdp" ]
+  grdp_files = [ mojo_grdp ]
 }
diff --git a/ash/webui/system_extensions_internals_ui/mojom/BUILD.gn b/ash/webui/system_extensions_internals_ui/mojom/BUILD.gn
new file mode 100644
index 0000000..0755d379
--- /dev/null
+++ b/ash/webui/system_extensions_internals_ui/mojom/BUILD.gn
@@ -0,0 +1,15 @@
+# Copyright 2021 The Chromium Authors. All rights reserved.
+# 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("//mojo/public/tools/bindings/mojom.gni")
+
+assert(is_chromeos_ash, "System Extensions Internals is ash-chrome only")
+
+mojom("mojom") {
+  sources = [ "system_extensions_internals_ui.mojom" ]
+
+  public_deps = [ "//mojo/public/mojom/base" ]
+  webui_module_path = "/ash/webui/system_extensions_internals_ui/mojom/"
+}
diff --git a/ash/webui/system_extensions_internals_ui/mojom/OWNERS b/ash/webui/system_extensions_internals_ui/mojom/OWNERS
new file mode 100644
index 0000000..08850f4
--- /dev/null
+++ b/ash/webui/system_extensions_internals_ui/mojom/OWNERS
@@ -0,0 +1,2 @@
+per-file *.mojom=set noparent
+per-file *.mojom=file://ipc/SECURITY_OWNERS
diff --git a/ash/webui/system_extensions_internals_ui/mojom/system_extensions_internals_ui.mojom b/ash/webui/system_extensions_internals_ui/mojom/system_extensions_internals_ui.mojom
new file mode 100644
index 0000000..5af20556
--- /dev/null
+++ b/ash/webui/system_extensions_internals_ui/mojom/system_extensions_internals_ui.mojom
@@ -0,0 +1,15 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module ash.mojom.system_extensions_internals;
+
+import "mojo/public/mojom/base/safe_base_name.mojom";
+
+// Interface for installing an unpacked System Extension.
+interface PageHandler {
+  // Installs a system extension from `system_extension_dir_name`, which must be
+  // a folder located at the top level of the default Downloads directory.
+  InstallSystemExtensionFromDownloadsDir(
+    mojo_base.mojom.SafeBaseName system_extension_dir_name) => (bool success);
+};
diff --git a/ash/webui/system_extensions_internals_ui/resources/index.js b/ash/webui/system_extensions_internals_ui/resources/index.js
index 1441a844..b1dc57b 100644
--- a/ash/webui/system_extensions_internals_ui/resources/index.js
+++ b/ash/webui/system_extensions_internals_ui/resources/index.js
@@ -2,9 +2,21 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import {pageHandler} from './page_handler.js';
+
 const chooseDirButton = document.querySelector('#choose-directory');
 const resultDialog = document.querySelector('#result-dialog');
 
 chooseDirButton.addEventListener('click', async event => {
+  const directory = await window.showDirectoryPicker({startIn: 'downloads'});
+  const {success} = await pageHandler.installSystemExtensionFromDownloadsDir(
+      {path: {path: directory.name}});
+  if (success) {
+    resultDialog.textContent =
+        `System Extension in '${directory.name}' was successfully installed.`;
+  } else {
+    resultDialog.textContent =
+        `System Extension in '${directory.name}' failed to be installed.`;
+  }
   resultDialog.showModal();
 });
diff --git a/ash/webui/system_extensions_internals_ui/resources/page_handler.js b/ash/webui/system_extensions_internals_ui/resources/page_handler.js
new file mode 100644
index 0000000..74fd29d
--- /dev/null
+++ b/ash/webui/system_extensions_internals_ui/resources/page_handler.js
@@ -0,0 +1,11 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Convenience module to bind to initialize a PageHandler
+ * remote i.e. a PageHandler that we can use to talk to the browser.
+ */
+import {PageHandler, PageHandlerRemote} from '/ash/webui/system_extensions_internals_ui/mojom/system_extensions_internals_ui.mojom-webui.js';
+
+export const pageHandler = PageHandler.getRemote();
diff --git a/ash/webui/system_extensions_internals_ui/system_extensions_internals_ui.h b/ash/webui/system_extensions_internals_ui/system_extensions_internals_ui.h
index c1dcb1c..5a757713 100644
--- a/ash/webui/system_extensions_internals_ui/system_extensions_internals_ui.h
+++ b/ash/webui/system_extensions_internals_ui/system_extensions_internals_ui.h
@@ -5,6 +5,7 @@
 #ifndef ASH_WEBUI_SYSTEM_EXTENSIONS_INTERNALS_UI_SYSTEM_EXTENSIONS_INTERNALS_UI_H_
 #define ASH_WEBUI_SYSTEM_EXTENSIONS_INTERNALS_UI_SYSTEM_EXTENSIONS_INTERNALS_UI_H_
 
+#include "ash/webui/system_extensions_internals_ui/mojom/system_extensions_internals_ui.mojom.h"
 #include "ui/webui/mojo_web_ui_controller.h"
 
 namespace ash {
@@ -18,6 +19,12 @@
       delete;
   ~SystemExtensionsInternalsUI() override;
 
+  // Implemented in //chrome/browser/chrome_browser_interface_binders.cc
+  // because PageHandler is implemented in //chrome/browser.
+  void BindInterface(
+      mojo::PendingReceiver<mojom::system_extensions_internals::PageHandler>
+          page_handler);
+
  private:
   WEB_UI_CONTROLLER_TYPE_DECL();
 };
diff --git a/base/power_monitor/power_monitor_device_source_stub.cc b/base/power_monitor/power_monitor_device_source_stub.cc
index 96a9ffd..8aeadc9 100644
--- a/base/power_monitor/power_monitor_device_source_stub.cc
+++ b/base/power_monitor/power_monitor_device_source_stub.cc
@@ -8,7 +8,6 @@
 namespace base {
 
 bool PowerMonitorDeviceSource::IsOnBatteryPower() {
-  NOTIMPLEMENTED();
   return false;
 }
 
diff --git a/base/power_monitor/power_monitor_source.h b/base/power_monitor/power_monitor_source.h
index 891dcb6..40f6e5c 100644
--- a/base/power_monitor/power_monitor_source.h
+++ b/base/power_monitor/power_monitor_source.h
@@ -36,8 +36,8 @@
 
   // Reads the initial operating system CPU speed limit, if available on the
   // platform. Otherwise returns PowerThermalObserver::kSpeedLimitMax.
-  // Only called on the main thead in PowerMonitor::Initialize().
-  // The actual speed limit value will be updated asynchronosulsy via the
+  // Only called on the main thread in PowerMonitor::Initialize().
+  // The actual speed limit value will be updated asynchronously via the
   // ProcessSpeedLimitEvent() if/when the value changes.
   virtual int GetInitialSpeedLimit();
 
diff --git a/build/fuchsia/linux_internal.sdk.sha1 b/build/fuchsia/linux_internal.sdk.sha1
index d489e93..ac95ece6 100644
--- a/build/fuchsia/linux_internal.sdk.sha1
+++ b/build/fuchsia/linux_internal.sdk.sha1
@@ -1 +1 @@
-7.20220301.2.2
+7.20220301.3.1
diff --git a/cc/document_transition/document_transition_request.cc b/cc/document_transition/document_transition_request.cc
index 0783540c..3d2eb1df 100644
--- a/cc/document_transition/document_transition_request.cc
+++ b/cc/document_transition/document_transition_request.cc
@@ -35,45 +35,117 @@
   return "<unknown>";
 }
 
+std::string EffectToString(
+    viz::CompositorFrameTransitionDirective::Effect effect) {
+  switch (effect) {
+    case viz::CompositorFrameTransitionDirective::Effect::kNone:
+      return "kNone";
+    case viz::CompositorFrameTransitionDirective::Effect::kCoverDown:
+      return "kCoverDown";
+    case viz::CompositorFrameTransitionDirective::Effect::kCoverLeft:
+      return "kCoverLeft";
+    case viz::CompositorFrameTransitionDirective::Effect::kCoverRight:
+      return "kCoverRight";
+    case viz::CompositorFrameTransitionDirective::Effect::kCoverUp:
+      return "kCoverUp";
+    case viz::CompositorFrameTransitionDirective::Effect::kExplode:
+      return "kExplode";
+    case viz::CompositorFrameTransitionDirective::Effect::kFade:
+      return "kFade";
+    case viz::CompositorFrameTransitionDirective::Effect::kImplode:
+      return "kImplode";
+    case viz::CompositorFrameTransitionDirective::Effect::kRevealDown:
+      return "kRevealDown";
+    case viz::CompositorFrameTransitionDirective::Effect::kRevealLeft:
+      return "kRevealLeft";
+    case viz::CompositorFrameTransitionDirective::Effect::kRevealRight:
+      return "kRevealRight";
+    case viz::CompositorFrameTransitionDirective::Effect::kRevealUp:
+      return "kRevealUp";
+  }
+  return "<unknown>";
+}
+
 }  // namespace
 
 uint32_t DocumentTransitionRequest::s_next_sequence_id_ = 1;
 
 // static
 std::unique_ptr<DocumentTransitionRequest>
-DocumentTransitionRequest::CreateCapture(uint32_t document_tag,
-                                         uint32_t shared_element_count,
-                                         base::OnceClosure commit_callback) {
+DocumentTransitionRequest::CreatePrepare(
+    Effect effect,
+    uint32_t document_tag,
+    TransitionConfig root_config,
+    std::vector<TransitionConfig> shared_element_config,
+    base::OnceClosure commit_callback,
+    bool is_renderer_driven_animation) {
   return base::WrapUnique(new DocumentTransitionRequest(
-      Type::kSave, document_tag, shared_element_count,
-      std::move(commit_callback)));
+      effect, document_tag, root_config, shared_element_config,
+      std::move(commit_callback), is_renderer_driven_animation));
+}
+
+// static
+std::unique_ptr<DocumentTransitionRequest>
+DocumentTransitionRequest::CreateStart(uint32_t document_tag,
+                                       uint32_t shared_element_count,
+                                       base::OnceClosure commit_callback) {
+  return base::WrapUnique(new DocumentTransitionRequest(
+      document_tag, shared_element_count, std::move(commit_callback)));
 }
 
 // static
 std::unique_ptr<DocumentTransitionRequest>
 DocumentTransitionRequest::CreateAnimateRenderer(uint32_t document_tag) {
-  return base::WrapUnique(new DocumentTransitionRequest(
-      Type::kAnimateRenderer, document_tag, 0u, base::DoNothing()));
+  return base::WrapUnique(
+      new DocumentTransitionRequest(Type::kAnimateRenderer, document_tag));
 }
 
 // static
 std::unique_ptr<DocumentTransitionRequest>
 DocumentTransitionRequest::CreateRelease(uint32_t document_tag) {
-  return base::WrapUnique(new DocumentTransitionRequest(
-      Type::kRelease, document_tag, 0u, base::DoNothing()));
+  return base::WrapUnique(
+      new DocumentTransitionRequest(Type::kRelease, document_tag));
 }
 
 DocumentTransitionRequest::DocumentTransitionRequest(
-    Type type,
+    Effect effect,
+    uint32_t document_tag,
+    TransitionConfig root_config,
+    std::vector<TransitionConfig> shared_element_config,
+    base::OnceClosure commit_callback,
+    bool is_renderer_driven_animation)
+    : type_(Type::kSave),
+      effect_(effect),
+      root_config_(root_config),
+      document_tag_(document_tag),
+      shared_element_count_(shared_element_config.size()),
+      shared_element_config_(std::move(shared_element_config)),
+      commit_callback_(std::move(commit_callback)),
+      is_renderer_driven_animation_(is_renderer_driven_animation),
+      sequence_id_(s_next_sequence_id_++) {}
+
+DocumentTransitionRequest::DocumentTransitionRequest(
     uint32_t document_tag,
     uint32_t shared_element_count,
     base::OnceClosure commit_callback)
-    : type_(type),
+    : type_(Type::kAnimate),
       document_tag_(document_tag),
       shared_element_count_(shared_element_count),
       commit_callback_(std::move(commit_callback)),
+      is_renderer_driven_animation_(false),
       sequence_id_(s_next_sequence_id_++) {}
 
+DocumentTransitionRequest::DocumentTransitionRequest(Type type,
+                                                     uint32_t document_tag)
+    : type_(type),
+      document_tag_(document_tag),
+      shared_element_count_(0u),
+      commit_callback_(base::DoNothing()),
+      is_renderer_driven_animation_(true),
+      sequence_id_(s_next_sequence_id_++) {
+  DCHECK(type_ == Type::kAnimateRenderer || type_ == Type::kRelease);
+}
+
 DocumentTransitionRequest::~DocumentTransitionRequest() = default;
 
 viz::CompositorFrameTransitionDirective
@@ -82,7 +154,15 @@
         shared_element_render_pass_id_map) const {
   std::vector<viz::CompositorFrameTransitionDirective::SharedElement>
       shared_elements(shared_element_count_);
+  DCHECK(shared_element_config_.empty() ||
+         shared_element_config_.size() == shared_elements.size());
   for (uint32_t i = 0; i < shared_elements.size(); ++i) {
+    // For transitions with a null element on the source page, we won't find a
+    // render pass below. But we still need to propagate the configuration
+    // params.
+    if (!shared_element_config_.empty())
+      shared_elements[i].config = shared_element_config_[i];
+
     auto it = std::find_if(
         shared_element_render_pass_id_map.begin(),
         shared_element_render_pass_id_map.end(),
@@ -95,17 +175,16 @@
     shared_elements[i].render_pass_id = it->second.render_pass_id;
     shared_elements[i].shared_element_resource_id = it->second.resource_id;
   }
-  // TODO(vmpstr): Clean up the directive parameters.
   return viz::CompositorFrameTransitionDirective(
-      sequence_id_, type_, /*is_renderer_driven_animation=*/true,
-      viz::CompositorFrameTransitionDirective::Effect::kNone, {},
+      sequence_id_, type_, is_renderer_driven_animation_, effect_, root_config_,
       std::move(shared_elements));
 }
 
 std::string DocumentTransitionRequest::ToString() const {
   std::ostringstream str;
-  str << "[type: " << TypeToString(type_) << " sequence_id: " << sequence_id_
-      << "]";
+  str << "[type: " << TypeToString(type_)
+      << " effect: " << EffectToString(effect_)
+      << " sequence_id: " << sequence_id_ << "]";
   return str.str();
 }
 
diff --git a/cc/document_transition/document_transition_request.h b/cc/document_transition/document_transition_request.h
index 6bda2402..a16c3a4 100644
--- a/cc/document_transition/document_transition_request.h
+++ b/cc/document_transition/document_transition_request.h
@@ -24,8 +24,21 @@
 // transition to occur.
 class CC_EXPORT DocumentTransitionRequest {
  public:
-  // Creates a Type::kCapture type of request.
-  static std::unique_ptr<DocumentTransitionRequest> CreateCapture(
+  using Effect = viz::CompositorFrameTransitionDirective::Effect;
+  using TransitionConfig =
+      viz::CompositorFrameTransitionDirective::TransitionConfig;
+
+  // Creates a Type::kPrepare type of request.
+  static std::unique_ptr<DocumentTransitionRequest> CreatePrepare(
+      Effect effect,
+      uint32_t document_tag,
+      TransitionConfig root_config,
+      std::vector<TransitionConfig> shared_element_config,
+      base::OnceClosure commit_callback,
+      bool is_renderer_driven_animation);
+
+  // Creates a Type::kSave type of request.
+  static std::unique_ptr<DocumentTransitionRequest> CreateStart(
       uint32_t document_tag,
       uint32_t shared_element_count,
       base::OnceClosure commit_callback);
@@ -74,15 +87,25 @@
  private:
   using Type = viz::CompositorFrameTransitionDirective::Type;
 
-  DocumentTransitionRequest(Type type,
+  DocumentTransitionRequest(Effect effect,
                             uint32_t document_tag,
+                            TransitionConfig root_config,
+                            std::vector<TransitionConfig> shared_element_config,
+                            base::OnceClosure commit_callback,
+                            bool is_renderer_driven_animation);
+  DocumentTransitionRequest(uint32_t document_tag,
                             uint32_t shared_element_count,
                             base::OnceClosure commit_callback);
+  DocumentTransitionRequest(Type type, uint32_t document_tag);
 
   const Type type_;
+  const Effect effect_ = Effect::kNone;
+  const TransitionConfig root_config_;
   const uint32_t document_tag_;
   const uint32_t shared_element_count_;
+  const std::vector<TransitionConfig> shared_element_config_;
   base::OnceClosure commit_callback_;
+  const bool is_renderer_driven_animation_;
   const uint32_t sequence_id_;
 
   static uint32_t s_next_sequence_id_;
diff --git a/cc/document_transition/document_transition_request_unittest.cc b/cc/document_transition/document_transition_request_unittest.cc
index 40f6af6e..a1f3b553 100644
--- a/cc/document_transition/document_transition_request_unittest.cc
+++ b/cc/document_transition/document_transition_request_unittest.cc
@@ -15,8 +15,10 @@
   bool called = false;
   auto callback = base::BindLambdaForTesting([&called]() { called = true; });
 
-  auto request = DocumentTransitionRequest::CreateCapture(
-      /*document_tag=*/0, /*shared_element_count=*/0, std::move(callback));
+  auto request = DocumentTransitionRequest::CreatePrepare(
+      DocumentTransitionRequest::Effect::kRevealLeft,
+      /*document_tag=*/0, DocumentTransitionRequest::TransitionConfig(),
+      /*shared_element_config=*/{}, std::move(callback), false);
 
   EXPECT_FALSE(called);
   request->TakeFinishedCallback().Run();
@@ -25,29 +27,36 @@
 
   auto directive = request->ConstructDirective({});
   EXPECT_GT(directive.sequence_id(), 0u);
+  EXPECT_EQ(DocumentTransitionRequest::Effect::kRevealLeft, directive.effect());
   EXPECT_EQ(viz::CompositorFrameTransitionDirective::Type::kSave,
             directive.type());
-  EXPECT_TRUE(directive.is_renderer_driven_animation());
+  EXPECT_FALSE(directive.is_renderer_driven_animation());
 
   auto duplicate = request->ConstructDirective({});
   EXPECT_EQ(duplicate.sequence_id(), directive.sequence_id());
+  EXPECT_EQ(duplicate.effect(), directive.effect());
   EXPECT_EQ(duplicate.type(), directive.type());
   EXPECT_EQ(duplicate.is_renderer_driven_animation(),
             directive.is_renderer_driven_animation());
 }
 
 TEST(DocumentTransitionRequestTest, StartRequest) {
-  auto request = DocumentTransitionRequest::CreateAnimateRenderer(
-      /*document_tag=*/0);
+  bool called = false;
+  auto callback = base::BindLambdaForTesting([&called]() { called = true; });
 
+  auto request = DocumentTransitionRequest::CreateStart(
+      /*document_tag=*/0, /*shared_element_transition=*/0, std::move(callback));
+
+  EXPECT_FALSE(called);
   request->TakeFinishedCallback().Run();
+  EXPECT_TRUE(called);
   EXPECT_TRUE(request->TakeFinishedCallback().is_null());
 
   auto directive = request->ConstructDirective({});
   EXPECT_GT(directive.sequence_id(), 0u);
-  EXPECT_EQ(viz::CompositorFrameTransitionDirective::Type::kAnimateRenderer,
+  EXPECT_EQ(viz::CompositorFrameTransitionDirective::Type::kAnimate,
             directive.type());
-  EXPECT_TRUE(directive.is_renderer_driven_animation());
+  EXPECT_FALSE(directive.is_renderer_driven_animation());
 }
 
 }  // namespace cc
diff --git a/cc/trees/layer_tree_host_impl_unittest.cc b/cc/trees/layer_tree_host_impl_unittest.cc
index d6422192..fc42c4e 100644
--- a/cc/trees/layer_tree_host_impl_unittest.cc
+++ b/cc/trees/layer_tree_host_impl_unittest.cc
@@ -18200,8 +18200,8 @@
 
   // Adding a transition effect should cause us to redraw.
   host_impl_->active_tree()->AddDocumentTransitionRequest(
-      DocumentTransitionRequest::CreateAnimateRenderer(
-          /*document_tag=*/0));
+      DocumentTransitionRequest::CreateStart(
+          /*document_tag=*/0, /*shared_element_count=*/0, base::OnceClosure()));
 
   // Ensure there is damage and we requested a redraw.
   host_impl_->OnDraw(draw_transform, draw_viewport, resourceless_software_draw,
diff --git a/cc/trees/layer_tree_host_unittest.cc b/cc/trees/layer_tree_host_unittest.cc
index 158478c..cb9d5c6 100644
--- a/cc/trees/layer_tree_host_unittest.cc
+++ b/cc/trees/layer_tree_host_unittest.cc
@@ -9865,7 +9865,14 @@
 
   void BeginTest() override {
     layer_tree_host()->AddDocumentTransitionRequest(
-        DocumentTransitionRequest::CreateCapture(
+        DocumentTransitionRequest::CreatePrepare(
+            DocumentTransitionRequest::Effect::kExplode,
+            /*document_tag=*/0, DocumentTransitionRequest::TransitionConfig(),
+            /*shared_element_config=*/{},
+            base::BindLambdaForTesting([this]() { CommitLambdaCalled(); }),
+            /*is_renderer_driven_animation=*/false));
+    layer_tree_host()->AddDocumentTransitionRequest(
+        DocumentTransitionRequest::CreateStart(
             /*document_tag=*/0, /*shared_element_count=*/0,
             base::BindLambdaForTesting([this]() { CommitLambdaCalled(); })));
   }
@@ -9874,12 +9881,20 @@
 
   void DisplayReceivedCompositorFrameOnThread(
       const viz::CompositorFrame& frame) override {
-    ASSERT_EQ(1u, frame.metadata.transition_directives.size());
+    ASSERT_EQ(2u, frame.metadata.transition_directives.size());
     const auto& save = frame.metadata.transition_directives[0];
     submitted_sequence_ids_.push_back(save.sequence_id());
 
     EXPECT_EQ(save.type(),
               viz::CompositorFrameTransitionDirective::Type::kSave);
+    EXPECT_EQ(save.effect(),
+              viz::CompositorFrameTransitionDirective::Effect::kExplode);
+
+    const auto& animate = frame.metadata.transition_directives[1];
+    EXPECT_GT(animate.sequence_id(), save.sequence_id());
+    EXPECT_EQ(animate.type(),
+              viz::CompositorFrameTransitionDirective::Type::kAnimate);
+    submitted_sequence_ids_.push_back(animate.sequence_id());
   }
 
   void DidReceiveCompositorFrameAck() override {
@@ -9888,7 +9903,7 @@
     EndTest();
   }
 
-  void AfterTest() override { EXPECT_EQ(1, num_lambda_calls_); }
+  void AfterTest() override { EXPECT_EQ(2, num_lambda_calls_); }
 
   std::vector<uint32_t> submitted_sequence_ids_;
   int num_lambda_calls_ = 0;
diff --git a/chrome/VERSION b/chrome/VERSION
index 5019a18..a700d10b3 100644
--- a/chrome/VERSION
+++ b/chrome/VERSION
@@ -1,4 +1,4 @@
 MAJOR=101
 MINOR=0
-BUILD=4919
+BUILD=4920
 PATCH=0
diff --git a/chrome/android/chrome_test_java_sources.gni b/chrome/android/chrome_test_java_sources.gni
index b989877..828c202 100644
--- a/chrome/android/chrome_test_java_sources.gni
+++ b/chrome/android/chrome_test_java_sources.gni
@@ -136,9 +136,11 @@
   "javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchObserverTest.java",
   "javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchPolicyTest.java",
   "javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchPreferenceFragmentTest.java",
+  "javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchRelatedSearchesTest.java",
   "javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchRequestTest.java",
   "javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchSystemTest.java",
   "javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchTest.java",
+  "javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchTriggerTest.java",
   "javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchUnbatchedTest.java",
   "javatests/src/org/chromium/chrome/browser/contextualsearch/MockContextualSearchPolicy.java",
   "javatests/src/org/chromium/chrome/browser/continuous_search/ContinuousSearchFullUiTest.java",
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchInstrumentationBase.java b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchInstrumentationBase.java
index a2b083ef..b084395 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchInstrumentationBase.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchInstrumentationBase.java
@@ -17,6 +17,7 @@
 import android.graphics.Point;
 import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
+import android.text.TextUtils;
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.widget.LinearLayout;
@@ -191,12 +192,22 @@
     /**
      * A SelectionPopupController that has some methods stubbed out for testing.
      */
-    private static final class StubbedSelectionPopupController
+    protected static final class StubbedSelectionPopupController
             extends TestSelectionPopupController {
         private String mCurrentText;
+        private boolean mIsFocusedNodeEditable;
 
         public StubbedSelectionPopupController() {}
 
+        public void setIsFocusedNodeEditableForTest(boolean isFocusedNodeEditable) {
+            mIsFocusedNodeEditable = isFocusedNodeEditable;
+        }
+
+        @Override
+        public boolean isFocusedNodeEditable() {
+            return mIsFocusedNodeEditable;
+        }
+
         @Override
         public String getSelectedText() {
             return mCurrentText;
@@ -280,9 +291,19 @@
         }
     }
 
+    /**
+     * The DOM node for the word "search" on the test page, which causes a plain search response
+     * with the Search Term "Search" from the Fake server.
+     */
     protected static final String SEARCH_NODE = "search";
     protected static final String SEARCH_NODE_TERM = "Search";
 
+    /**
+     * The DOM node for the word "intelligence" on the test page, which causes a search response
+     * for the Search Term "Intelligence" and also includes Related Searches suggestions.
+     */
+    protected static final String RELATED_SEARCHES_NODE = "intelligence";
+
     private static final String TAG = "CSIBase";
     private static final int TEST_TIMEOUT = 15000;
     private static final int TEST_EXPECTED_FAILURE_TIMEOUT = 1000;
@@ -353,7 +374,7 @@
     private LayoutManagerImpl mLayoutManager;
 
     private ActivityMonitor mActivityMonitor;
-    private ContextualSearchSelectionController mSelectionController;
+    protected ContextualSearchSelectionController mSelectionController;
     private ContextualSearchInstrumentationTestHost mTestHost;
 
     private float mDpToPx;
@@ -665,6 +686,14 @@
     }
 
     /**
+     * Waits for the selected text string to be the given string, and asserts.
+     * @param text The string to wait for the selection to become.
+     */
+    protected void waitForSelectionToBe(final String text) {
+        mTestHost.waitForSelectionToBe(text);
+    }
+
+    /**
      * Asserts that the action bar does or does not become visible in response to a selection.
      * @param visible Whether the Action Bar must become visible or not.
      */
@@ -1145,7 +1174,7 @@
     /**
      * Waits for the Search Panel to expand, and asserts that it did expand.
      */
-    private void waitForPanelToExpand() {
+    protected void waitForPanelToExpand() {
         waitForPanelToEnterState(PanelState.EXPANDED);
     }
 
@@ -1177,6 +1206,42 @@
     }
 
     /**
+     * Asserts that the panel is still in the given state and continues to stay that way
+     * for a while.
+     * Waits for a reasonable amount of time for the panel to change to a different state,
+     * and verifies that it did not change state while this method is executing.
+     * Note that it's quite possible for the panel to transition through some other state and
+     * back to the initial state before this method is called without that being detected,
+     * because this method only monitors state during its own execution.
+     * @param initialState The initial state of the panel at the beginning of an operation that
+     *        should not change the panel state.
+     * @throws InterruptedException
+     */
+    protected void assertPanelStillInState(final @PanelState int initialState)
+            throws InterruptedException {
+        boolean didChangeState = false;
+        long startTime = SystemClock.uptimeMillis();
+        while (!didChangeState
+                && SystemClock.uptimeMillis() - startTime < TEST_EXPECTED_FAILURE_TIMEOUT) {
+            Thread.sleep(DEFAULT_POLLING_INTERVAL);
+            didChangeState = mPanel.getPanelState() != initialState;
+        }
+        Assert.assertFalse(didChangeState);
+    }
+
+    /**
+     * Shorthand for a common sequence:
+     * 1) Waits for gesture processing,
+     * 2) Waits for the panel to close,
+     * 3) Asserts that there is no selection and that the panel closed.
+     */
+    protected void waitForGestureToClosePanelAndAssertNoSelection() {
+        waitForPanelToClose();
+        assertPanelClosedOrUndefined();
+        Assert.assertTrue(TextUtils.isEmpty(getSelectedText()));
+    }
+
+    /**
      * Waits for the selection to be empty. Use this method any time a test repeatedly establishes
      * and dissolves a selection to ensure that the selection has been completely dissolved before
      * simulating the next selection event. This is needed because the renderer's notification of a
@@ -1192,7 +1257,7 @@
     /**
      * Waits for the panel to close and then waits for the selection to dissolve.
      */
-    private void waitForPanelToCloseAndSelectionEmpty() {
+    protected void waitForPanelToCloseAndSelectionEmpty() {
         waitForPanelToClose();
         waitForSelectionEmpty();
     }
@@ -1258,7 +1323,7 @@
     /**
      * Scrolls the base page.
      */
-    private void scrollBasePage() {
+    protected void scrollBasePage() {
         fling(0.f, 0.75f, 0.f, 0.7f, 100);
     }
 
@@ -1384,4 +1449,34 @@
     private Object loggedToRanker(@ContextualSearchInteractionRecorder.Feature int feature) {
         return getRankerLogger().getFeaturesLogged().get(feature);
     }
+
+    /** Asserts that all the expected features have been logged to Ranker. **/
+    protected void assertLoggedAllExpectedFeaturesToRanker() {
+        for (int feature = 0; feature < ContextualSearchInteractionRecorder.Feature.NUM_ENTRIES;
+                feature++) {
+            if (expectedFeatureName(feature) != null) Assert.assertNotNull(loggedToRanker(feature));
+        }
+    }
+
+    /** Asserts that all the expected outcomes have been logged to Ranker. **/
+    protected void assertLoggedAllExpectedOutcomesToRanker() {
+        for (int feature = 0; feature < ContextualSearchInteractionRecorder.Feature.NUM_ENTRIES;
+                feature++) {
+            if (expectedOutcomeName(feature) != null) {
+                Assert.assertNotNull("Expected this outcome to be logged: " + feature,
+                        getRankerLogger().getOutcomesLogged().get(feature));
+            }
+        }
+    }
+
+    /**
+     * Returns whether all the supported gestures for opted-in users trigger a Resolve request,
+     * aka intelligent search.
+     */
+    protected boolean isConfigurationForResolvingGesturesOnly() {
+        // The current interpretation of the ability to resolve Longpress (which is forced by the
+        // Translations Feature as well as the LongpressResolve Feature) preserves a resolving Tap
+        // so there is no non-resolving gesture for opted-in users.
+        return mPolicy.canResolveLongpress();
+    }
 }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManagerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManagerTest.java
index 397e72a..316b72e 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManagerTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManagerTest.java
@@ -96,14 +96,11 @@
 import org.chromium.chrome.test.util.ChromeTabUtils;
 import org.chromium.chrome.test.util.FullscreenTestUtils;
 import org.chromium.chrome.test.util.MenuUtils;
-import org.chromium.components.browser_ui.widget.chips.ChipProperties;
 import org.chromium.components.external_intents.ExternalNavigationHandler;
 import org.chromium.content_public.browser.NavigationHandle;
-import org.chromium.content_public.browser.SelectionClient;
 import org.chromium.content_public.browser.SelectionPopupController;
 import org.chromium.content_public.browser.WebContents;
 import org.chromium.content_public.browser.test.util.DOMUtils;
-import org.chromium.content_public.browser.test.util.TestSelectionPopupController;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
 import org.chromium.content_public.browser.test.util.TouchCommon;
 import org.chromium.net.test.EmbeddedTestServer;
@@ -115,21 +112,14 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeoutException;
 
 // TODO(donnd): Create class with limited API to encapsulate the internals of simulations.
-// TODO(donnd): Separate tests into different classes grouped by type of tests. Examples:
-// Gestures (Tap, Long-press), Search Term Resolution (resolves, expand selection, prevent preload,
-// translation), Panel interaction (tap, fling up/down, close), Content (creation, loading,
-// visibility, history, delayed load), Tab Promotion, Policy (add tests to check if policies
-// affect the behavior correctly), General (remaining tests), etc.
 
 /**
  * Tests the Contextual Search Manager using instrumentation tests.
@@ -781,25 +771,6 @@
         mManager.getOverlayContentDelegate().onMainFrameNavigation(url, false, isFailure, false);
     }
 
-    /**
-     * A SelectionPopupController that has some methods stubbed out for testing.
-     */
-    private static final class StubbedSelectionPopupController
-            extends TestSelectionPopupController {
-        private boolean mIsFocusedNodeEditable;
-
-        public StubbedSelectionPopupController() {}
-
-        public void setIsFocusedNodeEditableForTest(boolean isFocusedNodeEditable) {
-            mIsFocusedNodeEditable = isFocusedNodeEditable;
-        }
-
-        @Override
-        public boolean isFocusedNodeEditable() {
-            return mIsFocusedNodeEditable;
-        }
-    }
-
     //============================================================================================
     // Other Helpers
     // TODO(donnd): organize into sections.
@@ -1362,56 +1333,6 @@
     //============================================================================================
 
     /**
-     * Tests the doesContainAWord method.
-     * TODO(donnd): Change to a unit test.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    public void testDoesContainAWord() {
-        Assert.assertTrue(mSelectionController.doesContainAWord("word"));
-        Assert.assertTrue(mSelectionController.doesContainAWord("word "));
-        Assert.assertFalse("Emtpy string should not be considered a word!",
-                mSelectionController.doesContainAWord(""));
-        Assert.assertFalse("Special symbols should not be considered a word!",
-                mSelectionController.doesContainAWord("@"));
-        Assert.assertFalse("White space should not be considered a word",
-                mSelectionController.doesContainAWord(" "));
-        Assert.assertTrue(mSelectionController.doesContainAWord("Q2"));
-        Assert.assertTrue(mSelectionController.doesContainAWord("123"));
-    }
-
-    /**
-     * Tests the isValidSelection method.
-     * TODO(donnd): Change to a unit test.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    public void testIsValidSelection() {
-        StubbedSelectionPopupController c = new StubbedSelectionPopupController();
-        Assert.assertTrue(mSelectionController.isValidSelection("valid", c));
-        Assert.assertFalse(mSelectionController.isValidSelection(" ", c));
-        c.setIsFocusedNodeEditableForTest(true);
-        Assert.assertFalse(mSelectionController.isValidSelection("editable", c));
-        c.setIsFocusedNodeEditableForTest(false);
-        String numberString = "0123456789";
-        Assert.assertTrue(mSelectionController.isValidSelection(numberString, c));
-        StringBuilder longStringBuilder = new StringBuilder().append(numberString);
-        for (int i = 0; i < 10; i++) {
-            longStringBuilder.append(longStringBuilder.toString());
-            if (longStringBuilder.toString().length() < 1000) {
-                Assert.assertTrue(
-                        mSelectionController.isValidSelection(longStringBuilder.toString(), c));
-            } else {
-                Assert.assertFalse(
-                        mSelectionController.isValidSelection(longStringBuilder.toString(), c));
-                break;
-            }
-        }
-    }
-
-    /**
      * Tests Ranker logging for a simple trigger that resolves.
      */
     @Test
@@ -1437,23 +1358,6 @@
     }
 
     /**
-     * Tests a simple non-resolving gesture, without opening the panel.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    @ParameterAnnotations.UseMethodParameter(FeatureParamProvider.class)
-    public void testNonResolveTrigger(@EnabledFeature int enabledFeature) throws Exception {
-        if (isConfigurationForResolvingGesturesOnly()) return;
-        triggerNonResolve("states");
-
-        Assert.assertNull(mFakeServer.getSearchTermRequested());
-        waitForPanelToPeek();
-        assertLoadedNoUrl();
-        assertNoWebContents();
-    }
-
-    /**
      * Tests swiping the overlay open, after an initial trigger that activates the peeking card.
      */
     @Test
@@ -1585,174 +1489,6 @@
     }
 
     //============================================================================================
-    // Tap=gesture Tests
-    //============================================================================================
-
-    /**
-     * Tests that a Tap gesture on a special character does not select or show the panel.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    // Previously flaky and disabled 4/2021.  https://crbug.com/1180304
-    public void testTapGestureOnSpecialCharacterDoesntSelect() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_NONE);
-
-        clickNode("question-mark");
-        Assert.assertNull(getSelectedText());
-        assertPanelClosedOrUndefined();
-        assertLoadedNoUrl();
-    }
-
-    /**
-     * Tests that a Tap gesture followed by scrolling clears the selection.
-     */
-    @Test
-    @DisableIf.
-    Build(sdk_is_greater_than = Build.VERSION_CODES.LOLLIPOP, message = "crbug.com/841017")
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    public void testTapGestureFollowedByScrollClearsSelection() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_NONE);
-
-        clickWordNode("intelligence");
-        fakeResponse(false, 200, "Intelligence", "Intelligence", "alternate-term", false);
-        assertContainsParameters("Intelligence", "alternate-term");
-        waitForPanelToPeek();
-        assertLoadedLowPriorityUrl();
-        scrollBasePage();
-        assertPanelClosedOrUndefined();
-        Assert.assertTrue(TextUtils.isEmpty(mSelectionController.getSelectedText()));
-    }
-
-    /**
-     * Tests that a Tap gesture followed by tapping an invalid character doesn't select.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    // Previously flaky and disabled 4/2021.  https://crbug.com/1192285
-    public void testTapGestureFollowedByInvalidTextTapCloses() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_NONE);
-
-        clickWordNode("states-far");
-        waitForPanelToPeek();
-        clickNode("question-mark");
-        waitForPanelToClose();
-        Assert.assertNull(mSelectionController.getSelectedText());
-    }
-
-    /**
-     * Tests that a Tap gesture followed by tapping a non-text character doesn't select.
-     * @SmallTest
-     * @Feature({"ContextualSearch"})
-     * crbug.com/665633
-     */
-    @Test
-    @DisabledTest
-    public void testTapGestureFollowedByNonTextTap() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_NONE);
-
-        clickWordNode("states-far");
-        waitForPanelToPeek();
-        clickNode("button");
-        waitForPanelToCloseAndSelectionEmpty();
-    }
-
-    /**
-     * Tests that a Tap gesture far away toggles selecting text.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    public void testTapGestureFarAwayTogglesSelecting() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_NONE);
-
-        clickWordNode("states");
-        Assert.assertEquals("States", getSelectedText());
-        waitForPanelToPeek();
-        clickNode("states-far");
-        waitForPanelToClose();
-        Assert.assertNull(getSelectedText());
-        clickNode("states-far");
-        waitForPanelToPeek();
-        Assert.assertEquals("States", getSelectedText());
-    }
-
-    /**
-     * Tests a "retap" -- that sequential Tap gestures nearby keep selecting.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    @DisabledTest(message = "https://crbug.com/1075895")
-    public void testTapGesturesNearbyKeepSelecting() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_NONE);
-
-        clickWordNode("states");
-        Assert.assertEquals("States", getSelectedText());
-        waitForPanelToPeek();
-        assertLoggedAllExpectedFeaturesToRanker();
-        // Avoid issues with double-tap detection by ensuring sequential taps
-        // aren't treated as such. Double-tapping can also select words much as
-        // longpress, in turn showing the pins and preventing contextual tap
-        // refinement from nearby taps. The double-tap timeout is sufficiently
-        // short that this shouldn't conflict with tap refinement by the user.
-        Thread.sleep(ViewConfiguration.getDoubleTapTimeout());
-        // Because sequential taps never hide the bar, we we can't wait for it to peek.
-        // Instead we use clickNode (which doesn't wait) instead of clickWordNode and wait
-        // for the selection to change.
-        clickNode("states-near");
-        waitForSelectionToBe("StatesNear");
-        assertLoggedAllExpectedOutcomesToRanker();
-        assertLoggedAllExpectedFeaturesToRanker();
-        Thread.sleep(ViewConfiguration.getDoubleTapTimeout());
-        clickNode("states");
-        waitForSelectionToBe("States");
-        assertLoggedAllExpectedOutcomesToRanker();
-    }
-
-    //============================================================================================
-    // Long-press non-triggering gesture tests.
-    //============================================================================================
-
-    /**
-     * Tests that a long-press gesture followed by scrolling does not clear the selection.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    @ParameterAnnotations.UseMethodParameter(FeatureParamProvider.class)
-    @DisableIf.Build(sdk_is_greater_than = Build.VERSION_CODES.O, message = "crbug.com/1071080")
-    public void testLongPressGestureFollowedByScrollMaintainsSelection(
-            @EnabledFeature int enabledFeature) throws Exception {
-        longPressNode("intelligence");
-        waitForPanelToPeek();
-        scrollBasePage();
-        assertPanelClosedOrUndefined();
-        Assert.assertEquals("Intelligence", getSelectedText());
-        assertLoadedNoUrl();
-    }
-
-    /**
-     * Tests that a long-press gesture followed by a tap does not select.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
-    @DisabledTest(message = "See https://crbug.com/837998")
-    public void testLongPressGestureFollowedByTapDoesntSelect() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_NONE);
-
-        longPressNode("intelligence");
-        waitForPanelToPeek();
-        clickWordNode("states-far");
-        waitForGestureToClosePanelAndAssertNoSelection();
-        assertLoadedNoUrl();
-    }
-
-    //============================================================================================
     // Various Tests
     //============================================================================================
 
@@ -1814,88 +1550,6 @@
     }
 
     //============================================================================================
-    // Tap-non-triggering when ARIA annotated as interactive.
-    //============================================================================================
-
-    /**
-     * Tests that a Tap gesture on an element with an ARIA role does not trigger.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    @DisabledTest(message = "http://crbug.com/1296677")
-    public void testTapOnRoleIgnored() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_NONE);
-
-        @PanelState
-        int initialState = mPanel.getPanelState();
-        clickNode("role");
-        assertPanelStillInState(initialState);
-    }
-
-    /**
-     * Tests that a Tap gesture on an element with an ARIA attribute does not trigger.
-     * http://crbug.com/542874
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    // Previously flaky and disabled 4/2021.  https://crbug.com/1192285
-    @DisabledTest(message = "https://crbug.com/1291558")
-    public void testTapOnARIAIgnored() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_NONE);
-
-        @PanelState
-        int initialState = mPanel.getPanelState();
-        clickNode("aria");
-        assertPanelStillInState(initialState);
-    }
-
-    /**
-     * Tests that a Tap gesture on an element that is focusable does not trigger.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    public void testTapOnFocusableIgnored() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_NONE);
-
-        @PanelState
-        int initialState = mPanel.getPanelState();
-        clickNode("focusable");
-        assertPanelStillInState(initialState);
-    }
-
-    //============================================================================================
-    // Search-term resolution (server request to determine a search).
-    //============================================================================================
-
-    /**
-     * Tests expanding the panel before the search term has resolved, verifies that nothing
-     * loads until the resolve completes and that it's now a normal priority URL.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    @ParameterAnnotations.UseMethodParameter(FeatureParamProvider.class)
-    public void testExpandBeforeSearchTermResolution(@EnabledFeature int enabledFeature)
-            throws Exception {
-        simulateSlowResolveSearch("states");
-        assertNoWebContents();
-
-        // Expanding before the search term resolves should not load anything.
-        tapPeekingBarToExpandAndAssert();
-        assertLoadedNoUrl();
-
-        // Once the response comes in, it should load.
-        simulateSlowResolveFinished();
-        assertContainsParameters("States");
-        assertLoadedNormalPriorityUrl();
-        assertWebContentsCreated();
-        assertWebContentsVisible();
-    }
-
-    //============================================================================================
     // Undecided/Decided users.
     //============================================================================================
 
@@ -2024,40 +1678,6 @@
     }
 
     /**
-     * Tests that the Contextual Search panel does not reappear when a long-press selection is
-     * modified after the user has taken an action to explicitly dismiss the panel. Also tests
-     * that the panel reappears when a new selection is made.
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    // Previously flaky, disabled 4/2021.  https://crbug.com/1192285
-    @DisabledTest(message = "https://crbug.com/1291558")
-    public void testPreventHandlingCurrentSelectionModification() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_NONE);
-
-        simulateNonResolveSearch("search");
-
-        // Dismiss the Contextual Search panel.
-        closePanel();
-        Assert.assertEquals("Search", getSelectedText());
-
-        // Simulate a selection change event and assert that the panel has not reappeared.
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            SelectionClient selectionClient = mManager.getContextualSearchSelectionClient();
-            selectionClient.onSelectionEvent(
-                    SelectionEventType.SELECTION_HANDLE_DRAG_STARTED, 333, 450);
-            selectionClient.onSelectionEvent(
-                    SelectionEventType.SELECTION_HANDLE_DRAG_STOPPED, 303, 450);
-        });
-        assertPanelClosedOrUndefined();
-
-        // Select a different word and assert that the panel has appeared.
-        simulateNonResolveSearch("resolution");
-        // The simulateNonResolveSearch call will verify that the panel peeks.
-    }
-
-    /**
      * Tests ContextualSearchManager#shouldInterceptNavigation for a case that an external
      * navigation has a user gesture.
      */
@@ -2183,26 +1803,6 @@
         Assert.assertEquals(0, mActivityMonitor.getHits());
     }
 
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    @ParameterAnnotations.UseMethodParameter(FeatureParamProvider.class)
-    // Previously flaky and disabled 4/2021.  https://crbug.com/1180304
-    public void testSelectionExpansionOnSearchTermResolution(@EnabledFeature int enabledFeature)
-            throws Exception {
-        mFakeServer.reset();
-        triggerResolve("intelligence");
-        waitForPanelToPeek();
-
-        ResolvedSearchTerm resolvedSearchTerm =
-                new ResolvedSearchTerm
-                        .Builder(false, 200, "Intelligence", "United States Intelligence")
-                        .setSelectionStartAdjust(-14)
-                        .build();
-        fakeResponse(resolvedSearchTerm);
-        waitForSelectionToBe("United States Intelligence");
-    }
-
     //============================================================================================
     // Translate Tests
     //============================================================================================
@@ -2907,224 +2507,4 @@
         Assert.assertEquals(2, userActionMonitor.get("ContextualSearch.ManualRefine"));
         Assert.assertEquals(2, userActionMonitor.get("ContextualSearch.SelectionEstablished"));
     }
-
-    // --------------------------------------------------------------------------------------------
-    // Related Searches Feature tests: base feature enables requests, UI feature allows results.
-    // --------------------------------------------------------------------------------------------
-
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    public void testRelatedSearchesInBar() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_RELATED_SEARCHES_IN_BAR);
-        mFakeServer.reset();
-        FakeResolveSearch fakeSearch = simulateResolveSearch("intelligence");
-        ResolvedSearchTerm resolvedSearchTerm = fakeSearch.getResolvedSearchTerm();
-        Assert.assertTrue("Related Searches results should have been returned but were not!",
-                !resolvedSearchTerm.relatedSearchesJson().isEmpty());
-        // Select a chip in the Bar, which should expand the panel.
-        final int chipToSelect = 1;
-        TestThreadUtils.runOnUiThreadBlocking(
-                () -> mPanel.getRelatedSearchesInBarControl().selectChipForTest(chipToSelect));
-        waitForPanelToExpand();
-
-        // Close the panel
-        closePanel();
-        // TODO(donnd): Validate UMA metrics once we log in-bar selections.
-    }
-
-    /**
-     * Tests that the offset of the SERP is unaffected by whether we are showing Related Searches
-     * in the Bar or not. See https://crbug.com/1250546.
-     * @throws Exception
-     */
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    public void testRelatedSearchesInBarSerpOffset() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_RELATED_SEARCHES_IN_BAR);
-        mFakeServer.reset();
-        simulateResolveSearch(SIMPLE_SEARCH_NODE_ID);
-        float plainSearchBarHeight = mPanel.getBarHeight();
-        float plainSearchContentY = mPanel.getContentY();
-        closePanel();
-
-        // Bring up a panel with Related Searches in order to expand the Bar
-        simulateResolveSearch(RELATED_SEARCHES_NODE_ID);
-        // Wait for the animation to start growing the Bar.
-        CriteriaHelper.pollUiThread(() -> {
-            Criteria.checkThat(
-                    mPanel.getInBarRelatedSearchesAnimatedHeightDps(), Matchers.greaterThan(0f));
-        });
-        // We should have a taller Bar, but that should not affect the Y offset of the content.
-        Assert.assertNotEquals(
-                "Test code failure - unable to open panels with differing Bar heights!",
-                plainSearchBarHeight, mPanel.getBarHeight(), 0.1f);
-        Assert.assertEquals("SERP content offsets with and without Related Searches should match!",
-                plainSearchContentY, mPanel.getContentY(), 0.1f);
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    public void testRelatedSearchesInBarWithDefaultQuery() throws Exception {
-        FeatureList.TestValues testValues = new FeatureList.TestValues();
-        testValues.setFeatureFlagsOverride(ENABLE_RELATED_SEARCHES_IN_BAR);
-        testValues.addFieldTrialParamOverride(ChromeFeatureList.RELATED_SEARCHES_IN_BAR,
-                ContextualSearchFieldTrial.RELATED_SEARCHES_SHOW_DEFAULT_QUERY_CHIP_PARAM_NAME,
-                "true");
-        FeatureList.setTestValues(testValues);
-        mFakeServer.reset();
-
-        FakeResolveSearch fakeSearch = simulateResolveSearch("intelligence");
-        ResolvedSearchTerm resolvedSearchTerm = fakeSearch.getResolvedSearchTerm();
-        Assert.assertTrue("Related Searches results should have been returned but were not!",
-                !resolvedSearchTerm.relatedSearchesJson().isEmpty());
-        // Select a chip in the Bar, which should expand the panel.
-        final int chipToSelect = 0;
-        TestThreadUtils.runOnUiThreadBlocking(
-                () -> mPanel.getRelatedSearchesInBarControl().selectChipForTest(chipToSelect));
-        waitForPanelToExpand();
-
-        CriteriaHelper.pollUiThread(() -> {
-            Criteria.checkThat(
-                    mPanel.getSearchBarControl().getSearchTerm(), Matchers.is("Intelligence"));
-        });
-
-        // Close the panel
-        closePanel();
-        // TODO(donnd): Validate UMA metrics once we log in-bar selections.
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    @DisabledTest(message = "https://crbug.com/1244089")
-    public void testRelatedSearchesInBarWithDefaultQuery_HighlightDefaultQuery() throws Exception {
-        FeatureList.TestValues testValues = new FeatureList.TestValues();
-        testValues.setFeatureFlagsOverride(ENABLE_RELATED_SEARCHES_IN_BAR);
-        testValues.addFieldTrialParamOverride(ChromeFeatureList.RELATED_SEARCHES_IN_BAR,
-                ContextualSearchFieldTrial.RELATED_SEARCHES_SHOW_DEFAULT_QUERY_CHIP_PARAM_NAME,
-                "true");
-        FeatureList.setTestValues(testValues);
-        mFakeServer.reset();
-
-        FakeResolveSearch fakeSearch = simulateResolveSearch("intelligence");
-        ResolvedSearchTerm resolvedSearchTerm = fakeSearch.getResolvedSearchTerm();
-        Assert.assertTrue("Related Searches results should have been returned but were not!",
-                !resolvedSearchTerm.relatedSearchesJson().isEmpty());
-        // Select a chip in the Bar, which should expand the panel.
-        tapPeekingBarToExpandAndAssert();
-
-        CriteriaHelper.pollUiThread(() -> {
-            Criteria.checkThat(
-                    mPanel.getSearchBarControl().getSearchTerm(), Matchers.is("Intelligence"));
-            Criteria.checkThat(mPanel.getRelatedSearchesInBarControl().getSelectedChipForTest(),
-                    Matchers.is(0));
-        });
-
-        // Close the panel
-        closePanel();
-        // TODO(donnd): Validate UMA metrics once we log in-bar selections.
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    public void testRelatedSearchesInBarWithDefaultQuery_Ellipsize() throws Exception {
-        FeatureList.TestValues testValues = new FeatureList.TestValues();
-        testValues.setFeatureFlagsOverride(ENABLE_RELATED_SEARCHES_IN_BAR);
-        testValues.addFieldTrialParamOverride(ChromeFeatureList.RELATED_SEARCHES_IN_BAR,
-                ContextualSearchFieldTrial.RELATED_SEARCHES_SHOW_DEFAULT_QUERY_CHIP_PARAM_NAME,
-                "true");
-        testValues.addFieldTrialParamOverride(ChromeFeatureList.RELATED_SEARCHES_IN_BAR,
-                ContextualSearchFieldTrial
-                        .RELATED_SEARCHES_DEFAULT_QUERY_CHIP_MAX_WIDTH_SP_PARAM_NAME,
-                "60");
-        FeatureList.setTestValues(testValues);
-        mFakeServer.reset();
-
-        FakeResolveSearch fakeSearch = simulateResolveSearch("intelligence");
-        ResolvedSearchTerm resolvedSearchTerm = fakeSearch.getResolvedSearchTerm();
-        Assert.assertTrue("Related Searches results should have been returned but were not!",
-                !resolvedSearchTerm.relatedSearchesJson().isEmpty());
-        // Select a chip in the Bar, which should expand the panel.
-        tapPeekingBarToExpandAndAssert();
-
-        CriteriaHelper.pollUiThread(() -> {
-            Criteria.checkThat(
-                    mPanel.getRelatedSearchesInBarControl().getChipsForTest().get(0).model.get(
-                            ChipProperties.TEXT_MAX_WIDTH_PX),
-                    Matchers.not(ChipProperties.SHOW_WHOLE_TEXT));
-        });
-
-        // Close the panel
-        closePanel();
-        // TODO(donnd): Validate UMA metrics once we log in-bar selections.
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    public void testRelatedSearchesInBarForDefinitionCard() throws Exception {
-        CompositorAnimationHandler.setTestingMode(true);
-        FeatureList.setTestFeatures(ENABLE_RELATED_SEARCHES_IN_BAR);
-        mFakeServer.reset();
-        // Do a normal search without Related Searches or Definition cards.
-        simulateResolveSearch("search");
-        float normalHeight = mPanel.getHeight();
-
-        // Simulate a response that includes both a definition and Related Searches
-        List<String> inBarSuggestions = new ArrayList<String>();
-        inBarSuggestions.add("Related Suggestion 1");
-        inBarSuggestions.add("Related Suggestion 2");
-        TestThreadUtils.runOnUiThreadBlocking(
-                ()
-                        -> mPanel.onSearchTermResolved("obscure · əbˈskyo͝or", null, null,
-                                QuickActionCategory.NONE, CardTag.CT_DEFINITION, inBarSuggestions,
-                                false /* showDefaultSearchInBar */,
-                                null /* relatedSearchesInContent */,
-                                false /* showDefaultSearchInContent */));
-        boolean didPanelGetTaller = mPanel.getHeight() > normalHeight;
-        Assert.assertTrue(
-                "Related Searches should show in a taller Bar when there's a definition card, "
-                        + "but they did not!",
-                didPanelGetTaller);
-        // Clean up
-        closePanel();
-        CompositorAnimationHandler.setTestingMode(false);
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"ContextualSearch"})
-    @DisabledTest(message = "https://crbug.com/1251774")
-    public void testRelatedSearchesDismissDuringAnimation() throws Exception {
-        FeatureList.setTestFeatures(ENABLE_RELATED_SEARCHES_IN_BAR);
-        mFakeServer.reset();
-        // Use the "intelligence" node to generate Related Searches suggestions.
-        simulateResolveSearch("intelligence");
-
-        // Wait for the animation to start growing the Bar.
-        CriteriaHelper.pollUiThread(() -> {
-            Criteria.checkThat(
-                    mPanel.getInBarRelatedSearchesAnimatedHeightDps(), Matchers.greaterThan(0f));
-        });
-
-        // Wait for the animation to change to make sure that doesn't bring the Bar back
-        final boolean[] didAnimationChange = {false};
-        mPanel.getSearchBarControl().setInBarAnimationTestNotifier(
-                () -> { didAnimationChange[0] = true; });
-        CriteriaHelper.pollUiThread(
-                () -> { Criteria.checkThat(didAnimationChange[0], Matchers.is(true)); });
-        // Repeatedly closing the panel should not bring it back even during ongoing animation.
-        closePanel();
-        Assert.assertFalse("The panel is showing again due to Animation!", mPanel.isShowing());
-        // Another scroll might try to close the panel when it thinks it's already closed, which
-        // could fail due to inconsistencies in internal logic, so test that too.
-        closePanel();
-        Assert.assertFalse("Expected the panel to not be showing after a close! "
-                        + "Animation of the Bar height is the likely cause.",
-                mPanel.isShowing());
-    }
 }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchRelatedSearchesTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchRelatedSearchesTest.java
new file mode 100644
index 0000000..1bc4fbf
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchRelatedSearchesTest.java
@@ -0,0 +1,275 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// 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.contextualsearch;
+
+import static org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE;
+
+import androidx.test.filters.SmallTest;
+
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.FeatureList;
+import org.chromium.base.test.BaseJUnit4ClassRunner;
+import org.chromium.base.test.util.Batch;
+import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.base.test.util.Criteria;
+import org.chromium.base.test.util.CriteriaHelper;
+import org.chromium.base.test.util.DisabledTest;
+import org.chromium.base.test.util.Feature;
+import org.chromium.base.test.util.Restriction;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
+import org.chromium.chrome.browser.flags.ChromeSwitches;
+import org.chromium.chrome.browser.layouts.animation.CompositorAnimationHandler;
+import org.chromium.components.browser_ui.widget.chips.ChipProperties;
+import org.chromium.content_public.browser.test.util.TestThreadUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests the Related Searches Feature of Contextual Search using instrumentation tests.
+ */
+@RunWith(BaseJUnit4ClassRunner.class)
+// NOTE: Disable online detection so we we'll default to online on test bots with no network.
+@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
+        ContextualSearchFieldTrial.ONLINE_DETECTION_DISABLED})
+@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
+@Batch(Batch.PER_CLASS)
+public class ContextualSearchRelatedSearchesTest extends ContextualSearchInstrumentationBase {
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        mTestPage = "/chrome/test/data/android/contextualsearch/tap_test.html";
+        super.setUp();
+    }
+
+    // --------------------------------------------------------------------------------------------
+    // Related Searches Feature tests: base feature enables requests, UI feature allows results.
+    // --------------------------------------------------------------------------------------------
+
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    public void testRelatedSearchesInBar() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_RELATED_SEARCHES_IN_BAR);
+        mFakeServer.reset();
+        ContextualSearchFakeServer.FakeResolveSearch fakeSearch =
+                simulateResolveSearch("intelligence");
+        ResolvedSearchTerm resolvedSearchTerm = fakeSearch.getResolvedSearchTerm();
+        Assert.assertTrue("Related Searches results should have been returned but were not!",
+                !resolvedSearchTerm.relatedSearchesJson().isEmpty());
+        // Select a chip in the Bar, which should expand the panel.
+        final int chipToSelect = 1;
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> mPanel.getRelatedSearchesInBarControl().selectChipForTest(chipToSelect));
+        waitForPanelToExpand();
+
+        // Close the panel
+        closePanel();
+        // TODO(donnd): Validate UMA metrics once we log in-bar selections.
+    }
+
+    /**
+     * Tests that the offset of the SERP is unaffected by whether we are showing Related Searches
+     * in the Bar or not. See https://crbug.com/1250546.
+     * @throws Exception
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    public void testRelatedSearchesInBarSerpOffset() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_RELATED_SEARCHES_IN_BAR);
+        mFakeServer.reset();
+        simulateResolveSearch(SEARCH_NODE);
+        float plainSearchBarHeight = mPanel.getBarHeight();
+        float plainSearchContentY = mPanel.getContentY();
+        closePanel();
+
+        // Bring up a panel with Related Searches in order to expand the Bar
+        simulateResolveSearch(RELATED_SEARCHES_NODE);
+        // Wait for the animation to start growing the Bar.
+        CriteriaHelper.pollUiThread(() -> {
+            Criteria.checkThat(
+                    mPanel.getInBarRelatedSearchesAnimatedHeightDps(), Matchers.greaterThan(0f));
+        });
+        // We should have a taller Bar, but that should not affect the Y offset of the content.
+        Assert.assertNotEquals(
+                "Test code failure - unable to open panels with differing Bar heights!",
+                plainSearchBarHeight, mPanel.getBarHeight(), 0.1f);
+        Assert.assertEquals("SERP content offsets with and without Related Searches should match!",
+                plainSearchContentY, mPanel.getContentY(), 0.1f);
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    public void testRelatedSearchesInBarWithDefaultQuery() throws Exception {
+        FeatureList.TestValues testValues = new FeatureList.TestValues();
+        testValues.setFeatureFlagsOverride(ENABLE_RELATED_SEARCHES_IN_BAR);
+        testValues.addFieldTrialParamOverride(ChromeFeatureList.RELATED_SEARCHES_IN_BAR,
+                ContextualSearchFieldTrial.RELATED_SEARCHES_SHOW_DEFAULT_QUERY_CHIP_PARAM_NAME,
+                "true");
+        FeatureList.setTestValues(testValues);
+        mFakeServer.reset();
+
+        ContextualSearchFakeServer.FakeResolveSearch fakeSearch =
+                simulateResolveSearch("intelligence");
+        ResolvedSearchTerm resolvedSearchTerm = fakeSearch.getResolvedSearchTerm();
+        Assert.assertTrue("Related Searches results should have been returned but were not!",
+                !resolvedSearchTerm.relatedSearchesJson().isEmpty());
+        // Select a chip in the Bar, which should expand the panel.
+        final int chipToSelect = 0;
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> mPanel.getRelatedSearchesInBarControl().selectChipForTest(chipToSelect));
+        waitForPanelToExpand();
+
+        CriteriaHelper.pollUiThread(() -> {
+            Criteria.checkThat(
+                    mPanel.getSearchBarControl().getSearchTerm(), Matchers.is("Intelligence"));
+        });
+
+        // Close the panel
+        closePanel();
+        // TODO(donnd): Validate UMA metrics once we log in-bar selections.
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    @DisabledTest(message = "https://crbug.com/1244089")
+    public void testRelatedSearchesInBarWithDefaultQuery_HighlightDefaultQuery() throws Exception {
+        FeatureList.TestValues testValues = new FeatureList.TestValues();
+        testValues.setFeatureFlagsOverride(ENABLE_RELATED_SEARCHES_IN_BAR);
+        testValues.addFieldTrialParamOverride(ChromeFeatureList.RELATED_SEARCHES_IN_BAR,
+                ContextualSearchFieldTrial.RELATED_SEARCHES_SHOW_DEFAULT_QUERY_CHIP_PARAM_NAME,
+                "true");
+        FeatureList.setTestValues(testValues);
+        mFakeServer.reset();
+
+        ContextualSearchFakeServer.FakeResolveSearch fakeSearch =
+                simulateResolveSearch("intelligence");
+        ResolvedSearchTerm resolvedSearchTerm = fakeSearch.getResolvedSearchTerm();
+        Assert.assertTrue("Related Searches results should have been returned but were not!",
+                !resolvedSearchTerm.relatedSearchesJson().isEmpty());
+        // Select a chip in the Bar, which should expand the panel.
+        tapPeekingBarToExpandAndAssert();
+
+        CriteriaHelper.pollUiThread(() -> {
+            Criteria.checkThat(
+                    mPanel.getSearchBarControl().getSearchTerm(), Matchers.is("Intelligence"));
+            Criteria.checkThat(mPanel.getRelatedSearchesInBarControl().getSelectedChipForTest(),
+                    Matchers.is(0));
+        });
+
+        // Close the panel
+        closePanel();
+        // TODO(donnd): Validate UMA metrics once we log in-bar selections.
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    public void testRelatedSearchesInBarWithDefaultQuery_Ellipsize() throws Exception {
+        FeatureList.TestValues testValues = new FeatureList.TestValues();
+        testValues.setFeatureFlagsOverride(ENABLE_RELATED_SEARCHES_IN_BAR);
+        testValues.addFieldTrialParamOverride(ChromeFeatureList.RELATED_SEARCHES_IN_BAR,
+                ContextualSearchFieldTrial.RELATED_SEARCHES_SHOW_DEFAULT_QUERY_CHIP_PARAM_NAME,
+                "true");
+        testValues.addFieldTrialParamOverride(ChromeFeatureList.RELATED_SEARCHES_IN_BAR,
+                ContextualSearchFieldTrial
+                        .RELATED_SEARCHES_DEFAULT_QUERY_CHIP_MAX_WIDTH_SP_PARAM_NAME,
+                "60");
+        FeatureList.setTestValues(testValues);
+        mFakeServer.reset();
+
+        ContextualSearchFakeServer.FakeResolveSearch fakeSearch =
+                simulateResolveSearch("intelligence");
+        ResolvedSearchTerm resolvedSearchTerm = fakeSearch.getResolvedSearchTerm();
+        Assert.assertTrue("Related Searches results should have been returned but were not!",
+                !resolvedSearchTerm.relatedSearchesJson().isEmpty());
+        // Select a chip in the Bar, which should expand the panel.
+        tapPeekingBarToExpandAndAssert();
+
+        CriteriaHelper.pollUiThread(() -> {
+            Criteria.checkThat(
+                    mPanel.getRelatedSearchesInBarControl().getChipsForTest().get(0).model.get(
+                            ChipProperties.TEXT_MAX_WIDTH_PX),
+                    Matchers.not(ChipProperties.SHOW_WHOLE_TEXT));
+        });
+
+        // Close the panel
+        closePanel();
+        // TODO(donnd): Validate UMA metrics once we log in-bar selections.
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    public void testRelatedSearchesInBarForDefinitionCard() throws Exception {
+        CompositorAnimationHandler.setTestingMode(true);
+        FeatureList.setTestFeatures(ENABLE_RELATED_SEARCHES_IN_BAR);
+        mFakeServer.reset();
+        // Do a normal search without Related Searches or Definition cards.
+        simulateResolveSearch("search");
+        float normalHeight = mPanel.getHeight();
+
+        // Simulate a response that includes both a definition and Related Searches
+        List<String> inBarSuggestions = new ArrayList<String>();
+        inBarSuggestions.add("Related Suggestion 1");
+        inBarSuggestions.add("Related Suggestion 2");
+        TestThreadUtils.runOnUiThreadBlocking(
+                ()
+                        -> mPanel.onSearchTermResolved("obscure · əbˈskyo͝or", null, null,
+                                QuickActionCategory.NONE, ResolvedSearchTerm.CardTag.CT_DEFINITION,
+                                inBarSuggestions, false /* showDefaultSearchInBar */,
+                                null /* relatedSearchesInContent */,
+                                false /* showDefaultSearchInContent */));
+        boolean didPanelGetTaller = mPanel.getHeight() > normalHeight;
+        Assert.assertTrue(
+                "Related Searches should show in a taller Bar when there's a definition card, "
+                        + "but they did not!",
+                didPanelGetTaller);
+        // Clean up
+        closePanel();
+        CompositorAnimationHandler.setTestingMode(false);
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    @DisabledTest(message = "https://crbug.com/1251774")
+    public void testRelatedSearchesDismissDuringAnimation() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_RELATED_SEARCHES_IN_BAR);
+        mFakeServer.reset();
+        // Use the "intelligence" node to generate Related Searches suggestions.
+        simulateResolveSearch("intelligence");
+
+        // Wait for the animation to start growing the Bar.
+        CriteriaHelper.pollUiThread(() -> {
+            Criteria.checkThat(
+                    mPanel.getInBarRelatedSearchesAnimatedHeightDps(), Matchers.greaterThan(0f));
+        });
+
+        // Wait for the animation to change to make sure that doesn't bring the Bar back
+        final boolean[] didAnimationChange = {false};
+        mPanel.getSearchBarControl().setInBarAnimationTestNotifier(
+                () -> { didAnimationChange[0] = true; });
+        CriteriaHelper.pollUiThread(
+                () -> { Criteria.checkThat(didAnimationChange[0], Matchers.is(true)); });
+        // Repeatedly closing the panel should not bring it back even during ongoing animation.
+        closePanel();
+        Assert.assertFalse("The panel is showing again due to Animation!", mPanel.isShowing());
+        // Another scroll might try to close the panel when it thinks it's already closed, which
+        // could fail due to inconsistencies in internal logic, so test that too.
+        closePanel();
+        Assert.assertFalse("Expected the panel to not be showing after a close! "
+                        + "Animation of the Bar height is the likely cause.",
+                mPanel.isShowing());
+    }
+}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchTriggerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchTriggerTest.java
new file mode 100644
index 0000000..260f6c33
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchTriggerTest.java
@@ -0,0 +1,430 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// 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.contextualsearch;
+
+import static org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE;
+
+import android.os.Build;
+import android.text.TextUtils;
+import android.view.ViewConfiguration;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.FeatureList;
+import org.chromium.base.test.params.ParameterAnnotations;
+import org.chromium.base.test.params.ParameterizedRunner;
+import org.chromium.base.test.util.Batch;
+import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.base.test.util.DisableIf;
+import org.chromium.base.test.util.DisabledTest;
+import org.chromium.base.test.util.Feature;
+import org.chromium.base.test.util.Restriction;
+import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel;
+import org.chromium.chrome.browser.flags.ChromeSwitches;
+import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
+import org.chromium.content_public.browser.SelectionClient;
+import org.chromium.content_public.browser.test.util.TestThreadUtils;
+import org.chromium.ui.test.util.UiRestriction;
+
+/**
+ * Tests the Related Searches Feature of Contextual Search using instrumentation tests.
+ */
+@RunWith(ParameterizedRunner.class)
+@ParameterAnnotations.UseRunnerDelegate(ChromeJUnit4RunnerDelegate.class)
+// NOTE: Disable online detection so we we'll default to online on test bots with no network.
+@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
+        ContextualSearchFieldTrial.ONLINE_DETECTION_DISABLED})
+@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
+@Batch(Batch.PER_CLASS)
+public class ContextualSearchTriggerTest extends ContextualSearchInstrumentationBase {
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        mTestPage = "/chrome/test/data/android/contextualsearch/tap_test.html";
+        super.setUp();
+    }
+
+    //============================================================================================
+    // Test Cases
+    //============================================================================================
+
+    /**
+     * Tests the doesContainAWord method.
+     * TODO(donnd): Change to a unit test.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    public void testDoesContainAWord() {
+        Assert.assertTrue(mSelectionController.doesContainAWord("word"));
+        Assert.assertTrue(mSelectionController.doesContainAWord("word "));
+        Assert.assertFalse("Emtpy string should not be considered a word!",
+                mSelectionController.doesContainAWord(""));
+        Assert.assertFalse("Special symbols should not be considered a word!",
+                mSelectionController.doesContainAWord("@"));
+        Assert.assertFalse("White space should not be considered a word",
+                mSelectionController.doesContainAWord(" "));
+        Assert.assertTrue(mSelectionController.doesContainAWord("Q2"));
+        Assert.assertTrue(mSelectionController.doesContainAWord("123"));
+    }
+
+    /**
+     * Tests the isValidSelection method.
+     * TODO(donnd): Change to a unit test.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    public void testIsValidSelection() {
+        StubbedSelectionPopupController c = new StubbedSelectionPopupController();
+        Assert.assertTrue(mSelectionController.isValidSelection("valid", c));
+        Assert.assertFalse(mSelectionController.isValidSelection(" ", c));
+        c.setIsFocusedNodeEditableForTest(true);
+        Assert.assertFalse(mSelectionController.isValidSelection("editable", c));
+        c.setIsFocusedNodeEditableForTest(false);
+        String numberString = "0123456789";
+        Assert.assertTrue(mSelectionController.isValidSelection(numberString, c));
+        StringBuilder longStringBuilder = new StringBuilder().append(numberString);
+        for (int i = 0; i < 10; i++) {
+            longStringBuilder.append(longStringBuilder.toString());
+            if (longStringBuilder.toString().length() < 1000) {
+                Assert.assertTrue(
+                        mSelectionController.isValidSelection(longStringBuilder.toString(), c));
+            } else {
+                Assert.assertFalse(
+                        mSelectionController.isValidSelection(longStringBuilder.toString(), c));
+                break;
+            }
+        }
+    }
+
+    /**
+     * Tests a simple non-resolving gesture, without opening the panel.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    @ParameterAnnotations.UseMethodParameter(ContextualSearchManagerTest.FeatureParamProvider.class)
+    public void testNonResolveTrigger(@EnabledFeature int enabledFeature) throws Exception {
+        if (isConfigurationForResolvingGesturesOnly()) return;
+        triggerNonResolve("states");
+
+        Assert.assertNull(mFakeServer.getSearchTermRequested());
+        waitForPanelToPeek();
+        assertLoadedNoUrl();
+        assertNoWebContents();
+    }
+
+    //============================================================================================
+    // Tap=gesture Tests
+    //============================================================================================
+
+    /**
+     * Tests that a Tap gesture on a special character does not select or show the panel.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    // Previously flaky and disabled 4/2021.  https://crbug.com/1180304
+    public void testTapGestureOnSpecialCharacterDoesntSelect() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_NONE);
+
+        clickNode("question-mark");
+        Assert.assertNull(getSelectedText());
+        assertPanelClosedOrUndefined();
+        assertLoadedNoUrl();
+    }
+
+    /**
+     * Tests that a Tap gesture followed by scrolling clears the selection.
+     */
+    @Test
+    @DisableIf.
+    Build(sdk_is_greater_than = Build.VERSION_CODES.LOLLIPOP, message = "crbug.com/841017")
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    public void testTapGestureFollowedByScrollClearsSelection() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_NONE);
+
+        clickWordNode("intelligence");
+        fakeResponse(false, 200, "Intelligence", "Intelligence", "alternate-term", false);
+        assertContainsParameters("Intelligence", "alternate-term");
+        waitForPanelToPeek();
+        assertLoadedLowPriorityUrl();
+        scrollBasePage();
+        assertPanelClosedOrUndefined();
+        Assert.assertTrue(TextUtils.isEmpty(mSelectionController.getSelectedText()));
+    }
+
+    /**
+     * Tests that a Tap gesture followed by tapping an invalid character doesn't select.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    // Previously flaky and disabled 4/2021.  https://crbug.com/1192285
+    public void testTapGestureFollowedByInvalidTextTapCloses() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_NONE);
+
+        clickWordNode("states-far");
+        waitForPanelToPeek();
+        clickNode("question-mark");
+        waitForPanelToClose();
+        Assert.assertNull(mSelectionController.getSelectedText());
+    }
+
+    /**
+     * Tests that a Tap gesture followed by tapping a non-text character doesn't select.
+     * @SmallTest
+     * @Feature({"ContextualSearch"})
+     * crbug.com/665633
+     */
+    @Test
+    @DisabledTest
+    public void testTapGestureFollowedByNonTextTap() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_NONE);
+
+        clickWordNode("states-far");
+        waitForPanelToPeek();
+        clickNode("button");
+        waitForPanelToCloseAndSelectionEmpty();
+    }
+
+    /**
+     * Tests that a Tap gesture far away toggles selecting text.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    public void testTapGestureFarAwayTogglesSelecting() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_NONE);
+
+        clickWordNode("states");
+        Assert.assertEquals("States", getSelectedText());
+        waitForPanelToPeek();
+        clickNode("states-far");
+        waitForPanelToClose();
+        Assert.assertNull(getSelectedText());
+        clickNode("states-far");
+        waitForPanelToPeek();
+        Assert.assertEquals("States", getSelectedText());
+    }
+
+    /**
+     * Tests a "retap" -- that sequential Tap gestures nearby keep selecting.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    @DisabledTest(message = "https://crbug.com/1075895")
+    public void testTapGesturesNearbyKeepSelecting() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_NONE);
+
+        clickWordNode("states");
+        Assert.assertEquals("States", getSelectedText());
+        waitForPanelToPeek();
+        assertLoggedAllExpectedFeaturesToRanker();
+        // Avoid issues with double-tap detection by ensuring sequential taps
+        // aren't treated as such. Double-tapping can also select words much as
+        // longpress, in turn showing the pins and preventing contextual tap
+        // refinement from nearby taps. The double-tap timeout is sufficiently
+        // short that this shouldn't conflict with tap refinement by the user.
+        Thread.sleep(ViewConfiguration.getDoubleTapTimeout());
+        // Because sequential taps never hide the bar, we we can't wait for it to peek.
+        // Instead we use clickNode (which doesn't wait) instead of clickWordNode and wait
+        // for the selection to change.
+        clickNode("states-near");
+        waitForSelectionToBe("StatesNear");
+        assertLoggedAllExpectedOutcomesToRanker();
+        assertLoggedAllExpectedFeaturesToRanker();
+        Thread.sleep(ViewConfiguration.getDoubleTapTimeout());
+        clickNode("states");
+        waitForSelectionToBe("States");
+        assertLoggedAllExpectedOutcomesToRanker();
+    }
+
+    //============================================================================================
+    // Long-press non-triggering gesture tests.
+    //============================================================================================
+
+    /**
+     * Tests that a long-press gesture followed by scrolling does not clear the selection.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    @ParameterAnnotations.UseMethodParameter(ContextualSearchManagerTest.FeatureParamProvider.class)
+    @DisableIf.Build(sdk_is_greater_than = Build.VERSION_CODES.O, message = "crbug.com/1071080")
+    public void testLongPressGestureFollowedByScrollMaintainsSelection(
+            @EnabledFeature int enabledFeature) throws Exception {
+        longPressNode("intelligence");
+        waitForPanelToPeek();
+        scrollBasePage();
+        assertPanelClosedOrUndefined();
+        Assert.assertEquals("Intelligence", getSelectedText());
+        assertLoadedNoUrl();
+    }
+
+    /**
+     * Tests that a long-press gesture followed by a tap does not select.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
+    @DisabledTest(message = "See https://crbug.com/837998")
+    public void testLongPressGestureFollowedByTapDoesntSelect() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_NONE);
+
+        longPressNode("intelligence");
+        waitForPanelToPeek();
+        clickWordNode("states-far");
+        waitForGestureToClosePanelAndAssertNoSelection();
+        assertLoadedNoUrl();
+    }
+
+    //============================================================================================
+    // Tap-non-triggering when ARIA annotated as interactive.
+    //============================================================================================
+
+    /**
+     * Tests that a Tap gesture on an element with an ARIA role does not trigger.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    @DisabledTest(message = "http://crbug.com/1296677")
+    public void testTapOnRoleIgnored() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_NONE);
+
+        @OverlayPanel.PanelState
+        int initialState = mPanel.getPanelState();
+        clickNode("role");
+        assertPanelStillInState(initialState);
+    }
+
+    /**
+     * Tests that a Tap gesture on an element with an ARIA attribute does not trigger.
+     * http://crbug.com/542874
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    // Previously flaky and disabled 4/2021.  https://crbug.com/1192285
+    @DisabledTest(message = "https://crbug.com/1291558")
+    public void testTapOnARIAIgnored() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_NONE);
+
+        @OverlayPanel.PanelState
+        int initialState = mPanel.getPanelState();
+        clickNode("aria");
+        assertPanelStillInState(initialState);
+    }
+
+    /**
+     * Tests that a Tap gesture on an element that is focusable does not trigger.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    public void testTapOnFocusableIgnored() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_NONE);
+
+        @OverlayPanel.PanelState
+        int initialState = mPanel.getPanelState();
+        clickNode("focusable");
+        assertPanelStillInState(initialState);
+    }
+
+    //============================================================================================
+    // Search-term resolution (server request to determine a search).
+    //============================================================================================
+
+    /**
+     * Tests expanding the panel before the search term has resolved, verifies that nothing
+     * loads until the resolve completes and that it's now a normal priority URL.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    @ParameterAnnotations.UseMethodParameter(ContextualSearchManagerTest.FeatureParamProvider.class)
+    public void testExpandBeforeSearchTermResolution(@EnabledFeature int enabledFeature)
+            throws Exception {
+        simulateSlowResolveSearch("states");
+        assertNoWebContents();
+
+        // Expanding before the search term resolves should not load anything.
+        tapPeekingBarToExpandAndAssert();
+        assertLoadedNoUrl();
+
+        // Once the response comes in, it should load.
+        simulateSlowResolveFinished();
+        assertContainsParameters("States");
+        assertLoadedNormalPriorityUrl();
+        assertWebContentsCreated();
+        assertWebContentsVisible();
+    }
+
+    /**
+     * Tests that the Contextual Search panel does not reappear when a long-press selection is
+     * modified after the user has taken an action to explicitly dismiss the panel. Also tests
+     * that the panel reappears when a new selection is made.
+     */
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    // Previously flaky, disabled 4/2021.  https://crbug.com/1192285
+    @DisabledTest(message = "https://crbug.com/1291558")
+    public void testPreventHandlingCurrentSelectionModification() throws Exception {
+        FeatureList.setTestFeatures(ENABLE_NONE);
+
+        simulateNonResolveSearch("search");
+
+        // Dismiss the Contextual Search panel.
+        closePanel();
+        Assert.assertEquals("Search", getSelectedText());
+
+        // Simulate a selection change event and assert that the panel has not reappeared.
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            SelectionClient selectionClient = mManager.getContextualSearchSelectionClient();
+            selectionClient.onSelectionEvent(org.chromium.ui.touch_selection.SelectionEventType
+                                                     .SELECTION_HANDLE_DRAG_STARTED,
+                    333, 450);
+            selectionClient.onSelectionEvent(org.chromium.ui.touch_selection.SelectionEventType
+                                                     .SELECTION_HANDLE_DRAG_STOPPED,
+                    303, 450);
+        });
+        assertPanelClosedOrUndefined();
+
+        // Select a different word and assert that the panel has appeared.
+        simulateNonResolveSearch("resolution");
+        // The simulateNonResolveSearch call will verify that the panel peeks.
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
+    @ParameterAnnotations.UseMethodParameter(ContextualSearchManagerTest.FeatureParamProvider.class)
+    // Previously flaky and disabled 4/2021.  https://crbug.com/1180304
+    public void testSelectionExpansionOnSearchTermResolution(@EnabledFeature int enabledFeature)
+            throws Exception {
+        mFakeServer.reset();
+        triggerResolve("intelligence");
+        waitForPanelToPeek();
+
+        ResolvedSearchTerm resolvedSearchTerm =
+                new ResolvedSearchTerm
+                        .Builder(false, 200, "Intelligence", "United States Intelligence")
+                        .setSelectionStartAdjust(-14)
+                        .build();
+        fakeResponse(resolvedSearchTerm);
+        waitForSelectionToBe("United States Intelligence");
+    }
+}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 14d1bbf..a4e30d7 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -2440,6 +2440,8 @@
       "//ash/webui/shimless_rma",
       "//ash/webui/shimless_rma/mojom",
       "//ash/webui/shortcut_customization_ui",
+      "//ash/webui/system_extensions_internals_ui",
+      "//ash/webui/system_extensions_internals_ui/mojom",
       "//chrome/app/theme:chrome_unscaled_resources_grit",
       "//chrome/browser/ash/system_extensions",
       "//chrome/browser/ash/system_extensions/api/hid",
diff --git a/chrome/browser/accessibility/live_caption_controller_browsertest.cc b/chrome/browser/accessibility/live_caption_controller_browsertest.cc
index 2d166dc9..121db42 100644
--- a/chrome/browser/accessibility/live_caption_controller_browsertest.cc
+++ b/chrome/browser/accessibility/live_caption_controller_browsertest.cc
@@ -83,14 +83,20 @@
   void SetLiveCaptionEnabled(bool enabled) {
     browser()->profile()->GetPrefs()->SetBoolean(prefs::kLiveCaptionEnabled,
                                                  enabled);
-    if (enabled)
+    if (enabled) {
+      speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting(
+          en_us());
       speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
+    }
   }
 
   void SetLiveCaptionEnabledOnProfile(bool enabled, Profile* profile) {
     profile->GetPrefs()->SetBoolean(prefs::kLiveCaptionEnabled, enabled);
-    if (enabled)
+    if (enabled) {
+      speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting(
+          en_us());
       speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
+    }
   }
 
   LiveCaptionController* GetController() {
@@ -181,6 +187,8 @@
 #endif
   }
 
+  speech::LanguageCode en_us() { return speech::LanguageCode::kEnUs; }
+
  private:
   base::test::ScopedFeatureList scoped_feature_list_;
   std::unique_ptr<CaptionBubbleContextBrowser> caption_bubble_context_;
@@ -263,6 +271,7 @@
   EXPECT_FALSE(HasBubbleController());
 
   // The UI is only created after SODA is installed.
+  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting(en_us());
   speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
   EXPECT_TRUE(HasBubbleController());
 }
diff --git a/chrome/browser/accessibility/live_caption_speech_recognition_host_browsertest.cc b/chrome/browser/accessibility/live_caption_speech_recognition_host_browsertest.cc
index 5ea48bc..d4f977f 100644
--- a/chrome/browser/accessibility/live_caption_speech_recognition_host_browsertest.cc
+++ b/chrome/browser/accessibility/live_caption_speech_recognition_host_browsertest.cc
@@ -125,8 +125,11 @@
   void SetLiveCaptionEnabled(bool enabled) {
     browser()->profile()->GetPrefs()->SetBoolean(prefs::kLiveCaptionEnabled,
                                                  enabled);
-    if (enabled)
+    if (enabled) {
+      speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting(
+          speech::LanguageCode::kEnUs);
       speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
+    }
   }
 
   bool HasBubbleController() {
diff --git a/chrome/browser/accessibility/soda_installer_impl.cc b/chrome/browser/accessibility/soda_installer_impl.cc
index aa9a2868..e663d94 100644
--- a/chrome/browser/accessibility/soda_installer_impl.cc
+++ b/chrome/browser/accessibility/soda_installer_impl.cc
@@ -10,6 +10,7 @@
 
 #include "base/bind.h"
 #include "base/check_op.h"
+#include "base/containers/contains.h"
 #include "base/containers/flat_set.h"
 #include "base/cxx17_backports.h"
 #include "base/feature_list.h"
@@ -25,32 +26,6 @@
 #include "media/base/media_switches.h"
 #include "ui/base/l10n/l10n_util.h"
 
-namespace {
-
-int GetDownloadProgress(
-    const std::map<std::string, update_client::CrxUpdateItem>&
-        downloading_components) {
-  int total_bytes = 0;
-  int downloaded_bytes = 0;
-
-  for (auto component : downloading_components) {
-    if (component.second.downloaded_bytes >= 0 &&
-        component.second.total_bytes > 0) {
-      downloaded_bytes += component.second.downloaded_bytes;
-      total_bytes += component.second.total_bytes;
-    }
-  }
-
-  if (total_bytes == 0)
-    return -1;
-
-  DCHECK_LE(downloaded_bytes, total_bytes);
-  return 100 * base::clamp(static_cast<double>(downloaded_bytes) / total_bytes,
-                           0.0, 1.0);
-}
-
-}  // namespace
-
 namespace speech {
 
 SodaInstallerImpl::SodaInstallerImpl() = default;
@@ -151,33 +126,22 @@
     case Events::COMPONENT_UPDATE_UPDATING: {
       update_client::CrxUpdateItem item;
       g_browser_process->component_updater()->GetComponentDetails(id, &item);
-      downloading_components_[id] = item;
-      const int combined_progress =
-          GetDownloadProgress(downloading_components_);
+      downloading_components_[language_code] = item;
 
-      // When GetDownloadProgress returns -1, do nothing. It returns -1 when the
-      // downloaded or total bytes is unknown.
-      if (combined_progress != -1) {
-        NotifyOnSodaProgress(combined_progress);
-      }
-
-      if (language_code != LanguageCode::kNone) {
-        const int language_progress = GetDownloadProgress(
-            std::map<std::string, update_client::CrxUpdateItem>{{id, item}});
-        if (language_progress != -1) {
-          language_pack_progress_[language_code] = language_progress;
-          NotifyOnSodaLanguagePackProgress(language_progress, language_code);
+      if (language_code == LanguageCode::kNone &&
+          !language_pack_progress_.empty()) {
+        for (auto language : language_pack_progress_) {
+          UpdateAndNotifyOnSodaProgress(language.first);
         }
+      } else {
+        UpdateAndNotifyOnSodaProgress(language_code);
       }
-
     } break;
     case Events::COMPONENT_UPDATE_ERROR:
       is_soda_downloading_ = false;
 
       if (language_code != LanguageCode::kNone) {
         language_pack_progress_.erase(language_code);
-        NotifyOnSodaLanguagePackError(language_code);
-
         base::UmaHistogramTimes(
             GetInstallationFailureTimeMetricForLanguagePack(language_code),
             base::Time::Now() -
@@ -185,7 +149,6 @@
 
         base::UmaHistogramBoolean(
             GetInstallationResultMetricForLanguagePack(language_code), false);
-
       } else {
         base::UmaHistogramTimes(
             kSodaBinaryInstallationFailureTimeTaken,
@@ -194,7 +157,7 @@
         base::UmaHistogramBoolean(kSodaBinaryInstallationResult, false);
       }
 
-      NotifyOnSodaError();
+      NotifyOnSodaError(language_code);
       break;
     case Events::COMPONENT_CHECKING_FOR_UPDATES:
     case Events::COMPONENT_UPDATED:
@@ -207,8 +170,8 @@
 void SodaInstallerImpl::OnSodaBinaryInstalled() {
   soda_binary_installed_ = true;
   is_soda_downloading_ = false;
-  if (IsAnyLanguagePackInstalled()) {
-    NotifyOnSodaInstalled();
+  for (LanguageCode language : installed_languages_) {
+    NotifyOnSodaInstalled(language);
   }
 
   base::UmaHistogramTimes(kSodaBinaryInstallationSuccessTimeTaken,
@@ -220,10 +183,9 @@
     speech::LanguageCode language_code) {
   installed_languages_.insert(language_code);
   language_pack_progress_.erase(language_code);
-  NotifyOnSodaLanguagePackInstalled(language_code);
 
   if (soda_binary_installed_) {
-    NotifyOnSodaInstalled();
+    NotifyOnSodaInstalled(language_code);
   }
 
   base::UmaHistogramTimes(
@@ -233,4 +195,32 @@
       GetInstallationResultMetricForLanguagePack(language_code), true);
 }
 
+void SodaInstallerImpl::UpdateAndNotifyOnSodaProgress(
+    speech::LanguageCode language_code) {
+  int total_bytes = 0;
+  int downloaded_bytes = 0;
+  speech::LanguageCode soda_code = speech::LanguageCode::kNone;
+
+  if (base::Contains(downloading_components_, soda_code)) {
+    total_bytes += downloading_components_[soda_code].total_bytes;
+    downloaded_bytes += downloading_components_[soda_code].downloaded_bytes;
+  }
+
+  if (language_code != soda_code) {
+    total_bytes += downloading_components_[language_code].total_bytes;
+    downloaded_bytes += downloading_components_[language_code].downloaded_bytes;
+  }
+
+  if (total_bytes == 0)
+    return;
+
+  DCHECK_LE(downloaded_bytes, total_bytes);
+  int progress =
+      100 * base::clamp(static_cast<double>(downloaded_bytes) / total_bytes,
+                        0.0, 1.0);
+  if (language_code != soda_code)
+    language_pack_progress_[language_code] = progress;
+  NotifyOnSodaProgress(language_code, progress);
+}
+
 }  // namespace speech
diff --git a/chrome/browser/accessibility/soda_installer_impl.h b/chrome/browser/accessibility/soda_installer_impl.h
index 8e694e7c..fc63790f 100644
--- a/chrome/browser/accessibility/soda_installer_impl.h
+++ b/chrome/browser/accessibility/soda_installer_impl.h
@@ -57,7 +57,10 @@
   void OnSodaLanguagePackInstalled(speech::LanguageCode language_code);
 
  private:
-  std::map<std::string, update_client::CrxUpdateItem> downloading_components_;
+  void UpdateAndNotifyOnSodaProgress(speech::LanguageCode language_code);
+
+  std::map<speech::LanguageCode, update_client::CrxUpdateItem>
+      downloading_components_;
 
   base::Time soda_binary_install_start_time_;
   base::flat_map<LanguageCode, base::Time> language_pack_install_start_time_;
diff --git a/chrome/browser/apps/app_service/notifications_browsertest.cc b/chrome/browser/apps/app_service/notifications_browsertest.cc
index de15794b..7409982a 100644
--- a/chrome/browser/apps/app_service/notifications_browsertest.cc
+++ b/chrome/browser/apps/app_service/notifications_browsertest.cc
@@ -94,7 +94,7 @@
 }
 
 absl::optional<bool> HasBadge(Profile* profile, const std::string& app_id) {
-  auto mojom_has_badge = OptionalBool::kUnknown;
+  absl::optional<bool> mojom_has_badge;
   auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
   proxy->FlushMojoCallsForTesting();
   proxy->AppRegistryCache().ForOneApp(
@@ -105,13 +105,13 @@
   absl::optional<bool> has_badge;
   proxy->AppRegistryCache().ForApp(app_id,
                                    [&has_badge](const apps::AppUpdate& update) {
-                                     has_badge = update.GetHasBadge();
+                                     has_badge = update.HasBadge();
                                    });
 
-  if (has_badge.has_value()) {
-    if (has_badge.value() && mojom_has_badge == OptionalBool::kTrue)
+  if (has_badge.has_value() && has_badge == mojom_has_badge) {
+    if (has_badge.value())
       return true;
-    if (!has_badge.value() && mojom_has_badge == OptionalBool::kFalse)
+    if (!has_badge.value())
       return false;
   }
   return absl::nullopt;
diff --git a/chrome/browser/apps/app_service/publishers/arc_apps.cc b/chrome/browser/apps/app_service/publishers/arc_apps.cc
index 1b97dbe..3c97ece 100644
--- a/chrome/browser/apps/app_service/publishers/arc_apps.cc
+++ b/chrome/browser/apps/app_service/publishers/arc_apps.cc
@@ -395,7 +395,7 @@
 }
 
 // Constructs an OpenUrlsRequest to be passed to
-// FileSystemInstance.OpenUrlsWithPermission.
+// FileSystemInstance.DEPRECATED_OpenUrlsWithPermission.
 arc::mojom::OpenUrlsRequestPtr ConstructOpenUrlsRequest(
     const apps::mojom::IntentPtr& intent,
     const arc::mojom::ActivityNamePtr& activity,
@@ -449,12 +449,12 @@
   } else {
     arc_file_system = ARC_GET_INSTANCE_FOR_METHOD(
         arc_service_manager->arc_bridge_service()->file_system(),
-        OpenUrlsWithPermission);
+        DEPRECATED_OpenUrlsWithPermission);
     if (!arc_file_system) {
       return;
     }
 
-    arc_file_system->OpenUrlsWithPermission(
+    arc_file_system->DEPRECATED_OpenUrlsWithPermission(
         ConstructOpenUrlsRequest(intent, activity, content_urls),
         base::DoNothing());
   }
diff --git a/chrome/browser/apps/app_service/webapk/webapk_install_task.cc b/chrome/browser/apps/app_service/webapk/webapk_install_task.cc
index 4330115..ee35f1c 100644
--- a/chrome/browser/apps/app_service/webapk/webapk_install_task.cc
+++ b/chrome/browser/apps/app_service/webapk/webapk_install_task.cc
@@ -462,19 +462,13 @@
     std::unique_ptr<std::string> response_body) {
   timer_.Stop();
 
-  int response_or_error_code = -1;
+  int response_code = -1;
   if (url_loader_->ResponseInfo() && url_loader_->ResponseInfo()->headers) {
-    response_or_error_code =
-        url_loader_->ResponseInfo()->headers->response_code();
-  } else {
-    response_or_error_code = url_loader_->NetError();
+    response_code = url_loader_->ResponseInfo()->headers->response_code();
   }
-  base::UmaHistogramSparse(kWebApkMinterErrorCodeHistogram,
-                           response_or_error_code);
 
-  if (!response_body || response_or_error_code != net::HTTP_OK) {
-    LOG(WARNING) << "WebAPK server request returned error "
-                 << response_or_error_code;
+  if (!response_body || response_code != net::HTTP_OK) {
+    LOG(WARNING) << "WebAPK server returned response code " << response_code;
     DeliverResult(WebApkInstallStatus::kNetworkError);
     return;
   }
diff --git a/chrome/browser/apps/app_service/webapk/webapk_install_task_unittest.cc b/chrome/browser/apps/app_service/webapk/webapk_install_task_unittest.cc
index 4cd28c0..fba00c95 100644
--- a/chrome/browser/apps/app_service/webapk/webapk_install_task_unittest.cc
+++ b/chrome/browser/apps/app_service/webapk/webapk_install_task_unittest.cc
@@ -223,8 +223,6 @@
                                apps::WebApkInstallStatus::kSuccess, 1);
   histograms.ExpectBucketCount(apps::kWebApkArcInstallResultHistogram,
                                arc::mojom::WebApkInstallResult::kSuccess, 1);
-  histograms.ExpectBucketCount(apps::kWebApkMinterErrorCodeHistogram,
-                               net::HTTP_OK, 1);
 }
 
 TEST_F(WebApkInstallTaskTest, ShareTarget) {
@@ -291,8 +289,6 @@
   ASSERT_EQ(apps::webapk_prefs::GetWebApkAppIds(profile()).size(), 0u);
   histograms.ExpectBucketCount(apps::kWebApkInstallResultHistogram,
                                apps::WebApkInstallStatus::kNetworkError, 1);
-  histograms.ExpectBucketCount(apps::kWebApkMinterErrorCodeHistogram,
-                               net::HTTP_BAD_REQUEST, 1);
 }
 
 TEST_F(WebApkInstallTaskTest, FailedArcInstall) {
diff --git a/chrome/browser/apps/app_service/webapk/webapk_manager.cc b/chrome/browser/apps/app_service/webapk/webapk_manager.cc
index 63d121c..ffbcfc5 100644
--- a/chrome/browser/apps/app_service/webapk/webapk_manager.cc
+++ b/chrome/browser/apps/app_service/webapk/webapk_manager.cc
@@ -203,7 +203,6 @@
   // If an installed WebAPK is not listed in WebAPK prefs, then we will generate
   // and install a new WebAPK automatically, possibly resulting in duplicate
   // apps visible to the user.
-  int uninstall_count = 0;
   std::vector<std::string> installed_packages =
       app_list_prefs_->GetPackagesFromPrefs();
   base::flat_set<std::string> installed_webapk_packages =
@@ -211,7 +210,6 @@
   for (const auto& package_name : installed_packages) {
     if (base::StartsWith(package_name, kGeneratedWebApkPackagePrefix) &&
         !installed_webapk_packages.contains(package_name)) {
-      uninstall_count++;
       auto* instance = ARC_GET_INSTANCE_FOR_METHOD(
           app_list_prefs_->app_connection_holder(), UninstallPackage);
       if (!instance) {
@@ -220,14 +218,6 @@
       instance->UninstallPackage(package_name);
     }
   }
-
-  if (uninstall_count > 0) {
-    // Record the number of instances of this issue so we can determine whether
-    // further investigation/prevention is warranted.
-    base::UmaHistogramCustomCounts("ChromeOS.WebAPK.UnlinkedWebAPKCount",
-                                   uninstall_count, /*min=*/1, /*max=*/20,
-                                   /*buckets=*/10);
-  }
 }
 
 void WebApkManager::OnPackageRemoved(const std::string& package_name,
@@ -248,26 +238,7 @@
 
   // TODO(crbug.com/1200199): Remove the web app as well, if it is still
   // installed and eligible, and WebAPKs are not disabled by policy.
-  absl::optional<std::string> app_id =
-      webapk_prefs::RemoveWebApkByPackageName(profile_, package_name);
-
-  if (!uninstalled || !app_id.has_value()) {
-    return;
-  }
-
-  bool is_installed_and_eligible = false;
-  proxy_->AppRegistryCache().ForOneApp(
-      app_id.value(), [&](const AppUpdate& update) {
-        is_installed_and_eligible = IsAppEligibleForWebApk(update);
-      });
-
-  // Record a metric so we can determine how often WebAPKs are uninstalled from
-  // Android settings.
-  WebApkUninstallSource uninstall_source = is_installed_and_eligible
-                                               ? WebApkUninstallSource::kArc
-                                               : WebApkUninstallSource::kAsh;
-  base::UmaHistogramEnumeration(kWebApkUninstallSourceHistogram,
-                                uninstall_source);
+  webapk_prefs::RemoveWebApkByPackageName(profile_, package_name);
 }
 
 void WebApkManager::OnArcPlayStoreEnabledChanged(bool enabled) {
diff --git a/chrome/browser/apps/app_service/webapk/webapk_manager_unittest.cc b/chrome/browser/apps/app_service/webapk/webapk_manager_unittest.cc
index 3d5d322..80fc4dd 100644
--- a/chrome/browser/apps/app_service/webapk/webapk_manager_unittest.cc
+++ b/chrome/browser/apps/app_service/webapk/webapk_manager_unittest.cc
@@ -213,15 +213,12 @@
   apps::webapk_prefs::AddWebApk(profile(), app_id, kTestWebApkPackageName);
   StartWebApkManager();
   arc_test()->app_instance()->SendRefreshPackageList({});
-  base::HistogramTester histograms;
 
   app_service_proxy()->UninstallSilently(
       app_id, apps::mojom::UninstallSource::kUnknown);
   app_service_test()->FlushMojoCalls();
 
   ASSERT_FALSE(apps::webapk_prefs::GetWebApkPackageName(profile(), app_id));
-  histograms.ExpectBucketCount(apps::kWebApkUninstallSourceHistogram,
-                               apps::WebApkUninstallSource::kAsh, 1);
 }
 
 TEST_F(WebApkManagerTest, QueuesUpdatedApp) {
@@ -327,12 +324,10 @@
                                 "org.chromium.webapk.package1");
   StartWebApkManager();
 
-  base::HistogramTester histograms;
   arc_test()->app_instance()->SendRefreshPackageList(std::move(packages));
 
   ASSERT_TRUE(ArcAppListPrefs::Get(profile())->GetPackage(
       "org.chromium.webapk.package1"));
   ASSERT_FALSE(ArcAppListPrefs::Get(profile())->GetPackage(
       "org.chromium.webapk.package2"));
-  histograms.ExpectUniqueSample("ChromeOS.WebAPK.UnlinkedWebAPKCount", 1, 1);
 }
diff --git a/chrome/browser/apps/app_service/webapk/webapk_metrics.cc b/chrome/browser/apps/app_service/webapk/webapk_metrics.cc
index 59be6da..39e6ef7 100644
--- a/chrome/browser/apps/app_service/webapk/webapk_metrics.cc
+++ b/chrome/browser/apps/app_service/webapk/webapk_metrics.cc
@@ -15,10 +15,6 @@
     "ChromeOS.WebAPK.Install.ArcInstallResult";
 const char kWebApkArcUpdateResultHistogram[] =
     "ChromeOS.WebAPK.Update.ArcInstallResult";
-const char kWebApkMinterErrorCodeHistogram[] =
-    "ChromeOS.WebAPK.MinterResponseOrErrorCode";
-const char kWebApkUninstallSourceHistogram[] =
-    "ChromeOS.WebApk.UninstallSource";
 
 void RecordWebApkInstallResult(bool is_update, WebApkInstallStatus result) {
   const char* histogram =
diff --git a/chrome/browser/apps/app_service/webapk/webapk_metrics.h b/chrome/browser/apps/app_service/webapk/webapk_metrics.h
index 2d09b33..c6d4c85 100644
--- a/chrome/browser/apps/app_service/webapk/webapk_metrics.h
+++ b/chrome/browser/apps/app_service/webapk/webapk_metrics.h
@@ -49,8 +49,6 @@
 extern const char kWebApkUpdateResultHistogram[];
 extern const char kWebApkArcInstallResultHistogram[];
 extern const char kWebApkArcUpdateResultHistogram[];
-extern const char kWebApkMinterErrorCodeHistogram[];
-extern const char kWebApkUninstallSourceHistogram[];
 
 // Records the overall result of installing/updating a WebAPK to UMA.
 void RecordWebApkInstallResult(bool is_update, WebApkInstallStatus result);
diff --git a/chrome/browser/ash/accessibility/accessibility_manager.cc b/chrome/browser/ash/accessibility/accessibility_manager.cc
index 573f9a8d85..56e8fd2a 100644
--- a/chrome/browser/ash/accessibility/accessibility_manager.cc
+++ b/chrome/browser/ash/accessibility/accessibility_manager.cc
@@ -946,7 +946,7 @@
     // nudge hasn't yet been shown to the user.
     if (!offline_nudge || !offline_nudge.value()) {
       if (speech::SodaInstaller::GetInstance()->IsSodaInstalled(
-              speech::GetLanguageCode(dictation_locale))) {
+              GetDictationLanguageCode())) {
         // The locale is already installed on device, show the nudge
         // immediately.
         ShowDictationLanguageUpgradedNudge(dictation_locale);
@@ -2027,16 +2027,11 @@
   if (!::features::IsDictationOfflineAvailable())
     return true;
 
+  // Show the dialog for languages not supported by SODA.
   speech::SodaInstaller* soda_installer = speech::SodaInstaller::GetInstance();
   std::vector<std::string> supported_languages =
       soda_installer->GetAvailableLanguages();
-  if (std::find(supported_languages.begin(), supported_languages.end(),
-                locale) == supported_languages.end()) {
-    // Show the dialog for languages not supported by SODA.
-    return true;
-  }
-
-  return false;
+  return !base::Contains(supported_languages, locale);
 }
 
 void AccessibilityManager::ShowNetworkDictationDialog() {
@@ -2098,19 +2093,6 @@
   soda_failed_notification_shown_ = false;
 }
 
-void AccessibilityManager::OnSodaInstallSucceeded() {
-  if (ShouldShowSodaSucceededNotificationForDictation())
-    ShowSodaDownloadNotificationForDictation(true);
-  OnSodaInstallUpdated(100);
-}
-
-void AccessibilityManager::OnSodaInstallError(
-    speech::LanguageCode language_code) {
-  if (ShouldShowSodaFailedNotificationForDictation(language_code))
-    ShowSodaDownloadNotificationForDictation(false);
-  OnSodaInstallUpdated(0);
-}
-
 void AccessibilityManager::OnSodaInstallUpdated(int progress) {
   if (!::features::IsDictationOfflineAvailable())
     return;
@@ -2118,15 +2100,13 @@
   speech::SodaInstaller* soda_installer = speech::SodaInstaller::GetInstance();
   const std::string dictation_locale =
       profile_->GetPrefs()->GetString(prefs::kAccessibilityDictationLocale);
-  speech::LanguageCode dictation_language_code =
-      speech::GetLanguageCode(dictation_locale);
   // Update the Dictation button tray.
   // TODO(https://crbug.com/1266491): Ensure we use combined progress instead
   // of just the language pack progress.
   AccessibilityController::Get()
       ->UpdateDictationButtonOnSpeechRecognitionDownloadChanged(progress);
 
-  if (soda_installer->IsSodaDownloading(dictation_language_code))
+  if (soda_installer->IsSodaDownloading(GetDictationLanguageCode()))
     return;
 
   const absl::optional<bool> offline_nudge =
@@ -2135,41 +2115,42 @@
   // shown to the user (the key is in kAccessibilityDictationLocale but the
   // value is false).
   if (offline_nudge && !offline_nudge.value() &&
-      soda_installer->IsSodaInstalled(
-          speech::GetLanguageCode(dictation_locale))) {
+      soda_installer->IsSodaInstalled(GetDictationLanguageCode())) {
     ShowDictationLanguageUpgradedNudge(dictation_locale);
   }
 }
 
 // SodaInstaller::Observer:
-void AccessibilityManager::OnSodaInstalled() {
-  OnSodaInstallSucceeded();
-}
-
-void AccessibilityManager::OnSodaError() {
-  OnSodaInstallError(speech::LanguageCode::kNone);
-}
-
-void AccessibilityManager::OnSodaLanguagePackInstalled(
-    speech::LanguageCode language_code) {
-  OnSodaInstallSucceeded();
-}
-
-void AccessibilityManager::OnSodaLanguagePackProgress(
-    int language_progress,
-    speech::LanguageCode language_code) {
-  const std::string locale =
-      profile_->GetPrefs()->GetString(prefs::kAccessibilityDictationLocale);
-
-  if (language_code != speech::GetLanguageCode(locale))
+void AccessibilityManager::OnSodaInstalled(speech::LanguageCode language_code) {
+  if (language_code != GetDictationLanguageCode())
     return;
 
-  OnSodaInstallUpdated(language_progress);
+  if (ShouldShowSodaSucceededNotificationForDictation())
+    ShowSodaDownloadNotificationForDictation(true);
+  OnSodaInstallUpdated(100);
 }
 
-void AccessibilityManager::OnSodaLanguagePackError(
-    speech::LanguageCode language_code) {
-  OnSodaInstallError(language_code);
+void AccessibilityManager::OnSodaError(speech::LanguageCode language_code) {
+  if (language_code != speech::LanguageCode::kNone &&
+      language_code != GetDictationLanguageCode()) {
+    return;
+  }
+
+  // Show the failed message if either the Dictation locale failed or the SODA
+  // binary failed (encoded by LanguageCode::kNone).
+  if (ShouldShowSodaFailedNotificationForDictation(language_code))
+    ShowSodaDownloadNotificationForDictation(false);
+  OnSodaInstallUpdated(0);
+}
+
+void AccessibilityManager::OnSodaProgress(speech::LanguageCode language_code,
+                                          int progress) {
+  if (language_code != speech::LanguageCode::kNone &&
+      language_code != GetDictationLanguageCode()) {
+    return;
+  }
+
+  OnSodaInstallUpdated(progress);
 }
 
 bool AccessibilityManager::ShouldShowSodaSucceededNotificationForDictation() {
@@ -2182,14 +2163,8 @@
   // download, either for the SODA binary or a language pack.
   // Both the SODA binary and the language pack matching the Dictation locale
   // need to be downloaded to return true.
-  const std::string locale =
-      profile_->GetPrefs()->GetString(prefs::kAccessibilityDictationLocale);
-  if (speech::SodaInstaller::GetInstance()->IsSodaInstalled(
-          speech::GetLanguageCode(locale))) {
-    return true;
-  }
-
-  return false;
+  return speech::SodaInstaller::GetInstance()->IsSodaInstalled(
+      GetDictationLanguageCode());
 }
 
 bool AccessibilityManager::ShouldShowSodaFailedNotificationForDictation(
@@ -2207,13 +2182,8 @@
   // 1. |language_code| == kNone (encodes that this was an error for the SODA
   // binary), or
   // 2. |language_code| matches the Dictation locale.
-  const std::string locale =
-      profile_->GetPrefs()->GetString(prefs::kAccessibilityDictationLocale);
-  if (language_code == speech::LanguageCode::kNone ||
-      language_code == speech::GetLanguageCode(locale))
-    return true;
-
-  return false;
+  return language_code == speech::LanguageCode::kNone ||
+         language_code == GetDictationLanguageCode();
 }
 
 void AccessibilityManager::ShowSodaDownloadNotificationForDictation(
@@ -2236,4 +2206,10 @@
     soda_failed_notification_shown_ = true;
 }
 
+speech::LanguageCode AccessibilityManager::GetDictationLanguageCode() {
+  DCHECK(profile_);
+  return speech::GetLanguageCode(
+      profile_->GetPrefs()->GetString(prefs::kAccessibilityDictationLocale));
+}
+
 }  // namespace ash
diff --git a/chrome/browser/ash/accessibility/accessibility_manager.h b/chrome/browser/ash/accessibility/accessibility_manager.h
index 0376a59..868f6172 100644
--- a/chrome/browser/ash/accessibility/accessibility_manager.h
+++ b/chrome/browser/ash/accessibility/accessibility_manager.h
@@ -365,13 +365,10 @@
                                   double value);
 
   // SodaInstaller::Observer:
-  void OnSodaInstalled() override;
-  void OnSodaLanguagePackInstalled(speech::LanguageCode language_code) override;
-  void OnSodaError() override;
-  void OnSodaLanguagePackError(speech::LanguageCode language_code) override;
-  void OnSodaProgress(int combined_progress) override {}
-  void OnSodaLanguagePackProgress(int language_progress,
-                                  speech::LanguageCode language_code) override;
+  void OnSodaInstalled(speech::LanguageCode language_code) override;
+  void OnSodaError(speech::LanguageCode language_code) override;
+  void OnSodaProgress(speech::LanguageCode language_code,
+                      int progress) override;
 
   // Test helpers:
   void SetProfileForTest(Profile* profile);
@@ -494,8 +491,6 @@
 
   // SODA-related methods.
   void MaybeInstallSoda(const std::string& locale);
-  void OnSodaInstallSucceeded();
-  void OnSodaInstallError(speech::LanguageCode language_code);
   void OnSodaInstallUpdated(int progress);
   bool ShouldShowSodaSucceededNotificationForDictation();
   bool ShouldShowSodaFailedNotificationForDictation(
@@ -503,6 +498,7 @@
   void ShowSodaDownloadNotificationForDictation(bool succeeded);
 
   void ShowDictationLanguageUpgradedNudge(const std::string& locale);
+  speech::LanguageCode GetDictationLanguageCode();
 
   void CreateChromeVoxPanel();
 
diff --git a/chrome/browser/ash/accessibility/accessibility_manager_browsertest.cc b/chrome/browser/ash/accessibility/accessibility_manager_browsertest.cc
index b787a60..15ca45d 100644
--- a/chrome/browser/ash/accessibility/accessibility_manager_browsertest.cc
+++ b/chrome/browser/ash/accessibility/accessibility_manager_browsertest.cc
@@ -801,6 +801,7 @@
   }
 
   speech::LanguageCode en_us() { return speech::LanguageCode::kEnUs; }
+  speech::LanguageCode fr_fr() { return speech::LanguageCode::kFrFr; }
 
   const std::u16string en_us_display_name() {
     return u"English (United States)";
@@ -821,7 +822,8 @@
   // The nudge should not be requested to be shown because this was a
   // user-initiated change.
   EXPECT_FALSE(GetDictationOfflineNudgePref("en-US"));
-  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   EXPECT_FALSE(IsSodaDownloading());
   // The nudge was never shown.
   EXPECT_FALSE(GetDictationOfflineNudgePref("en-US"));
@@ -841,9 +843,7 @@
   // The nudge should be shown when SODA download finishes.
   EXPECT_FALSE(GetDictationOfflineNudgePref("en-US").value());
   speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
-  speech::SodaInstaller::GetInstance()
-      ->NotifyOnSodaLanguagePackInstalledForTesting(
-          speech::LanguageCode::kEnUs);
+  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting(en_us());
   EXPECT_FALSE(IsSodaDownloading());
   EXPECT_TRUE(GetDictationOfflineNudgePref("en-US").value());
   // No notifications were shown.
@@ -876,7 +876,8 @@
   ClearDictationOfflineNudgePref("en-US");
   EnableDictationTriggeredByUser(/*soda_uninstalled_first=*/true);
   EXPECT_TRUE(IsSodaDownloading());
-  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   EXPECT_FALSE(IsSodaDownloading());
   EXPECT_TRUE(GetDictationOfflineNudgePref("en-US").value());
   UninstallSodaForTesting();
@@ -885,7 +886,8 @@
   // The second time the same language downloads, the nudge is not shown again.
   EnableDictationTriggeredByUser(/*soda_uninstalled_first=*/false);
   EXPECT_TRUE(IsSodaDownloading());
-  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   EXPECT_FALSE(IsSodaDownloading());
   // Unchanged.
   EXPECT_TRUE(GetDictationOfflineNudgePref("en-US").value());
@@ -893,7 +895,8 @@
 
 IN_PROC_BROWSER_TEST_F(AccessibilityManagerSodaTest,
                        SodaInstalledBeforeDictationEnabled) {
-  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   ClearDictationOfflineNudgePref("en-US");
   EnableDictationTriggeredByUser(/*soda_uninstalled_first=*/false);
 
@@ -920,7 +923,8 @@
   // enabled. This mocks selecting a new locale from settings.
   SetDictationLocale("en-US");
   EXPECT_TRUE(IsSodaDownloading());
-  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   EXPECT_FALSE(IsSodaDownloading());
   // The nudge was never shown because this was a user-initiated change.
   EXPECT_FALSE(GetDictationOfflineNudgePref("en-US"));
@@ -936,11 +940,11 @@
                        SucceededNotificationCase1) {
   // For this test, pretend that the Dictation locale is fr-FR.
   g_browser_process->SetApplicationLocale("fr-FR");
-  speech::LanguageCode fr_fr = speech::LanguageCode::kFrFr;
   SetDictationEnabled(true);
   soda_installer()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   AssertMessageCenterEmpty();
-  soda_installer()->NotifyOnSodaLanguagePackInstalledForTesting(fr_fr);
+  soda_installer()->NotifySodaInstalledForTesting(fr_fr());
   AssertSodaNotificationShownForDictation(u"français (France)",
                                           /*success=*/true);
 }
@@ -950,7 +954,7 @@
 IN_PROC_BROWSER_TEST_F(AccessibilityManagerSodaTest,
                        SucceededNotificationCase2) {
   SetDictationEnabled(true);
-  soda_installer()->NotifyOnSodaLanguagePackInstalledForTesting(en_us());
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   AssertMessageCenterEmpty();
   soda_installer()->NotifySodaInstalledForTesting();
   AssertSodaNotificationShownForDictation(en_us_display_name(),
@@ -972,7 +976,7 @@
 IN_PROC_BROWSER_TEST_F(AccessibilityManagerSodaTest,
                        SodaFailedNotificationLanguageError) {
   SetDictationEnabled(true);
-  soda_installer()->NotifyOnSodaLanguagePackErrorForTesting(en_us());
+  soda_installer()->NotifySodaErrorForTesting(en_us());
   AssertSodaNotificationShownForDictation(en_us_display_name(),
                                           /*success=*/false);
 }
@@ -982,7 +986,7 @@
 IN_PROC_BROWSER_TEST_F(AccessibilityManagerSodaTest,
                        LanguageInstalledBinaryFails) {
   SetDictationEnabled(true);
-  soda_installer()->NotifyOnSodaLanguagePackInstalledForTesting(en_us());
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   AssertMessageCenterEmpty();
   soda_installer()->NotifySodaErrorForTesting();
   AssertSodaNotificationShownForDictation(en_us_display_name(),
@@ -995,11 +999,11 @@
                        BinaryInstalledLanguageFails) {
   // For this test, pretend that the Dictation locale is fr-FR.
   g_browser_process->SetApplicationLocale("fr-FR");
-  speech::LanguageCode fr_fr = speech::LanguageCode::kFrFr;
   SetDictationEnabled(true);
   soda_installer()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   AssertMessageCenterEmpty();
-  soda_installer()->NotifyOnSodaLanguagePackErrorForTesting(fr_fr);
+  soda_installer()->NotifySodaErrorForTesting(fr_fr());
   AssertSodaNotificationShownForDictation(u"français (France)",
                                           /*success=*/false);
 }
@@ -1009,13 +1013,13 @@
 IN_PROC_BROWSER_TEST_F(AccessibilityManagerSodaTest,
                        SodaFailedNotificationNotShownTwice) {
   SetDictationEnabled(true);
-  soda_installer()->NotifyOnSodaLanguagePackErrorForTesting(en_us());
+  soda_installer()->NotifySodaErrorForTesting(en_us());
   AssertSodaNotificationShownForDictation(en_us_display_name(),
                                           /*success=*/false);
   ClearMessageCenter();
 
   // No second message is shown on additional failures.
-  soda_installer()->NotifyOnSodaLanguagePackErrorForTesting(en_us());
+  soda_installer()->NotifySodaErrorForTesting(en_us());
   AssertMessageCenterEmpty();
   soda_installer()->NotifySodaErrorForTesting();
   AssertMessageCenterEmpty();
@@ -1026,7 +1030,7 @@
 IN_PROC_BROWSER_TEST_F(AccessibilityManagerSodaTest,
                        SodaFailedNotificationShownOncePerDownload) {
   SetDictationEnabled(true);
-  soda_installer()->NotifyOnSodaLanguagePackErrorForTesting(en_us());
+  soda_installer()->NotifySodaErrorForTesting(en_us());
   AssertSodaNotificationShownForDictation(en_us_display_name(),
                                           /*success=*/false);
   SetDictationEnabled(false);
@@ -1037,7 +1041,7 @@
 
   // A fresh attempt at Dictation means another chance to show an error message.
   SetDictationEnabled(true);
-  soda_installer()->NotifyOnSodaLanguagePackErrorForTesting(en_us());
+  soda_installer()->NotifySodaErrorForTesting(en_us());
   AssertSodaNotificationShownForDictation(en_us_display_name(),
                                           /*success=*/false);
 }
@@ -1047,7 +1051,7 @@
 IN_PROC_BROWSER_TEST_F(AccessibilityManagerSodaTest, NotTriggeredByUser) {
   EnableDictationTriggeredByUser(/*soda_uninstalled_first=*/false);
   soda_installer()->NotifySodaInstalledForTesting();
-  soda_installer()->NotifyOnSodaLanguagePackInstalledForTesting(en_us());
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   AssertMessageCenterEmpty();
 }
 
@@ -1059,7 +1063,7 @@
   SetDictationEnabled(true);
   soda_installer()->NotifySodaInstalledForTesting();
   AssertMessageCenterEmpty();
-  soda_installer()->NotifyOnSodaLanguagePackInstalledForTesting(en_us());
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   AssertMessageCenterEmpty();
 }
 
@@ -1074,7 +1078,7 @@
   // enabled. This mocks selecting a new locale from settings.
   SetDictationLocale("en-US");
   soda_installer()->NotifySodaInstalledForTesting();
-  soda_installer()->NotifyOnSodaLanguagePackInstalledForTesting(en_us());
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
 
   // The notification should have been shown.
   AssertSodaNotificationShownForDictation(en_us_display_name(),
@@ -1095,22 +1099,20 @@
 
   // The API will not be called if the language pack differs from the Dictation
   // locale.
-  soda_installer()->NotifyOnSodaLanguagePackProgressForTesting(
-      30, speech::LanguageCode::kFrFr);
+  soda_installer()->NotifySodaProgressForTesting(30, fr_fr());
   EXPECT_EQ(0, test_api->GetDictationSodaDownloadProgress());
   // The API will be called if the language pack matches the Dictation locale.
-  soda_installer()->NotifyOnSodaLanguagePackProgressForTesting(
-      50, speech::LanguageCode::kEnUs);
+  soda_installer()->NotifySodaProgressForTesting(50, en_us());
   EXPECT_EQ(50, test_api->GetDictationSodaDownloadProgress());
   // If SODA download fails, the API will be called with a value of 0.
   soda_installer()->NotifySodaErrorForTesting();
   EXPECT_EQ(0, test_api->GetDictationSodaDownloadProgress());
   // Reset to a non-zero value.
-  soda_installer()->NotifyOnSodaLanguagePackProgressForTesting(
-      70, speech::LanguageCode::kEnUs);
+  soda_installer()->NotifySodaProgressForTesting(70, en_us());
   EXPECT_EQ(70, test_api->GetDictationSodaDownloadProgress());
   // If SODA download succeeds, the API will be called with a value of 100.
   soda_installer()->NotifySodaInstalledForTesting();
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   EXPECT_EQ(100, test_api->GetDictationSodaDownloadProgress());
 }
 
diff --git a/chrome/browser/ash/accessibility/spoken_feedback_app_list_browsertest.cc b/chrome/browser/ash/accessibility/spoken_feedback_app_list_browsertest.cc
index 384b4f47..a3cc56f5 100644
--- a/chrome/browser/ash/accessibility/spoken_feedback_app_list_browsertest.cc
+++ b/chrome/browser/ash/accessibility/spoken_feedback_app_list_browsertest.cc
@@ -217,7 +217,7 @@
   void ReadWindowTitle() {
     extensions::browsertest_util::ExecuteScriptInBackgroundPageNoWait(
         browser()->profile(), extension_misc::kChromeVoxExtensionId,
-        "CommandHandler.onCommand('readCurrentTitle');");
+        "CommandHandlerInterface.instance.onCommand('readCurrentTitle');");
   }
 
   AppListItem* FindItemByName(const std::string& name, int* index) {
diff --git a/chrome/browser/ash/accessibility/spoken_feedback_browsertest.cc b/chrome/browser/ash/accessibility/spoken_feedback_browsertest.cc
index f6add6037..04d566e 100644
--- a/chrome/browser/ash/accessibility/spoken_feedback_browsertest.cc
+++ b/chrome/browser/ash/accessibility/spoken_feedback_browsertest.cc
@@ -138,7 +138,7 @@
   // To avoid flakes in sending keys, execute the command directly in js.
   extensions::browsertest_util::ExecuteScriptInBackgroundPageNoWait(
       browser()->profile(), extension_misc::kChromeVoxExtensionId,
-      "CommandHandler.onCommand('toggleStickyMode');");
+      "CommandHandlerInterface.instance.onCommand('toggleStickyMode');");
 }
 
 void LoggedInSpokenFeedbackTest::SendMouseMoveTo(const gfx::Point& location) {
@@ -270,7 +270,7 @@
   sm_.Call([this]() {
     extensions::browsertest_util::ExecuteScriptInBackgroundPageNoWait(
         browser()->profile(), extension_misc::kChromeVoxExtensionId,
-        "CommandHandler.onCommand('showKbExplorerPage');");
+        "CommandHandlerInterface.instance.onCommand('showKbExplorerPage');");
   });
   sm_.ExpectSpeechPattern(
       "Press a qwerty key, refreshable braille key, or touch gesture to learn "
@@ -307,7 +307,7 @@
   sm_.Call([this]() {
     extensions::browsertest_util::ExecuteScriptInBackgroundPageNoWait(
         browser()->profile(), extension_misc::kChromeVoxExtensionId,
-        "CommandHandler.onCommand('showKbExplorerPage');");
+        "CommandHandlerInterface.instance.onCommand('showKbExplorerPage');");
   });
   sm_.ExpectSpeechPattern(
       "Press a qwerty key, refreshable braille key, or touch gesture to learn "
diff --git a/chrome/browser/ash/crosapi/browser_util.cc b/chrome/browser/ash/crosapi/browser_util.cc
index d5e7c60..93e75eac 100644
--- a/chrome/browser/ash/crosapi/browser_util.cc
+++ b/chrome/browser/ash/crosapi/browser_util.cc
@@ -800,5 +800,17 @@
   return base::StringPiece();
 }
 
+bool IsAshBrowserSyncEnabled() {
+  // Turn off sync from Ash if Lacros is enabled and Ash web browser is
+  // disabled.
+  // TODO(crbug.com/1293250): We must check whether profile migration is
+  // completed or not here. Currently that is checked inside `IsLacrosEnabled()`
+  // but it is planned to be decoupled with the function in the future.
+  if (IsLacrosEnabled() && !IsAshWebBrowserEnabled())
+    return false;
+
+  return true;
+}
+
 }  // namespace browser_util
 }  // namespace crosapi
diff --git a/chrome/browser/ash/crosapi/browser_util.h b/chrome/browser/ash/crosapi/browser_util.h
index 8726d99..468e7824 100644
--- a/chrome/browser/ash/crosapi/browser_util.h
+++ b/chrome/browser/ash/crosapi/browser_util.h
@@ -292,6 +292,13 @@
 // g_browser_process->local_state() etc.
 void SetProfileMigrationCompletedForTest(bool is_completed);
 
+// Indicate whether sync on Ash should be enabled for browser data. Sync should
+// stop syncing browser items from Ash if Lacros is enabled and once browser
+// data is migrated to Lacros making it safe to turn off web browser on
+// Ash and sync for browser data. Only use after the primary user profile is set
+// on UserManager since it calls `IsLacrosEnabled()`.
+bool IsAshBrowserSyncEnabled();
+
 // Returns who decided how Lacros should be used - or not: The User, the policy
 // or another edge case.
 LacrosLaunchSwitchSource GetLacrosLaunchSwitchSource();
diff --git a/chrome/browser/ash/crosapi/browser_util_unittest.cc b/chrome/browser/ash/crosapi/browser_util_unittest.cc
index ca1e77b..2d0558b 100644
--- a/chrome/browser/ash/crosapi/browser_util_unittest.cc
+++ b/chrome/browser/ash/crosapi/browser_util_unittest.cc
@@ -687,4 +687,44 @@
                                                                 user_id_hash));
 }
 
+TEST_F(BrowserUtilTest, IsAshBrowserSyncEnabled) {
+  {
+    browser_util::SetLacrosEnabledForTest(false);
+    EXPECT_FALSE(browser_util::IsLacrosEnabled());
+    EXPECT_TRUE(browser_util::IsAshWebBrowserEnabled());
+    EXPECT_TRUE(browser_util::IsAshBrowserSyncEnabled());
+  }
+
+  {
+    browser_util::SetLacrosEnabledForTest(true);
+    EXPECT_TRUE(browser_util::IsLacrosEnabled());
+    EXPECT_TRUE(browser_util::IsAshWebBrowserEnabled());
+    EXPECT_TRUE(browser_util::IsAshBrowserSyncEnabled());
+  }
+
+  {
+    base::test::ScopedFeatureList feature_list;
+    feature_list.InitWithFeatures(
+        {chromeos::features::kLacrosOnly, chromeos::features::kLacrosPrimary,
+         chromeos::features::kLacrosSupport},
+        {});
+    browser_util::SetLacrosEnabledForTest(false);
+    EXPECT_FALSE(browser_util::IsLacrosEnabled());
+    EXPECT_TRUE(browser_util::IsAshWebBrowserEnabled());
+    EXPECT_TRUE(browser_util::IsAshBrowserSyncEnabled());
+  }
+
+  {
+    base::test::ScopedFeatureList feature_list;
+    feature_list.InitWithFeatures(
+        {chromeos::features::kLacrosOnly, chromeos::features::kLacrosPrimary,
+         chromeos::features::kLacrosSupport},
+        {});
+    browser_util::SetLacrosEnabledForTest(true);
+    EXPECT_TRUE(browser_util::IsLacrosEnabled());
+    EXPECT_FALSE(browser_util::IsAshWebBrowserEnabled());
+    EXPECT_FALSE(browser_util::IsAshBrowserSyncEnabled());
+  }
+}
+
 }  // namespace crosapi
diff --git a/chrome/browser/ash/crosapi/message_center_ash.cc b/chrome/browser/ash/crosapi/message_center_ash.cc
index 9b62700..950aaef 100644
--- a/chrome/browser/ash/crosapi/message_center_ash.cc
+++ b/chrome/browser/ash/crosapi/message_center_ash.cc
@@ -84,6 +84,7 @@
   rich_data.accessible_name = notification->accessible_name;
   rich_data.fullscreen_visibility =
       FromMojo(notification->fullscreen_visibility);
+  rich_data.accent_color = notification->accent_color;
 
   gfx::Image icon;
   if (!notification->icon.isNull())
diff --git a/chrome/browser/ash/eche_app/eche_app_manager_factory.cc b/chrome/browser/ash/eche_app/eche_app_manager_factory.cc
index 4e0e706..baf1171 100644
--- a/chrome/browser/ash/eche_app/eche_app_manager_factory.cc
+++ b/chrome/browser/ash/eche_app/eche_app_manager_factory.cc
@@ -96,7 +96,14 @@
   // TODO(nayebi): if it is null log an error? Dcheck?
   if (eche_tray) {
     eche_tray->SetUrl(url);
-    eche_tray->ShowBubble();
+    if (!features::IsEcheSWAInBackgroundEnabled()) {
+      eche_tray->ShowBubble();
+    } else {
+      eche_tray->InitBubble();
+
+      // Hide bubble first until the streaming is ready.
+      eche_tray->HideBubble();
+    }
   }
 }
 
diff --git a/chrome/browser/ash/file_manager/arc_file_tasks.cc b/chrome/browser/ash/file_manager/arc_file_tasks.cc
index 6c3657d..b8e17c4 100644
--- a/chrome/browser/ash/file_manager/arc_file_tasks.cc
+++ b/chrome/browser/ash/file_manager/arc_file_tasks.cc
@@ -95,7 +95,7 @@
 }
 
 // Constructs an OpenUrlsRequest to be passed to
-// FileSystemInstance.OpenUrlsWithPermission.
+// FileSystemInstance.DEPRECATED_OpenUrlsWithPermission.
 arc::mojom::OpenUrlsRequestPtr ConstructOpenUrlsRequest(
     const TaskDescriptor& task,
     const std::vector<GURL>& content_urls,
@@ -286,7 +286,7 @@
 
   arc::mojom::FileSystemInstance* arc_file_system = ARC_GET_INSTANCE_FOR_METHOD(
       arc_service_manager->arc_bridge_service()->file_system(),
-      OpenUrlsWithPermission);
+      DEPRECATED_OpenUrlsWithPermission);
   if (!arc_file_system) {
     std::move(done).Run(
         extensions::api::file_manager_private::TASK_RESULT_FAILED,
@@ -296,8 +296,8 @@
 
   arc::mojom::OpenUrlsRequestPtr request =
       ConstructOpenUrlsRequest(task, content_urls, mime_types);
-  arc_file_system->OpenUrlsWithPermission(std::move(request),
-                                          base::DoNothing());
+  arc_file_system->DEPRECATED_OpenUrlsWithPermission(std::move(request),
+                                                     base::DoNothing());
   // TODO(benwells): return the correct code here, depending on how the app
   // will be opened in multiprofile.
   std::move(done).Run(
diff --git a/chrome/browser/ash/file_manager/extract_io_task.cc b/chrome/browser/ash/file_manager/extract_io_task.cc
index 26987946..0eea693 100644
--- a/chrome/browser/ash/file_manager/extract_io_task.cc
+++ b/chrome/browser/ash/file_manager/extract_io_task.cc
@@ -4,6 +4,9 @@
 
 #include "chrome/browser/ash/file_manager/extract_io_task.h"
 
+#include "chrome/browser/chromeos/fileapi/file_system_backend.h"
+#include "components/services/unzip/content/unzip_service.h"
+
 namespace file_manager {
 namespace io_task {
 
@@ -11,19 +14,44 @@
     std::vector<storage::FileSystemURL> source_urls,
     storage::FileSystemURL parent_folder,
     scoped_refptr<storage::FileSystemContext> file_system_context)
-    : file_system_context_(file_system_context) {
+    : source_urls_(std::move(source_urls)),
+      parent_folder_(std::move(parent_folder)),
+      file_system_context_(std::move(file_system_context)) {
   progress_.type = OperationType::kExtract;
+  progress_.state = State::kSuccess;
 }
 
 ExtractIOTask::~ExtractIOTask() {}
 
+void ExtractIOTask::ZipExtractCallback(bool success) {
+  progress_.state = success ? State::kSuccess : State::kError;
+}
+
 void ExtractIOTask::Execute(IOTask::ProgressCallback progress_callback,
                             IOTask::CompleteCallback complete_callback) {
   progress_callback_ = std::move(progress_callback);
   complete_callback_ = std::move(complete_callback);
 
   VLOG(1) << "Executing EXTRACT_ARCHIVE IO task";
-  Complete(State::kSuccess);
+  // TODO(crbug.com/953256) Generalize for multiple files.
+  if (source_urls_.size() == 1) {
+    if (!chromeos::FileSystemBackend::CanHandleURL(source_urls_[0])) {
+      progress_.state = State::kError;
+    } else {
+      base::FilePath source_file = source_urls_[0].path();
+      if (chromeos::FileSystemBackend::CanHandleURL(parent_folder_)) {
+        base::FilePath destination_directory = parent_folder_.path();
+        unzip::Unzip(unzip::LaunchUnzipper(), source_file,
+                     destination_directory,
+                     base::BindOnce(&ExtractIOTask::ZipExtractCallback,
+                                    weak_ptr_factory_.GetWeakPtr()));
+      } else {
+        progress_.state = State::kError;
+      }
+    }
+  }
+
+  Complete();
 }
 
 void ExtractIOTask::Cancel() {
@@ -33,8 +61,7 @@
 
 // Calls the completion callback for the task. |progress_| should not be
 // accessed after calling this.
-void ExtractIOTask::Complete(State state) {
-  progress_.state = state;
+void ExtractIOTask::Complete() {
   base::SequencedTaskRunnerHandle::Get()->PostTask(
       FROM_HERE,
       base::BindOnce(std::move(complete_callback_), std::move(progress_)));
diff --git a/chrome/browser/ash/file_manager/extract_io_task.h b/chrome/browser/ash/file_manager/extract_io_task.h
index fc34943f..8d876b5 100644
--- a/chrome/browser/ash/file_manager/extract_io_task.h
+++ b/chrome/browser/ash/file_manager/extract_io_task.h
@@ -12,6 +12,7 @@
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
 #include "chrome/browser/ash/file_manager/io_task.h"
+#include "components/services/unzip/public/cpp/unzip.h"
 #include "storage/browser/file_system/file_system_context.h"
 #include "storage/browser/file_system/file_system_url.h"
 
@@ -34,7 +35,15 @@
   void Cancel() override;
 
  private:
-  void Complete(State state);
+  void Complete();
+
+  void ZipExtractCallback(bool success);
+
+  // URLs of the files that have archives in them for extraction.
+  const std::vector<storage::FileSystemURL> source_urls_;
+
+  // Parent folder of the files in 'source_urls_'.
+  const storage::FileSystemURL parent_folder_;
 
   const scoped_refptr<storage::FileSystemContext> file_system_context_;
 
diff --git a/chrome/browser/ash/note_taking_helper.cc b/chrome/browser/ash/note_taking_helper.cc
index c74a192..a105051 100644
--- a/chrome/browser/ash/note_taking_helper.cc
+++ b/chrome/browser/ash/note_taking_helper.cc
@@ -768,11 +768,11 @@
     arc::mojom::FileSystemInstance* arc_file_system =
         ARC_GET_INSTANCE_FOR_METHOD(
             arc::ArcServiceManager::Get()->arc_bridge_service()->file_system(),
-            OpenUrlsWithPermission);
+            DEPRECATED_OpenUrlsWithPermission);
     if (!arc_file_system)
       return LaunchResult::ANDROID_NOT_RUNNING;
-    arc_file_system->OpenUrlsWithPermission(std::move(request),
-                                            base::DoNothing());
+    arc_file_system->DEPRECATED_OpenUrlsWithPermission(std::move(request),
+                                                       base::DoNothing());
 
     arc::ArcMetricsService::RecordArcUserInteraction(
         profile, arc::UserInteractionType::APP_STARTED_FROM_STYLUS_TOOLS);
diff --git a/chrome/browser/ash/system_extensions/BUILD.gn b/chrome/browser/ash/system_extensions/BUILD.gn
index 27f8bf9..5c23430 100644
--- a/chrome/browser/ash/system_extensions/BUILD.gn
+++ b/chrome/browser/ash/system_extensions/BUILD.gn
@@ -23,6 +23,8 @@
     "system_extensions_install_manager.cc",
     "system_extensions_install_manager.h",
     "system_extensions_install_status.h",
+    "system_extensions_internals_page_handler.cc",
+    "system_extensions_internals_page_handler.h",
     "system_extensions_profile_utils.cc",
     "system_extensions_profile_utils.h",
     "system_extensions_provider.cc",
@@ -40,6 +42,7 @@
   deps = [
     ":system_extensions_group",
     "//ash/constants",
+    "//ash/webui/system_extensions_internals_ui/mojom",
     "//base",
     "//chrome/browser/chromeos",
     "//chrome/browser/profiles",
diff --git a/chrome/browser/ash/system_extensions/api/window_management/BUILD.gn b/chrome/browser/ash/system_extensions/api/window_management/BUILD.gn
index 57b14a04..f2e1027e 100644
--- a/chrome/browser/ash/system_extensions/api/window_management/BUILD.gn
+++ b/chrome/browser/ash/system_extensions/api/window_management/BUILD.gn
@@ -2,6 +2,10 @@
 # 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")
+
+assert(is_chromeos_ash)
+
 source_set("window_management") {
   sources = [
     "window_management_impl.cc",
@@ -12,6 +16,7 @@
     "//components/services/app_service/public/cpp:instance_update",
     "//components/services/app_service/public/mojom",
     "//third_party/blink/public/mojom:mojom_platform",
+    "//ui/aura",
     "//ui/views",
     "//ui/webui",
   ]
diff --git a/chrome/browser/ash/system_extensions/api/window_management/window_management_impl.cc b/chrome/browser/ash/system_extensions/api/window_management/window_management_impl.cc
index ff4ca8b6..395f8c201 100644
--- a/chrome/browser/ash/system_extensions/api/window_management/window_management_impl.cc
+++ b/chrome/browser/ash/system_extensions/api/window_management/window_management_impl.cc
@@ -54,6 +54,15 @@
                                            int32_t y,
                                            int32_t width,
                                            int32_t height) {
+  aura::Window* target = GetWindow(id);
+  // TODO(crbug.com/1253318): Ensure this works with multiple screens.
+  if (target) {
+    target->SetBounds(gfx::Rect(x, y, width, height));
+  }
+}
+
+aura::Window* WindowManagementImpl::GetWindow(
+    const base::UnguessableToken& id) {
   aura::Window* target = nullptr;
   apps::AppServiceProxy* proxy = apps::AppServiceProxyFactory::GetForProfile(
       Profile::FromBrowserContext(browser_context_));
@@ -63,10 +72,8 @@
           target = update.Window();
         }
       });
-  // TODO(crbug.com/1253318): Ensure this works with multiple screens.
-  if (target) {
-    target->SetBounds(gfx::Rect(x, y, width, height));
-  }
+
+  return target;
 }
 
 }  // namespace ash
diff --git a/chrome/browser/ash/system_extensions/api/window_management/window_management_impl.h b/chrome/browser/ash/system_extensions/api/window_management/window_management_impl.h
index 3b12a121..7e866c43 100644
--- a/chrome/browser/ash/system_extensions/api/window_management/window_management_impl.h
+++ b/chrome/browser/ash/system_extensions/api/window_management/window_management_impl.h
@@ -7,6 +7,7 @@
 
 #include "base/unguessable_token.h"
 #include "third_party/blink/public/mojom/chromeos/system_extensions/window_management/cros_window_management.mojom.h"
+#include "ui/aura/window.h"
 
 namespace content {
 class BrowserContext;
@@ -28,6 +29,8 @@
                        int32_t height) override;
 
  private:
+  aura::Window* GetWindow(const base::UnguessableToken& id);
+
   content::BrowserContext* browser_context_;
 };
 
diff --git a/chrome/browser/ash/system_extensions/system_extensions_internals_page_handler.cc b/chrome/browser/ash/system_extensions/system_extensions_internals_page_handler.cc
new file mode 100644
index 0000000..a704db82
--- /dev/null
+++ b/chrome/browser/ash/system_extensions/system_extensions_internals_page_handler.cc
@@ -0,0 +1,45 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ash/system_extensions/system_extensions_internals_page_handler.h"
+
+#include "base/debug/stack_trace.h"
+
+SystemExtensionsInternalsPageHandler::SystemExtensionsInternalsPageHandler(
+    Profile* profile)
+    : profile_(profile) {}
+
+SystemExtensionsInternalsPageHandler::~SystemExtensionsInternalsPageHandler() =
+    default;
+
+void SystemExtensionsInternalsPageHandler::
+    InstallSystemExtensionFromDownloadsDir(
+        const base::SafeBaseName& system_extension_dir_name,
+        InstallSystemExtensionFromDownloadsDirCallback callback) {
+  base::FilePath downloads_path;
+  if (!base::PathService::Get(chrome::DIR_DEFAULT_DOWNLOADS, &downloads_path)) {
+    std::move(callback).Run(false);
+    return;
+  }
+
+  auto& install_manager =
+      SystemExtensionsProvider::Get(profile_)->install_manager();
+  base::FilePath system_extension_dir =
+      downloads_path.Append(system_extension_dir_name);
+
+  install_manager.InstallUnpackedExtensionFromDir(
+      system_extension_dir,
+      base::BindOnce(&SystemExtensionsInternalsPageHandler::OnInstallFinished,
+                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void SystemExtensionsInternalsPageHandler::OnInstallFinished(
+    InstallSystemExtensionFromDownloadsDirCallback callback,
+    InstallStatusOrSystemExtensionId result) {
+  if (!result.ok()) {
+    LOG(ERROR) << "failed with: " << static_cast<int32_t>(result.status());
+  }
+
+  std::move(callback).Run(result.ok());
+}
diff --git a/chrome/browser/ash/system_extensions/system_extensions_internals_page_handler.h b/chrome/browser/ash/system_extensions/system_extensions_internals_page_handler.h
new file mode 100644
index 0000000..b286c5b
--- /dev/null
+++ b/chrome/browser/ash/system_extensions/system_extensions_internals_page_handler.h
@@ -0,0 +1,40 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_ASH_SYSTEM_EXTENSIONS_SYSTEM_EXTENSIONS_INTERNALS_PAGE_HANDLER_H_
+#define CHROME_BROWSER_ASH_SYSTEM_EXTENSIONS_SYSTEM_EXTENSIONS_INTERNALS_PAGE_HANDLER_H_
+
+#include "ash/webui/system_extensions_internals_ui/mojom/system_extensions_internals_ui.mojom.h"
+#include "base/files/file_path.h"
+#include "base/files/safe_base_name.h"
+#include "base/memory/weak_ptr.h"
+#include "base/path_service.h"
+#include "chrome/browser/ash/system_extensions/system_extensions_install_status.h"
+#include "chrome/browser/ash/system_extensions/system_extensions_provider.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/common/chrome_paths.h"
+
+class SystemExtensionsInternalsPageHandler
+    : public ash::mojom::system_extensions_internals::PageHandler {
+ public:
+  explicit SystemExtensionsInternalsPageHandler(Profile* profile);
+  ~SystemExtensionsInternalsPageHandler() override;
+
+  // mojom::system_extensions_internals::PageHandler
+  void InstallSystemExtensionFromDownloadsDir(
+      const base::SafeBaseName& system_extension_dir_name,
+      InstallSystemExtensionFromDownloadsDirCallback callback) override;
+
+ private:
+  void OnInstallFinished(
+      InstallSystemExtensionFromDownloadsDirCallback callback,
+      InstallStatusOrSystemExtensionId result);
+
+  raw_ptr<Profile> profile_;
+
+  base::WeakPtrFactory<SystemExtensionsInternalsPageHandler> weak_ptr_factory_{
+      this};
+};
+
+#endif  // CHROME_BROWSER_ASH_SYSTEM_EXTENSIONS_SYSTEM_EXTENSIONS_INTERNALS_PAGE_HANDLER_H_
diff --git a/chrome/browser/ash/web_applications/eche_app_info.cc b/chrome/browser/ash/web_applications/eche_app_info.cc
index fe742456..c6e01aee 100644
--- a/chrome/browser/ash/web_applications/eche_app_info.cc
+++ b/chrome/browser/ash/web_applications/eche_app_info.cc
@@ -93,7 +93,8 @@
   // than half of the windows.
   gfx::Rect bounds =
       display::Screen::GetScreen()->GetDisplayForNewWindows().work_area();
-  const float bounds_aspect_ratio = bounds.width() / bounds.height();
+  const float bounds_aspect_ratio =
+      static_cast<float>(bounds.width()) / bounds.height();
   const bool is_landscape = (bounds_aspect_ratio >= 1);
   auto new_width = is_landscape ? (bounds.height() / 2) : bounds.width() / 2;
   if (kMinimumEcheSize.width() > new_width) {
diff --git a/chrome/browser/ash/web_applications/eche_app_integration_browsertest.cc b/chrome/browser/ash/web_applications/eche_app_integration_browsertest.cc
index 0b470e92..f519acf 100644
--- a/chrome/browser/ash/web_applications/eche_app_integration_browsertest.cc
+++ b/chrome/browser/ash/web_applications/eche_app_integration_browsertest.cc
@@ -57,11 +57,11 @@
   gfx::Rect work_area =
       display::Screen::GetScreen()->GetDisplayForNewWindows().work_area();
   int expected_width = work_area.height() / 2;
-  int expected_hight = work_area.height() / 2 * aspect_ratio;
+  int expected_height = work_area.height() * aspect_ratio / 2;
   int x = (work_area.width() - expected_width) / 2;
-  int y = (work_area.height() - expected_hight) / 2;
+  int y = (work_area.height() - expected_height) / 2;
   EXPECT_EQ(browser->window()->GetBounds(),
-            gfx::Rect(x, y, expected_width, expected_hight));
+            gfx::Rect(x, y, expected_width, expected_height));
 }
 
 IN_PROC_BROWSER_TEST_P(EcheAppIntegrationTest,
@@ -78,11 +78,11 @@
   gfx::Rect work_area =
       display::Screen::GetScreen()->GetDisplayForNewWindows().work_area();
   int expected_width = work_area.width() / 2;
-  int expected_hight = work_area.width() / 2 * aspect_ratio;
+  int expected_height = work_area.width() * aspect_ratio / 2;
   int x = (work_area.width() - expected_width) / 2;
-  int y = (work_area.height() - expected_hight) / 2;
+  int y = (work_area.height() - expected_height) / 2;
   EXPECT_EQ(browser->window()->GetBounds(),
-            gfx::Rect(x, y, expected_width, expected_hight));
+            gfx::Rect(x, y, expected_width, expected_height));
 }
 
 IN_PROC_BROWSER_TEST_P(EcheAppIntegrationTest,
diff --git a/chrome/browser/ash/web_applications/os_feedback_app_integration_browsertest.cc b/chrome/browser/ash/web_applications/os_feedback_app_integration_browsertest.cc
index 7e152ff..e20a77e 100644
--- a/chrome/browser/ash/web_applications/os_feedback_app_integration_browsertest.cc
+++ b/chrome/browser/ash/web_applications/os_feedback_app_integration_browsertest.cc
@@ -94,11 +94,11 @@
       display::Screen::GetScreen()->GetDisplayForNewWindows().work_area();
 
   int expected_width = 600;
-  int expected_hight = 640;
+  int expected_height = 640;
   int x = (work_area.width() - expected_width) / 2;
-  int y = (work_area.height() - expected_hight) / 2;
+  int y = (work_area.height() - expected_height) / 2;
   EXPECT_EQ(browser->window()->GetBounds(),
-            gfx::Rect(x, y, expected_width, expected_hight));
+            gfx::Rect(x, y, expected_width, expected_height));
 }
 
 // Test that the Feedback App
diff --git a/chrome/browser/chrome_browser_interface_binders.cc b/chrome/browser/chrome_browser_interface_binders.cc
index 315507e..10db96e 100644
--- a/chrome/browser/chrome_browser_interface_binders.cc
+++ b/chrome/browser/chrome_browser_interface_binders.cc
@@ -74,6 +74,7 @@
 #include "content/public/common/url_constants.h"
 #include "extensions/buildflags/buildflags.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/self_owned_receiver.h"
 #include "services/image_annotation/public/mojom/image_annotation.mojom.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/mojom/credentialmanager/credential_manager.mojom.h"
@@ -219,7 +220,10 @@
 #include "ash/webui/scanning/mojom/scanning.mojom.h"
 #include "ash/webui/scanning/scanning_ui.h"
 #include "ash/webui/shimless_rma/shimless_rma.h"
+#include "ash/webui/system_extensions_internals_ui/mojom/system_extensions_internals_ui.mojom.h"
+#include "ash/webui/system_extensions_internals_ui/system_extensions_internals_ui.h"
 #include "chrome/browser/apps/digital_goods/digital_goods_factory_impl.h"
+#include "chrome/browser/ash/system_extensions/system_extensions_internals_page_handler.h"
 #include "chrome/browser/nearby_sharing/common/nearby_share_features.h"
 #include "chrome/browser/speech/cros_speech_recognition_service_factory.h"
 #include "chrome/browser/ui/webui/chromeos/add_supervision/add_supervision.mojom.h"
@@ -304,6 +308,16 @@
 #include "components/history_clusters/history_clusters_internals/webui/history_clusters_internals_ui.h"
 #endif
 
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+void ash::SystemExtensionsInternalsUI::BindInterface(
+    mojo::PendingReceiver<ash::mojom::system_extensions_internals::PageHandler>
+        receiver) {
+  auto page_handler = std::make_unique<SystemExtensionsInternalsPageHandler>(
+      Profile::FromWebUI(web_ui()));
+  mojo::MakeSelfOwnedReceiver(std::move(page_handler), std::move(receiver));
+}
+#endif
+
 namespace chrome {
 namespace internal {
 
@@ -977,6 +991,10 @@
       map);
 
   RegisterWebUIControllerInterfaceBinder<
+      ash::eche_app::mojom::DisplayStreamHandler, ash::eche_app::EcheAppUI>(
+      map);
+
+  RegisterWebUIControllerInterfaceBinder<
       ash::media_app_ui::mojom::PageHandlerFactory, ash::MediaAppUI>(map);
 
   RegisterWebUIControllerInterfaceBinder<
@@ -1139,6 +1157,13 @@
       .Add<ash::mojom::sample_swa::PageHandlerFactory>();
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH) && !defined(OFFICIAL_BUILD)
 
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+  if (base::FeatureList::IsEnabled(ash::features::kSystemExtensions)) {
+    registry.ForWebUI<ash::SystemExtensionsInternalsUI>()
+        .Add<ash::mojom::system_extensions_internals::PageHandler>();
+  }
+#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
+
   // --- Section 2: chrome-untrusted:// WebUIs:
 
 #if BUILDFLAG(IS_CHROMEOS_ASH) && !defined(OFFICIAL_BUILD)
diff --git a/chrome/browser/extensions/api/web_navigation/web_navigation_api.cc b/chrome/browser/extensions/api/web_navigation/web_navigation_api.cc
index ba97b2d..6a16b9c8 100644
--- a/chrome/browser/extensions/api/web_navigation/web_navigation_api.cc
+++ b/chrome/browser/extensions/api/web_navigation/web_navigation_api.cc
@@ -453,69 +453,25 @@
 ExtensionFunction::ResponseAction WebNavigationGetFrameFunction::Run() {
   std::unique_ptr<GetFrame::Params> params(GetFrame::Params::Create(args()));
   EXTENSION_FUNCTION_VALIDATE(params.get());
+  int tab_id = params->details.tab_id;
+  int frame_id = params->details.frame_id;
 
-  int tab_id = api::tabs::TAB_ID_NONE;
-  int frame_id = -1;
-
-  content::RenderFrameHost* render_frame_host = nullptr;
-  if (params->details.document_id) {
-    ExtensionApiFrameIdMap::DocumentId document_id =
-        ExtensionApiFrameIdMap::DocumentIdFromString(
-            *params->details.document_id);
-    if (!document_id)
-      return RespondNow(Error("Invalid documentId."));
-
-    // Note that we will globally find a RenderFrameHost but validate that
-    // we are in the right context still as we may be in the wrong profile
-    // or in incognito mode.
-    render_frame_host =
-        ExtensionApiFrameIdMap::Get()->GetRenderFrameHostByDocumentId(
-            document_id);
-
-    if (!render_frame_host)
-      return RespondNow(OneArgument(base::Value()));
-
-    content::WebContents* web_contents =
-        content::WebContents::FromRenderFrameHost(render_frame_host);
-    // We found the RenderFrameHost through a generic lookup so we must test to
-    // see if the WebContents is actually in our BrowserContext.
-    if (!ExtensionTabUtil::IsWebContentsInContext(
-            web_contents, browser_context(), include_incognito_information())) {
-      return RespondNow(OneArgument(base::Value()));
-    }
-
-    tab_id = ExtensionTabUtil::GetTabId(web_contents);
-    frame_id = ExtensionApiFrameIdMap::GetFrameId(render_frame_host);
-
-    // If the provided tab_id and frame_id do not match the calculated ones
-    // return.
-    if ((params->details.tab_id && *params->details.tab_id != tab_id) ||
-        (params->details.frame_id && *params->details.frame_id != frame_id)) {
-      return RespondNow(OneArgument(base::Value()));
-    }
-  } else {
-    // If documentId is not provided, tab_id and frame_id must be. Return early
-    // if not.
-    if (!params->details.tab_id || !params->details.frame_id) {
-      return RespondNow(Error(
-          "Either documentId or both tabId and frameId must be specified."));
-    }
-
-    tab_id = *params->details.tab_id;
-    frame_id = *params->details.frame_id;
-
-    content::WebContents* web_contents = nullptr;
-    if (!ExtensionTabUtil::GetTabById(tab_id, browser_context(),
-                                      include_incognito_information(),
-                                      &web_contents) ||
-        !web_contents) {
-      return RespondNow(OneArgument(base::Value()));
-    }
-
-    render_frame_host = ExtensionApiFrameIdMap::Get()->GetRenderFrameHostById(
-        web_contents, frame_id);
+  content::WebContents* web_contents;
+  if (!ExtensionTabUtil::GetTabById(tab_id, browser_context(),
+                                    include_incognito_information(),
+                                    &web_contents) ||
+      !web_contents) {
+    return RespondNow(OneArgument(base::Value()));
   }
 
+  WebNavigationTabObserver* observer =
+      WebNavigationTabObserver::Get(web_contents);
+  DCHECK(observer);
+
+  content::RenderFrameHost* render_frame_host =
+      ExtensionApiFrameIdMap::Get()->GetRenderFrameHostById(web_contents,
+                                                            frame_id);
+
   auto* frame_navigation_state =
       render_frame_host
           ? FrameNavigationState::GetForCurrentDocument(render_frame_host)
@@ -555,7 +511,7 @@
   EXTENSION_FUNCTION_VALIDATE(params.get());
   int tab_id = params->details.tab_id;
 
-  content::WebContents* web_contents = nullptr;
+  content::WebContents* web_contents;
   if (!ExtensionTabUtil::GetTabById(tab_id, browser_context(),
                                     include_incognito_information(),
                                     &web_contents) ||
@@ -563,6 +519,10 @@
     return RespondNow(OneArgument(base::Value()));
   }
 
+  WebNavigationTabObserver* observer =
+      WebNavigationTabObserver::Get(web_contents);
+  DCHECK(observer);
+
   std::vector<GetAllFrames::Results::DetailsType> result_list;
 
   // We only iterate the frames in the active page. We currently do not
diff --git a/chrome/browser/extensions/extension_tab_util.cc b/chrome/browser/extensions/extension_tab_util.cc
index 253ea51..e81672c 100644
--- a/chrome/browser/extensions/extension_tab_util.cc
+++ b/chrome/browser/extensions/extension_tab_util.cc
@@ -769,24 +769,6 @@
   return active_contents;
 }
 
-// static
-bool ExtensionTabUtil::IsWebContentsInContext(
-    content::WebContents* web_contents,
-    content::BrowserContext* browser_context,
-    bool include_incognito) {
-  // Look at the WebContents BrowserContext and see if it is the same.
-  content::BrowserContext* web_contents_browser_context =
-      web_contents->GetBrowserContext();
-  if (web_contents_browser_context == browser_context)
-    return true;
-
-  // If not it might be to include the incongito mode, so we if the profiles
-  // are the same or the parent.
-  return include_incognito && Profile::FromBrowserContext(browser_context)
-                                  ->IsSameOrParent(Profile::FromBrowserContext(
-                                      web_contents_browser_context));
-}
-
 GURL ExtensionTabUtil::ResolvePossiblyRelativeURL(const std::string& url_string,
                                                   const Extension* extension) {
   GURL url = GURL(url_string);
diff --git a/chrome/browser/extensions/extension_tab_util.h b/chrome/browser/extensions/extension_tab_util.h
index a4faba975..0b196e5 100644
--- a/chrome/browser/extensions/extension_tab_util.h
+++ b/chrome/browser/extensions/extension_tab_util.h
@@ -202,12 +202,6 @@
       content::BrowserContext* browser_context,
       bool include_incognito);
 
-  // Determines if the |web_contents| is in |browser_context| or it's OTR
-  // BrowserContext if |include_incognito| is true.
-  static bool IsWebContentsInContext(content::WebContents* web_contents,
-                                     content::BrowserContext* browser_context,
-                                     bool include_incognito);
-
   // Takes |url_string| and returns a GURL which is either valid and absolute
   // or invalid. If |url_string| is not directly interpretable as a valid (it is
   // likely a relative URL) an attempt is made to resolve it. When |extension|
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 3d94c19..381f396a 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -1072,7 +1072,7 @@
   {
     "name": "dark-light-mode",
     "owners": [ "minch", "tclaiborne" ],
-    "expiry_milestone": 100
+    "expiry_milestone": 110
   },
   {
     "name": "darken-websites-checkbox-in-themes-setting",
diff --git a/chrome/browser/notifications/notification_platform_bridge_lacros.cc b/chrome/browser/notifications/notification_platform_bridge_lacros.cc
index 854ca97..e6e03edb 100644
--- a/chrome/browser/notifications/notification_platform_bridge_lacros.cc
+++ b/chrome/browser/notifications/notification_platform_bridge_lacros.cc
@@ -90,6 +90,7 @@
   mojo_note->accessible_name = notification.accessible_name();
   mojo_note->fullscreen_visibility =
       ToMojo(notification.fullscreen_visibility());
+  mojo_note->accent_color = notification.accent_color();
   return mojo_note;
 }
 
diff --git a/chrome/browser/policy/test/promotional_tabs_enabled_policy_browsertest.cc b/chrome/browser/policy/test/promotional_tabs_enabled_policy_browsertest.cc
index 35146a2..366471c9 100644
--- a/chrome/browser/policy/test/promotional_tabs_enabled_policy_browsertest.cc
+++ b/chrome/browser/policy/test/promotional_tabs_enabled_policy_browsertest.cc
@@ -170,7 +170,7 @@
   void SetUpCommandLine(base::CommandLine* command_line) override {
     ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
     command_line->RemoveSwitch(switches::kForceFirstRun);
-    command_line->AppendSwitch(switches::kNoFirstRun);
+    command_line->AppendSwitch(switches::kForceWhatsNew);
     command_line->AppendSwitchPath(switches::kUserDataDir, temp_dir_.GetPath());
 
     // Suppress the welcome page by setting the pref indicating that it has
diff --git a/chrome/browser/query_tiles/query_tile_utils.cc b/chrome/browser/query_tiles/query_tile_utils.cc
index dc72e63..89b2f31 100644
--- a/chrome/browser/query_tiles/query_tile_utils.cc
+++ b/chrome/browser/query_tiles/query_tile_utils.cc
@@ -43,8 +43,10 @@
 }
 
 bool IsQueryTilesEnabled() {
-  return query_tiles::features::IsQueryTilesEnabledForCountry(
-             GetCountryCode()) ||
+  return (!base::FeatureList::IsEnabled(
+              query_tiles::features::kQueryTilesDisableCountryOverride) &&
+          query_tiles::features::IsQueryTilesEnabledForCountry(
+              GetCountryCode())) ||
          (base::FeatureList::IsEnabled(query_tiles::features::kQueryTiles) &&
           base::FeatureList::IsEnabled(
               query_tiles::features::kQueryTilesInNTP));
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/BUILD.gn b/chrome/browser/resources/chromeos/accessibility/chromevox/BUILD.gn
index a0cea01..b4fb442a 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/BUILD.gn
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/BUILD.gn
@@ -39,7 +39,7 @@
   "background/chromevox_state.js",
   "background/classic_background.js",
   "background/color.js",
-  "background/command_handler.js",
+  "background/command_handler_interface.js",
   "background/custom_automation_event.js",
   "background/desktop_automation_handler.js",
   "background/editing/editable_line.js",
@@ -119,6 +119,7 @@
 chromevox_es6_modules = [
   "../common/instance_checker.js",
   "background/background.js",
+  "background/command_handler.js",
   "background/download_handler.js",
   "background/earcon_engine.js",
   "background/earcons.js",
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/auto_scroll_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/auto_scroll_handler.js
index 53926b1..f7f2051 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/auto_scroll_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/auto_scroll_handler.js
@@ -186,7 +186,8 @@
           nextRange = cursors.Range.fromNode(scrollable).sync(unit, dir);
           if (unit === cursors.Unit.NODE) {
             nextRange =
-                CommandHandler.skipLabelOrDescriptionFor(nextRange, dir);
+                CommandHandlerInterface.instance.skipLabelOrDescriptionFor(
+                    nextRange, dir);
           }
         } else if (pred) {
           let node;
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/background.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/background.js
index ef0aa8b0..303de378 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/background.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/background.js
@@ -3,6 +3,8 @@
 // found in the LICENSE file.
 
 import {InstanceChecker} from '../../common/instance_checker.js';
+
+import {CommandHandler} from './command_handler.js';
 import {DownloadHandler} from './download_handler.js';
 import {Earcons} from './earcons.js';
 import {FindHandler} from './find_handler.js';
@@ -102,7 +104,6 @@
     /** @private {!PageLoadSoundHandler} */
     this.pageLoadSoundHandler_ = new PageLoadSoundHandler();
 
-    CommandHandler.init();
     FindHandler.init();
     DownloadHandler.init();
     JaPhoneticData.init(JaPhoneticMap.MAP);
@@ -142,9 +143,11 @@
   }
 
   /**
+   * @param {cursors.Range} newRange The new range.
+   * @param {boolean=} opt_fromEditing
    * @override
    */
-  setCurrentRange(newRange) {
+  setCurrentRange(newRange, opt_fromEditing) {
     // Clear anything that was frozen on the braille display whenever
     // the user navigates.
     ChromeVox.braille.thaw();
@@ -158,8 +161,9 @@
 
     this.previousRange_ = this.currentRange_;
     this.currentRange_ = newRange;
+
     ChromeVoxState.observers.forEach(function(observer) {
-      observer.onCurrentRangeChanged(newRange);
+      observer.onCurrentRangeChanged(newRange, opt_fromEditing);
     });
 
     if (!this.currentRange_) {
@@ -318,7 +322,7 @@
           const isClassicEnabled = false;
           port.postMessage({target: 'next', isClassicEnabled});
         } else if (action === 'onCommand') {
-          CommandHandler.onCommand(msg['command']);
+          CommandHandlerInterface.instance.onCommand(msg['command']);
         } else if (action === 'flushNextUtterance') {
           Output.forceModeForNextSpeechUtterance(QueueMode.FLUSH);
         }
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js
index 388a23b..8a294528 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js
@@ -1046,7 +1046,7 @@
     // Fakes a toggleSelection command.
     root.addEventListener('textSelectionChanged', function() {
       if (root.focusOffset === 3) {
-        CommandHandler.onCommand('toggleSelection');
+        CommandHandlerInterface.instance.onCommand('toggleSelection');
       }
     }, true);
 
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille_command_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille_command_handler.js
index 372f3c0..d48a779 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille_command_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille_command_handler.js
@@ -45,22 +45,22 @@
   Output.forceModeForNextSpeechUtterance(QueueMode.FLUSH);
   switch (evt.command) {
     case BrailleKeyCommand.PAN_LEFT:
-      CommandHandler.onCommand('previousObject');
+      CommandHandlerInterface.instance.onCommand('previousObject');
       break;
     case BrailleKeyCommand.PAN_RIGHT:
-      CommandHandler.onCommand('nextObject');
+      CommandHandlerInterface.instance.onCommand('nextObject');
       break;
     case BrailleKeyCommand.LINE_UP:
-      CommandHandler.onCommand('previousLine');
+      CommandHandlerInterface.instance.onCommand('previousLine');
       break;
     case BrailleKeyCommand.LINE_DOWN:
-      CommandHandler.onCommand('nextLine');
+      CommandHandlerInterface.instance.onCommand('nextLine');
       break;
     case BrailleKeyCommand.TOP:
-      CommandHandler.onCommand('jumpToTop');
+      CommandHandlerInterface.instance.onCommand('jumpToTop');
       break;
     case BrailleKeyCommand.BOTTOM:
-      CommandHandler.onCommand('jumpToBottom');
+      CommandHandlerInterface.instance.onCommand('jumpToBottom');
       break;
     case BrailleKeyCommand.ROUTING:
       BrailleCommandHandler.onRoutingCommand_(
@@ -76,7 +76,7 @@
       const command = BrailleCommandData.getCommand(evt.brailleDots);
       if (command) {
         if (BrailleCommandHandler.onEditCommand_(command)) {
-          CommandHandler.onCommand(command);
+          CommandHandlerInterface.instance.onCommand(command);
         }
       }
       break;
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/chromevox_state.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/chromevox_state.js
index bdc5fd0..52f4cce 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/chromevox_state.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/chromevox_state.js
@@ -21,13 +21,12 @@
  * changes.
  * @interface
  */
-ChromeVoxStateObserver = function() {};
-
-ChromeVoxStateObserver.prototype = {
+ChromeVoxStateObserver = class {
   /**
    * @param {cursors.Range} range The new range.
+   * @param {boolean=} opt_fromEditing
    */
-  onCurrentRangeChanged(range) {}
+  onCurrentRangeChanged(range, opt_fromEditing) {}
 };
 
 /**
@@ -97,6 +96,7 @@
 
   /**
    * @param {cursors.Range} newRange The new range.
+   * @param {boolean=} opt_fromEditing
    */
   setCurrentRange: goog.abstractMethod,
   /**
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler.js
index 93c2584..97099e5 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler.js
@@ -6,25 +6,7 @@
  * @fileoverview ChromeVox commands.
  */
 
-goog.provide('CommandHandler');
 
-goog.require('AutoScrollHandler');
-goog.require('ChromeVoxState');
-goog.require('Color');
-goog.require('CustomAutomationEvent');
-goog.require('EventGenerator');
-goog.require('KeyCode');
-goog.require('LogStore');
-goog.require('Output');
-goog.require('PhoneticData');
-goog.require('SmartStickyMode');
-goog.require('TreeDumper');
-goog.require('ChromeVoxBackground');
-goog.require('ChromeVoxKbHandler');
-goog.require('ChromeVoxPrefs');
-goog.require('CommandStore');
-
-goog.scope(function() {
 const ActionType = chrome.automation.ActionType;
 const AutomationEvent = chrome.automation.AutomationEvent;
 const AutomationNode = chrome.automation.AutomationNode;
@@ -33,1212 +15,1188 @@
 const RoleType = chrome.automation.RoleType;
 const StateType = chrome.automation.StateType;
 
-/** @private {boolean} */
-CommandHandler.isIncognito_ = !!chrome.runtime.getManifest()['incognito'];
+export class CommandHandler extends CommandHandlerInterface {
+  /** @private */
+  constructor() {
+    super();
 
-/** @private {boolean} */
-CommandHandler.languageLoggingEnabled_ = false;
+    /** @private {boolean} */
+    this.isIncognito_ = !!chrome.runtime.getManifest()['incognito'];
 
-/**
- * Handles toggling sticky mode when encountering editables.
- * @private {!SmartStickyMode}
- */
-CommandHandler.smartStickyMode_ = new SmartStickyMode();
+    /** @private {boolean} */
+    this.languageLoggingEnabled_ = false;
 
-/**
- * Handles ChromeVox commands.
- * @param {string} command
- * @return {boolean} True if the command should propagate.
- */
-CommandHandler.onCommand = function(command) {
-  // Check for a command denied in incognito contexts and kiosk.
-  if ((CommandHandler.isIncognito_ || CommandHandler.isKioskSession_) &&
-      CommandStore.CMD_ALLOWLIST[command] &&
-      CommandStore.CMD_ALLOWLIST[command].denyOOBE) {
-    return true;
+    /**
+     * Handles toggling sticky mode when encountering editables.
+     * @private {!SmartStickyMode}
+     */
+    this.smartStickyMode_ = new SmartStickyMode();
+
+    /**
+     * To support viewGraphicAsBraille_(), the current image node.
+     * @type {AutomationNode?};
+     */
+    this.imageNode_;
+
+    /** @private {boolean} */
+    this.isKioskSession_ = false;
+
+    this.init();
   }
 
-  // Check for loss of focus which results in us invalidating our current
-  // range. Note this call is synchronous.
-  chrome.automation.getFocus(function(focusedNode) {
-    const cur = ChromeVoxState.instance.currentRange;
-    if (cur && !cur.isValid()) {
-      ChromeVoxState.instance.setCurrentRange(
-          cursors.Range.fromNode(focusedNode));
-    }
-
-    if (!focusedNode ||
-
-        // This case detects when TalkBack (in ARC++) is enabled (which also
-        // covers when the ARC++ window is active). Clear the ChromeVox range so
-        // keys get passed through for ChromeVox commands.
-        (ChromeVoxState.instance.talkBackEnabled &&
-
-         // This additional check is not strictly necessary, but we use it to
-         // ensure we are never inadvertently losing focus. ARC++ windows set
-         // "focus" on a root view.
-         focusedNode.role === RoleType.CLIENT)) {
-      ChromeVoxState.instance.setCurrentRange(null);
-    }
-  });
-
-  // These commands don't require a current range.
-  switch (command) {
-    case 'speakTimeAndDate':
-      chrome.automation.getDesktop(function(d) {
-        // First, try speaking the on-screen time.
-        const allTime = d.findAll({role: RoleType.TIME});
-        allTime.filter(function(t) {
-          return t.root.role === RoleType.DESKTOP;
-        });
-
-        let timeString = '';
-        allTime.forEach(function(t) {
-          if (t.name) {
-            timeString = t.name;
-          }
-        });
-        if (timeString) {
-          ChromeVox.tts.speak(timeString, QueueMode.FLUSH);
-          ChromeVox.braille.write(NavBraille.fromText(timeString));
-        } else {
-          // Fallback to the old way of speaking time.
-          const output = new Output();
-          const dateTime = new Date();
-          output
-              .withString(
-                  dateTime.toLocaleTimeString() + ', ' +
-                  dateTime.toLocaleDateString())
-              .go();
-        }
-      });
-      return false;
-    case 'showOptionsPage':
-      chrome.runtime.openOptionsPage();
-      break;
-    case 'toggleStickyMode':
-      ChromeVoxBackground.setPref('sticky', !ChromeVox.isStickyPrefOn, true);
-      CommandHandler.smartStickyMode_.onStickyModeCommand(
-          ChromeVoxState.instance.currentRange);
-      return false;
-    case 'passThroughMode':
-      ChromeVox.passThroughMode = true;
-      ChromeVox.tts.speak(Msgs.getMsg('pass_through_key'), QueueMode.QUEUE);
+  /** @override */
+  onCommand(command) {
+    // Check for a command denied in incognito contexts and kiosk.
+    if ((this.isIncognito_ || this.isKioskSession_) &&
+        CommandStore.CMD_ALLOWLIST[command] &&
+        CommandStore.CMD_ALLOWLIST[command].denyOOBE) {
       return true;
-    case 'showKbExplorerPage':
-      const explorerPage = {
-        url: 'chromevox/learn_mode/kbexplorer.html',
-        type: 'panel'
-      };
-      chrome.windows.create(explorerPage);
-      break;
-    case 'showLogPage':
-      const logPage = {url: 'chromevox/background/logging/log.html'};
-      chrome.tabs.create(logPage);
-      break;
-    case 'enableLogging': {
-      for (const type in ChromeVoxPrefs.loggingPrefs) {
-        ChromeVoxPrefs.instance.setLoggingPrefs(
-            ChromeVoxPrefs.loggingPrefs[type], true);
-      }
-    } break;
-    case 'disableLogging': {
-      for (const type in ChromeVoxPrefs.loggingPrefs) {
-        ChromeVoxPrefs.instance.setLoggingPrefs(
-            ChromeVoxPrefs.loggingPrefs[type], false);
-      }
-    } break;
-    case 'dumpTree':
-      chrome.automation.getDesktop(function(root) {
-        LogStore.getInstance().writeTreeLog(new TreeDumper(root));
-      });
-      break;
-    case 'decreaseTtsRate':
-      CommandHandler.increaseOrDecreaseSpeechProperty_(AbstractTts.RATE, false);
-      return false;
-    case 'increaseTtsRate':
-      CommandHandler.increaseOrDecreaseSpeechProperty_(AbstractTts.RATE, true);
-      return false;
-    case 'decreaseTtsPitch':
-      CommandHandler.increaseOrDecreaseSpeechProperty_(
-          AbstractTts.PITCH, false);
-      return false;
-    case 'increaseTtsPitch':
-      CommandHandler.increaseOrDecreaseSpeechProperty_(AbstractTts.PITCH, true);
-      return false;
-    case 'decreaseTtsVolume':
-      CommandHandler.increaseOrDecreaseSpeechProperty_(
-          AbstractTts.VOLUME, false);
-      return false;
-    case 'increaseTtsVolume':
-      CommandHandler.increaseOrDecreaseSpeechProperty_(
-          AbstractTts.VOLUME, true);
-      return false;
-    case 'stopSpeech':
-      ChromeVox.tts.stop();
-      ChromeVoxState.isReadingContinuously = false;
-      return false;
-    case 'toggleEarcons': {
-      ChromeVox.earcons.enabled = !ChromeVox.earcons.enabled;
-      const announce = ChromeVox.earcons.enabled ? Msgs.getMsg('earcons_on') :
-                                                   Msgs.getMsg('earcons_off');
-      ChromeVox.tts.speak(
-          announce, QueueMode.FLUSH, AbstractTts.PERSONALITY_ANNOTATION);
     }
-      return false;
-    case 'cycleTypingEcho': {
-      ChromeVox.typingEcho = TypingEcho.cycle(ChromeVox.typingEcho);
-      let announce = '';
-      switch (ChromeVox.typingEcho) {
-        case TypingEcho.CHARACTER:
-          announce = Msgs.getMsg('character_echo');
-          break;
-        case TypingEcho.WORD:
-          announce = Msgs.getMsg('word_echo');
-          break;
-        case TypingEcho.CHARACTER_AND_WORD:
-          announce = Msgs.getMsg('character_and_word_echo');
-          break;
-        case TypingEcho.NONE:
-          announce = Msgs.getMsg('none_echo');
-          break;
-      }
-      ChromeVox.tts.speak(
-          announce, QueueMode.FLUSH, AbstractTts.PERSONALITY_ANNOTATION);
-    }
-      return false;
-    case 'cyclePunctuationEcho':
-      ChromeVox.tts.speak(
-          Msgs.getMsg(ChromeVoxState.backgroundTts.cyclePunctuationEcho()),
-          QueueMode.FLUSH);
-      return false;
-    case 'reportIssue':
-      let url = 'https://code.google.com/p/chromium/issues/entry?' +
-          'labels=Type-Bug,Pri-2,OS-Chrome&' +
-          'components=OS>Accessibility>ChromeVox&' +
-          'description=';
 
-      const description = {};
-      description['Version'] = chrome.runtime.getManifest().version;
-      description['Reproduction Steps'] = '%0a1.%0a2.%0a3.';
-      for (const key in description) {
-        url += key + ':%20' + description[key] + '%0a';
-      }
-      chrome.tabs.create({url});
-      return false;
-    case 'toggleBrailleCaptions':
-      BrailleCaptionsBackground.setActive(
-          !BrailleCaptionsBackground.isEnabled());
-      return false;
-    case 'toggleBrailleTable': {
-      let brailleTableType = localStorage['brailleTableType'];
-      let output = '';
-      if (brailleTableType === 'brailleTable6') {
-        brailleTableType = 'brailleTable8';
-
-        // This label reads "switch to 8 dot braille".
-        output = '@OPTIONS_BRAILLE_TABLE_TYPE_6';
-      } else {
-        brailleTableType = 'brailleTable6';
-
-        // This label reads "switch to 6 dot braille".
-        output = '@OPTIONS_BRAILLE_TABLE_TYPE_8';
+    // Check for loss of focus which results in us invalidating our current
+    // range. Note this call is synchronous.
+    chrome.automation.getFocus(function(focusedNode) {
+      const cur = ChromeVoxState.instance.currentRange;
+      if (cur && !cur.isValid()) {
+        ChromeVoxState.instance.setCurrentRange(
+            cursors.Range.fromNode(focusedNode));
       }
 
-      localStorage['brailleTable'] = localStorage[brailleTableType];
-      localStorage['brailleTableType'] = brailleTableType;
-      BrailleBackground.getInstance().getTranslatorManager().refresh(
-          localStorage[brailleTableType]);
-      new Output().format(output).go();
-    }
-      return false;
-    case 'help':
-      (new PanelCommand(PanelCommandType.TUTORIAL)).send();
-      return false;
-    case 'toggleScreen':
-      const oldState = sessionStorage.getItem('darkScreen');
-      const newState = (oldState === 'true') ? false : true;
-      if (newState && localStorage['acceptToggleScreen'] !== 'true') {
-        // If this is the first time, show a confirmation dialog.
-        chrome.accessibilityPrivate.showConfirmationDialog(
-            Msgs.getMsg('toggle_screen_title'),
-            Msgs.getMsg('toggle_screen_description'), (confirmed) => {
-              if (confirmed) {
-                sessionStorage.setItem('darkScreen', 'true');
-                localStorage['acceptToggleScreen'] = true;
-                chrome.accessibilityPrivate.darkenScreen(true);
-                new Output().format('@toggle_screen_off').go();
-              }
-            });
-      } else {
-        sessionStorage.setItem('darkScreen', (newState) ? 'true' : 'false');
-        chrome.accessibilityPrivate.darkenScreen(newState);
-        new Output()
-            .format((newState) ? '@toggle_screen_off' : '@toggle_screen_on')
-            .go();
+      if (!focusedNode ||
+
+          // This case detects when TalkBack (in ARC++) is enabled (which also
+          // covers when the ARC++ window is active). Clear the ChromeVox range
+          // so keys get passed through for ChromeVox commands.
+          (ChromeVoxState.instance.talkBackEnabled &&
+
+           // This additional check is not strictly necessary, but we use it to
+           // ensure we are never inadvertently losing focus. ARC++ windows set
+           // "focus" on a root view.
+           focusedNode.role === RoleType.CLIENT)) {
+        ChromeVoxState.instance.setCurrentRange(null);
       }
-      return false;
-    case 'toggleSpeechOnOrOff':
-      const state = ChromeVox.tts.toggleSpeechOnOrOff();
-      new Output().format(state ? '@speech_on' : '@speech_off').go();
-      return false;
-    case 'enableChromeVoxArcSupportForCurrentApp':
-      chrome.accessibilityPrivate.setNativeChromeVoxArcSupportForCurrentApp(
-          true, (response) => {});
-      break;
-    case 'disableChromeVoxArcSupportForCurrentApp':
-      chrome.accessibilityPrivate.setNativeChromeVoxArcSupportForCurrentApp(
-          false, (response) => {
-            if (response ===
-                chrome.accessibilityPrivate.SetNativeChromeVoxResponse
-                    .TALKBACK_NOT_INSTALLED) {
-              ChromeVox.braille.write(NavBraille.fromText(
-                  Msgs.getMsg('announce_install_talkback')));
-              ChromeVox.tts.speak(
-                  Msgs.getMsg('announce_install_talkback'), QueueMode.FLUSH);
+    });
+
+    // These commands don't require a current range.
+    switch (command) {
+      case 'speakTimeAndDate':
+        chrome.automation.getDesktop(function(d) {
+          // First, try speaking the on-screen time.
+          const allTime = d.findAll({role: RoleType.TIME});
+          allTime.filter(function(t) {
+            return t.root.role === RoleType.DESKTOP;
+          });
+
+          let timeString = '';
+          allTime.forEach(function(t) {
+            if (t.name) {
+              timeString = t.name;
             }
           });
-      break;
-    case 'showTtsSettings':
-      chrome.accessibilityPrivate.openSettingsSubpage(
-          'manageAccessibility/tts');
-      break;
-    default:
-      break;
-    case 'toggleKeyboardHelp':
-      (new PanelCommand(PanelCommandType.OPEN_MENUS)).send();
-      return false;
-    case 'showPanelMenuMostRecent':
-      (new PanelCommand(PanelCommandType.OPEN_MENUS_MOST_RECENT)).send();
-      return false;
-    case 'nextGranularity':
-    case 'previousGranularity': {
-      const backwards = command === 'previousGranularity';
-      let gran = GestureCommandHandler.granularity;
-      const next = backwards ?
-          (--gran >= 0 ? gran : GestureGranularity.COUNT - 1) :
-          ++gran % GestureGranularity.COUNT;
-      GestureCommandHandler.granularity =
-          /** @type {GestureGranularity} */ (next);
-
-      let announce = '';
-      switch (GestureCommandHandler.granularity) {
-        case GestureGranularity.CHARACTER:
-          announce = Msgs.getMsg('character_granularity');
-          break;
-        case GestureGranularity.WORD:
-          announce = Msgs.getMsg('word_granularity');
-          break;
-        case GestureGranularity.LINE:
-          announce = Msgs.getMsg('line_granularity');
-          break;
-        case GestureGranularity.HEADING:
-          announce = Msgs.getMsg('heading_granularity');
-          break;
-        case GestureGranularity.LINK:
-          announce = Msgs.getMsg('link_granularity');
-          break;
-        case GestureGranularity.FORM_FIELD_CONTROL:
-          announce = Msgs.getMsg('form_field_control_granularity');
-          break;
+          if (timeString) {
+            ChromeVox.tts.speak(timeString, QueueMode.FLUSH);
+            ChromeVox.braille.write(NavBraille.fromText(timeString));
+          } else {
+            // Fallback to the old way of speaking time.
+            const output = new Output();
+            const dateTime = new Date();
+            output
+                .withString(
+                    dateTime.toLocaleTimeString() + ', ' +
+                    dateTime.toLocaleDateString())
+                .go();
+          }
+        });
+        return false;
+      case 'showOptionsPage':
+        chrome.runtime.openOptionsPage();
+        break;
+      case 'toggleStickyMode':
+        ChromeVoxBackground.setPref('sticky', !ChromeVox.isStickyPrefOn, true);
+        this.smartStickyMode_.onStickyModeCommand(
+            ChromeVoxState.instance.currentRange);
+        return false;
+      case 'passThroughMode':
+        ChromeVox.passThroughMode = true;
+        ChromeVox.tts.speak(Msgs.getMsg('pass_through_key'), QueueMode.QUEUE);
+        return true;
+      case 'showKbExplorerPage':
+        const explorerPage = {
+          url: 'chromevox/learn_mode/kbexplorer.html',
+          type: 'panel'
+        };
+        chrome.windows.create(explorerPage);
+        break;
+      case 'showLogPage':
+        const logPage = {url: 'chromevox/background/logging/log.html'};
+        chrome.tabs.create(logPage);
+        break;
+      case 'enableLogging': {
+        for (const type in ChromeVoxPrefs.loggingPrefs) {
+          ChromeVoxPrefs.instance.setLoggingPrefs(
+              ChromeVoxPrefs.loggingPrefs[type], true);
+        }
+      } break;
+      case 'disableLogging': {
+        for (const type in ChromeVoxPrefs.loggingPrefs) {
+          ChromeVoxPrefs.instance.setLoggingPrefs(
+              ChromeVoxPrefs.loggingPrefs[type], false);
+        }
+      } break;
+      case 'dumpTree':
+        chrome.automation.getDesktop(function(root) {
+          LogStore.getInstance().writeTreeLog(new TreeDumper(root));
+        });
+        break;
+      case 'decreaseTtsRate':
+        this.increaseOrDecreaseSpeechProperty_(AbstractTts.RATE, false);
+        return false;
+      case 'increaseTtsRate':
+        this.increaseOrDecreaseSpeechProperty_(AbstractTts.RATE, true);
+        return false;
+      case 'decreaseTtsPitch':
+        this.increaseOrDecreaseSpeechProperty_(AbstractTts.PITCH, false);
+        return false;
+      case 'increaseTtsPitch':
+        this.increaseOrDecreaseSpeechProperty_(AbstractTts.PITCH, true);
+        return false;
+      case 'decreaseTtsVolume':
+        this.increaseOrDecreaseSpeechProperty_(AbstractTts.VOLUME, false);
+        return false;
+      case 'increaseTtsVolume':
+        this.increaseOrDecreaseSpeechProperty_(AbstractTts.VOLUME, true);
+        return false;
+      case 'stopSpeech':
+        ChromeVox.tts.stop();
+        ChromeVoxState.isReadingContinuously = false;
+        return false;
+      case 'toggleEarcons': {
+        ChromeVox.earcons.enabled = !ChromeVox.earcons.enabled;
+        const announce = ChromeVox.earcons.enabled ? Msgs.getMsg('earcons_on') :
+                                                     Msgs.getMsg('earcons_off');
+        ChromeVox.tts.speak(
+            announce, QueueMode.FLUSH, AbstractTts.PERSONALITY_ANNOTATION);
       }
-      ChromeVox.tts.speak(announce, QueueMode.FLUSH);
+        return false;
+      case 'cycleTypingEcho': {
+        ChromeVox.typingEcho = TypingEcho.cycle(ChromeVox.typingEcho);
+        let announce = '';
+        switch (ChromeVox.typingEcho) {
+          case TypingEcho.CHARACTER:
+            announce = Msgs.getMsg('character_echo');
+            break;
+          case TypingEcho.WORD:
+            announce = Msgs.getMsg('word_echo');
+            break;
+          case TypingEcho.CHARACTER_AND_WORD:
+            announce = Msgs.getMsg('character_and_word_echo');
+            break;
+          case TypingEcho.NONE:
+            announce = Msgs.getMsg('none_echo');
+            break;
+        }
+        ChromeVox.tts.speak(
+            announce, QueueMode.FLUSH, AbstractTts.PERSONALITY_ANNOTATION);
+      }
+        return false;
+      case 'cyclePunctuationEcho':
+        ChromeVox.tts.speak(
+            Msgs.getMsg(ChromeVoxState.backgroundTts.cyclePunctuationEcho()),
+            QueueMode.FLUSH);
+        return false;
+      case 'reportIssue':
+        let url = 'https://code.google.com/p/chromium/issues/entry?' +
+            'labels=Type-Bug,Pri-2,OS-Chrome&' +
+            'components=OS>Accessibility>ChromeVox&' +
+            'description=';
+
+        const description = {};
+        description['Version'] = chrome.runtime.getManifest().version;
+        description['Reproduction Steps'] = '%0a1.%0a2.%0a3.';
+        for (const key in description) {
+          url += key + ':%20' + description[key] + '%0a';
+        }
+        chrome.tabs.create({url});
+        return false;
+      case 'toggleBrailleCaptions':
+        BrailleCaptionsBackground.setActive(
+            !BrailleCaptionsBackground.isEnabled());
+        return false;
+      case 'toggleBrailleTable': {
+        let brailleTableType = localStorage['brailleTableType'];
+        let output = '';
+        if (brailleTableType === 'brailleTable6') {
+          brailleTableType = 'brailleTable8';
+
+          // This label reads "switch to 8 dot braille".
+          output = '@OPTIONS_BRAILLE_TABLE_TYPE_6';
+        } else {
+          brailleTableType = 'brailleTable6';
+
+          // This label reads "switch to 6 dot braille".
+          output = '@OPTIONS_BRAILLE_TABLE_TYPE_8';
+        }
+
+        localStorage['brailleTable'] = localStorage[brailleTableType];
+        localStorage['brailleTableType'] = brailleTableType;
+        BrailleBackground.getInstance().getTranslatorManager().refresh(
+            localStorage[brailleTableType]);
+        new Output().format(output).go();
+      }
+        return false;
+      case 'help':
+        (new PanelCommand(PanelCommandType.TUTORIAL)).send();
+        return false;
+      case 'toggleScreen':
+        const oldState = sessionStorage.getItem('darkScreen');
+        const newState = (oldState === 'true') ? false : true;
+        if (newState && localStorage['acceptToggleScreen'] !== 'true') {
+          // If this is the first time, show a confirmation dialog.
+          chrome.accessibilityPrivate.showConfirmationDialog(
+              Msgs.getMsg('toggle_screen_title'),
+              Msgs.getMsg('toggle_screen_description'), (confirmed) => {
+                if (confirmed) {
+                  sessionStorage.setItem('darkScreen', 'true');
+                  localStorage['acceptToggleScreen'] = true;
+                  chrome.accessibilityPrivate.darkenScreen(true);
+                  new Output().format('@toggle_screen_off').go();
+                }
+              });
+        } else {
+          sessionStorage.setItem('darkScreen', (newState) ? 'true' : 'false');
+          chrome.accessibilityPrivate.darkenScreen(newState);
+          new Output()
+              .format((newState) ? '@toggle_screen_off' : '@toggle_screen_on')
+              .go();
+        }
+        return false;
+      case 'toggleSpeechOnOrOff':
+        const state = ChromeVox.tts.toggleSpeechOnOrOff();
+        new Output().format(state ? '@speech_on' : '@speech_off').go();
+        return false;
+      case 'enableChromeVoxArcSupportForCurrentApp':
+        chrome.accessibilityPrivate.setNativeChromeVoxArcSupportForCurrentApp(
+            true, (response) => {});
+        break;
+      case 'disableChromeVoxArcSupportForCurrentApp':
+        chrome.accessibilityPrivate.setNativeChromeVoxArcSupportForCurrentApp(
+            false, (response) => {
+              if (response ===
+                  chrome.accessibilityPrivate.SetNativeChromeVoxResponse
+                      .TALKBACK_NOT_INSTALLED) {
+                ChromeVox.braille.write(NavBraille.fromText(
+                    Msgs.getMsg('announce_install_talkback')));
+                ChromeVox.tts.speak(
+                    Msgs.getMsg('announce_install_talkback'), QueueMode.FLUSH);
+              }
+            });
+        break;
+      case 'showTtsSettings':
+        chrome.accessibilityPrivate.openSettingsSubpage(
+            'manageAccessibility/tts');
+        break;
+      default:
+        break;
+      case 'toggleKeyboardHelp':
+        (new PanelCommand(PanelCommandType.OPEN_MENUS)).send();
+        return false;
+      case 'showPanelMenuMostRecent':
+        (new PanelCommand(PanelCommandType.OPEN_MENUS_MOST_RECENT)).send();
+        return false;
+      case 'nextGranularity':
+      case 'previousGranularity': {
+        const backwards = command === 'previousGranularity';
+        let gran = GestureCommandHandler.granularity;
+        const next = backwards ?
+            (--gran >= 0 ? gran : GestureGranularity.COUNT - 1) :
+            ++gran % GestureGranularity.COUNT;
+        GestureCommandHandler.granularity =
+            /** @type {GestureGranularity} */ (next);
+
+        let announce = '';
+        switch (GestureCommandHandler.granularity) {
+          case GestureGranularity.CHARACTER:
+            announce = Msgs.getMsg('character_granularity');
+            break;
+          case GestureGranularity.WORD:
+            announce = Msgs.getMsg('word_granularity');
+            break;
+          case GestureGranularity.LINE:
+            announce = Msgs.getMsg('line_granularity');
+            break;
+          case GestureGranularity.HEADING:
+            announce = Msgs.getMsg('heading_granularity');
+            break;
+          case GestureGranularity.LINK:
+            announce = Msgs.getMsg('link_granularity');
+            break;
+          case GestureGranularity.FORM_FIELD_CONTROL:
+            announce = Msgs.getMsg('form_field_control_granularity');
+            break;
+        }
+        ChromeVox.tts.speak(announce, QueueMode.FLUSH);
+      }
+        return false;
+      case 'announceBatteryDescription':
+        chrome.accessibilityPrivate.getBatteryDescription(function(
+            batteryDescription) {
+          new Output()
+              .withString(batteryDescription)
+              .withQueueMode(QueueMode.FLUSH)
+              .go();
+        });
+        break;
+      case 'resetTextToSpeechSettings':
+        ChromeVox.tts.resetTextToSpeechSettings();
+        return false;
+      case 'copy':
+        EventGenerator.sendKeyPress(KeyCode.C, {ctrl: true});
+
+        // The above command doesn't trigger document clipboard events, so we
+        // need to set this manually.
+        ChromeVoxState.instance.readNextClipboardDataChange();
+        return false;
+      case 'toggleDictation':
+        EventGenerator.sendKeyPress(KeyCode.D, {search: true});
+        return false;
     }
-      return false;
-    case 'announceBatteryDescription':
-      chrome.accessibilityPrivate.getBatteryDescription(function(
-          batteryDescription) {
+
+    // Require a current range.
+    if (!ChromeVoxState.instance.currentRange_) {
+      if (!ChromeVoxState.instance.talkBackEnabled) {
         new Output()
-            .withString(batteryDescription)
+            .withString(Msgs.getMsg(
+                EventSourceState.get() === EventSourceType.TOUCH_GESTURE ?
+                    'no_focus_touch' :
+                    'no_focus'))
             .withQueueMode(QueueMode.FLUSH)
             .go();
-      });
-      break;
-    case 'resetTextToSpeechSettings':
-      ChromeVox.tts.resetTextToSpeechSettings();
-      return false;
-    case 'copy':
-      EventGenerator.sendKeyPress(KeyCode.C, {ctrl: true});
-
-      // The above command doesn't trigger document clipboard events, so we need
-      // to set this manually.
-      ChromeVoxState.instance.readNextClipboardDataChange();
-      return false;
-    case 'toggleDictation':
-      EventGenerator.sendKeyPress(KeyCode.D, {search: true});
-      return false;
-  }
-
-  // Require a current range.
-  if (!ChromeVoxState.instance.currentRange_) {
-    if (!ChromeVoxState.instance.talkBackEnabled) {
-      new Output()
-          .withString(Msgs.getMsg(
-              EventSourceState.get() === EventSourceType.TOUCH_GESTURE ?
-                  'no_focus_touch' :
-                  'no_focus'))
-          .withQueueMode(QueueMode.FLUSH)
-          .go();
+      }
+      return true;
     }
-    return true;
-  }
 
-  // Allow edit commands first.
-  if (!CommandHandler.onEditCommand_(command)) {
-    return false;
-  }
-
-  let current = ChromeVoxState.instance.currentRange;
-  let node = current.start.node;
-
-  // If true, will check if the predicate matches the current node.
-  let matchCurrent = false;
-
-  let dir = Dir.FORWARD;
-  let pred = null;
-  let predErrorMsg = undefined;
-  let rootPred = AutomationPredicate.rootOrEditableRoot;
-  let unit = null;
-  let shouldWrap = true;
-  const speechProps = {};
-  let skipSync = false;
-  let didNavigate = false;
-  let tryScrolling = true;
-  let skipSettingSelection = false;
-  let skipInitialAncestry = true;
-  switch (command) {
-    case 'nextCharacter':
-      didNavigate = true;
-      speechProps['phoneticCharacters'] = true;
-      unit = cursors.Unit.CHARACTER;
-      current = current.move(cursors.Unit.CHARACTER, Dir.FORWARD);
-      break;
-    case 'previousCharacter':
-      dir = Dir.BACKWARD;
-      didNavigate = true;
-      speechProps['phoneticCharacters'] = true;
-      unit = cursors.Unit.CHARACTER;
-      current = current.move(cursors.Unit.CHARACTER, dir);
-      break;
-    case 'nativeNextCharacter':
-    case 'nativePreviousCharacter':
-      if (DesktopAutomationHandler.instance.textEditHandler) {
-        DesktopAutomationHandler.instance.textEditHandler.injectInferredIntents(
-            [{
-              command: chrome.automation.IntentCommandType.MOVE_SELECTION,
-              textBoundary: chrome.automation.IntentTextBoundaryType.CHARACTER
-            }]);
-      }
-      return true;
-    case 'nextWord':
-      didNavigate = true;
-      unit = cursors.Unit.WORD;
-      current = current.move(cursors.Unit.WORD, Dir.FORWARD);
-      break;
-    case 'previousWord':
-      dir = Dir.BACKWARD;
-      didNavigate = true;
-      unit = cursors.Unit.WORD;
-      current = current.move(cursors.Unit.WORD, dir);
-      break;
-    case 'nativeNextWord':
-    case 'nativePreviousWord':
-      if (DesktopAutomationHandler.instance.textEditHandler) {
-        DesktopAutomationHandler.instance.textEditHandler.injectInferredIntents(
-            [{
-              command: chrome.automation.IntentCommandType.MOVE_SELECTION,
-              textBoundary: command === 'nativeNextWord' ?
-                  chrome.automation.IntentTextBoundaryType.WORD_END :
-                  chrome.automation.IntentTextBoundaryType.WORD_START
-            }]);
-      }
-      return true;
-    case 'forward':
-    case 'nextLine':
-      didNavigate = true;
-      unit = cursors.Unit.LINE;
-      current = current.move(cursors.Unit.LINE, Dir.FORWARD);
-      break;
-    case 'backward':
-    case 'previousLine':
-      dir = Dir.BACKWARD;
-      didNavigate = true;
-      unit = cursors.Unit.LINE;
-      current = current.move(cursors.Unit.LINE, dir);
-      break;
-    case 'nextButton':
-      dir = Dir.FORWARD;
-      pred = AutomationPredicate.button;
-      predErrorMsg = 'no_next_button';
-      break;
-    case 'previousButton':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.button;
-      predErrorMsg = 'no_previous_button';
-      break;
-    case 'nextCheckbox':
-      pred = AutomationPredicate.checkBox;
-      predErrorMsg = 'no_next_checkbox';
-      break;
-    case 'previousCheckbox':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.checkBox;
-      predErrorMsg = 'no_previous_checkbox';
-      break;
-    case 'nextComboBox':
-      pred = AutomationPredicate.comboBox;
-      predErrorMsg = 'no_next_combo_box';
-      break;
-    case 'previousComboBox':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.comboBox;
-      predErrorMsg = 'no_previous_combo_box';
-      break;
-    case 'nextEditText':
-      skipSettingSelection = true;
-      pred = AutomationPredicate.editText;
-      predErrorMsg = 'no_next_edit_text';
-      CommandHandler.smartStickyMode_.startIgnoringRangeChanges();
-      break;
-    case 'previousEditText':
-      skipSettingSelection = true;
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.editText;
-      predErrorMsg = 'no_previous_edit_text';
-      CommandHandler.smartStickyMode_.startIgnoringRangeChanges();
-      break;
-    case 'nextFormField':
-      skipSettingSelection = true;
-      pred = AutomationPredicate.formField;
-      predErrorMsg = 'no_next_form_field';
-      CommandHandler.smartStickyMode_.startIgnoringRangeChanges();
-      break;
-    case 'previousFormField':
-      skipSettingSelection = true;
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.formField;
-      predErrorMsg = 'no_previous_form_field';
-      CommandHandler.smartStickyMode_.startIgnoringRangeChanges();
-      break;
-    case 'previousGraphic':
-      skipSettingSelection = true;
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.image;
-      predErrorMsg = 'no_previous_graphic';
-      break;
-    case 'nextGraphic':
-      skipSettingSelection = true;
-      pred = AutomationPredicate.image;
-      predErrorMsg = 'no_next_graphic';
-      break;
-    case 'nextHeading':
-      pred = AutomationPredicate.heading;
-      predErrorMsg = 'no_next_heading';
-      break;
-    case 'nextHeading1':
-      pred = AutomationPredicate.makeHeadingPredicate(1);
-      predErrorMsg = 'no_next_heading_1';
-      break;
-    case 'nextHeading2':
-      pred = AutomationPredicate.makeHeadingPredicate(2);
-      predErrorMsg = 'no_next_heading_2';
-      break;
-    case 'nextHeading3':
-      pred = AutomationPredicate.makeHeadingPredicate(3);
-      predErrorMsg = 'no_next_heading_3';
-      break;
-    case 'nextHeading4':
-      pred = AutomationPredicate.makeHeadingPredicate(4);
-      predErrorMsg = 'no_next_heading_4';
-      break;
-    case 'nextHeading5':
-      pred = AutomationPredicate.makeHeadingPredicate(5);
-      predErrorMsg = 'no_next_heading_5';
-      break;
-    case 'nextHeading6':
-      pred = AutomationPredicate.makeHeadingPredicate(6);
-      predErrorMsg = 'no_next_heading_6';
-      break;
-    case 'previousHeading':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.heading;
-      predErrorMsg = 'no_previous_heading';
-      break;
-    case 'previousHeading1':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.makeHeadingPredicate(1);
-      predErrorMsg = 'no_previous_heading_1';
-      break;
-    case 'previousHeading2':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.makeHeadingPredicate(2);
-      predErrorMsg = 'no_previous_heading_2';
-      break;
-    case 'previousHeading3':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.makeHeadingPredicate(3);
-      predErrorMsg = 'no_previous_heading_3';
-      break;
-    case 'previousHeading4':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.makeHeadingPredicate(4);
-      predErrorMsg = 'no_previous_heading_4';
-      break;
-    case 'previousHeading5':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.makeHeadingPredicate(5);
-      predErrorMsg = 'no_previous_heading_5';
-      break;
-    case 'previousHeading6':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.makeHeadingPredicate(6);
-      predErrorMsg = 'no_previous_heading_6';
-      break;
-    case 'nextLink':
-      pred = AutomationPredicate.link;
-      predErrorMsg = 'no_next_link';
-      break;
-    case 'previousLink':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.link;
-      predErrorMsg = 'no_previous_link';
-      break;
-    case 'nextTable':
-      pred = AutomationPredicate.table;
-      predErrorMsg = 'no_next_table';
-      break;
-    case 'previousTable':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.table;
-      predErrorMsg = 'no_previous_table';
-      break;
-    case 'nextVisitedLink':
-      pred = AutomationPredicate.visitedLink;
-      predErrorMsg = 'no_next_visited_link';
-      break;
-    case 'previousVisitedLink':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.visitedLink;
-      predErrorMsg = 'no_previous_visited_link';
-      break;
-    case 'nextLandmark':
-      pred = AutomationPredicate.landmark;
-      predErrorMsg = 'no_next_landmark';
-      break;
-    case 'previousLandmark':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.landmark;
-      predErrorMsg = 'no_previous_landmark';
-      break;
-    case 'left':
-    case 'previousObject':
-      skipSettingSelection = true;
-      dir = Dir.BACKWARD;
-      // Falls through.
-    case 'right':
-    case 'nextObject':
-      skipSettingSelection = true;
-      didNavigate = true;
-      unit = cursors.Unit.NODE;
-      current = current.move(cursors.Unit.NODE, dir);
-      current = CommandHandler.skipLabelOrDescriptionFor(current, dir);
-      break;
-    case 'previousGroup':
-      skipSync = true;
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.group;
-      break;
-    case 'nextGroup':
-      skipSync = true;
-      pred = AutomationPredicate.group;
-      break;
-    case 'previousPage':
-    case 'nextPage':
-      const root = AutomationUtil.getTopLevelRoot(current.start.node);
-      if (root && root.scrollY !== undefined) {
-        let page = Math.ceil(root.scrollY / root.location.height) || 1;
-        page = command === 'nextPage' ? page + 1 : page - 1;
-        ChromeVox.tts.stop();
-        root.setScrollOffset(0, page * root.location.height);
-      }
+    // Allow edit commands first.
+    if (!this.onEditCommand_(command)) {
       return false;
-    case 'previousSimilarItem':
-      dir = Dir.BACKWARD;
-      // Falls through.
-    case 'nextSimilarItem': {
-      skipSync = true;
-      const originalNode = node;
+    }
 
-      // Scan upwards until we get a role we don't want to ignore.
-      while (node && AutomationPredicate.ignoreDuringJump(node)) {
-        node = node.parent;
-      }
+    let current = ChromeVoxState.instance.currentRange;
+    let node = current.start.node;
 
-      const useNode = node || originalNode;
-      pred = AutomationPredicate.roles([node.role]);
-    } break;
-    case 'previousInvalidItem': {
-      dir = Dir.BACKWARD;
-      rootPred = AutomationPredicate.root;
-      pred = AutomationPredicate.isInvalid;
-      predErrorMsg = 'no_invalid_item';
-    } break;
-    case 'nextInvalidItem': {
-      pred = AutomationPredicate.isInvalid;
-      rootPred = AutomationPredicate.root;
-      predErrorMsg = 'no_invalid_item';
-    } break;
-    case 'nextList':
-      pred = AutomationPredicate.makeListPredicate(current.start.node);
-      predErrorMsg = 'no_next_list';
-      break;
-    case 'previousList':
-      dir = Dir.BACKWARD;
-      pred = AutomationPredicate.makeListPredicate(current.start.node);
-      predErrorMsg = 'no_previous_list';
-      skipInitialAncestry = false;
-      break;
-    case 'jumpToTop': {
-      const node = AutomationUtil.findNodePost(
-          current.start.node.root, Dir.FORWARD, AutomationPredicate.object);
-      if (node) {
-        current = cursors.Range.fromNode(node);
-      }
-      tryScrolling = false;
-    } break;
-    case 'jumpToBottom': {
-      const node = AutomationUtil.findLastNode(
-          current.start.node.root, AutomationPredicate.object);
-      if (node) {
-        current = cursors.Range.fromNode(node);
-      }
-      tryScrolling = false;
-    } break;
-    case 'forceClickOnCurrentItem':
-      if (ChromeVoxState.instance.currentRange) {
-        let actionNode = ChromeVoxState.instance.currentRange.start.node;
-        // Scan for a clickable, which overrides the |actionNode|.
-        let clickable = actionNode;
-        while (clickable && !clickable.clickable &&
-               actionNode.root === clickable.root) {
-          clickable = clickable.parent;
+    // If true, will check if the predicate matches the current node.
+    let matchCurrent = false;
+
+    let dir = Dir.FORWARD;
+    let pred = null;
+    let predErrorMsg = undefined;
+    let rootPred = AutomationPredicate.rootOrEditableRoot;
+    let unit = null;
+    let shouldWrap = true;
+    const speechProps = {};
+    let skipSync = false;
+    let didNavigate = false;
+    let tryScrolling = true;
+    let skipSettingSelection = false;
+    let skipInitialAncestry = true;
+    switch (command) {
+      case 'nextCharacter':
+        didNavigate = true;
+        speechProps['phoneticCharacters'] = true;
+        unit = cursors.Unit.CHARACTER;
+        current = current.move(cursors.Unit.CHARACTER, Dir.FORWARD);
+        break;
+      case 'previousCharacter':
+        dir = Dir.BACKWARD;
+        didNavigate = true;
+        speechProps['phoneticCharacters'] = true;
+        unit = cursors.Unit.CHARACTER;
+        current = current.move(cursors.Unit.CHARACTER, dir);
+        break;
+      case 'nativeNextCharacter':
+      case 'nativePreviousCharacter':
+        if (DesktopAutomationHandler.instance.textEditHandler) {
+          DesktopAutomationHandler.instance.textEditHandler
+              .injectInferredIntents([{
+                command: chrome.automation.IntentCommandType.MOVE_SELECTION,
+                textBoundary: chrome.automation.IntentTextBoundaryType.CHARACTER
+              }]);
         }
-        if (clickable && actionNode.root === clickable.root) {
-          clickable.doDefault();
-          return false;
+        return true;
+      case 'nextWord':
+        didNavigate = true;
+        unit = cursors.Unit.WORD;
+        current = current.move(cursors.Unit.WORD, Dir.FORWARD);
+        break;
+      case 'previousWord':
+        dir = Dir.BACKWARD;
+        didNavigate = true;
+        unit = cursors.Unit.WORD;
+        current = current.move(cursors.Unit.WORD, dir);
+        break;
+      case 'nativeNextWord':
+      case 'nativePreviousWord':
+        if (DesktopAutomationHandler.instance.textEditHandler) {
+          DesktopAutomationHandler.instance.textEditHandler
+              .injectInferredIntents([{
+                command: chrome.automation.IntentCommandType.MOVE_SELECTION,
+                textBoundary: command === 'nativeNextWord' ?
+                    chrome.automation.IntentTextBoundaryType.WORD_END :
+                    chrome.automation.IntentTextBoundaryType.WORD_START
+              }]);
         }
-
-        if (EventSourceState.get() === EventSourceType.TOUCH_GESTURE &&
-            actionNode.state.editable) {
-          // Dispatch a click to ensure the VK gets shown.
-          const location = actionNode.location;
-          EventGenerator.sendMouseClick(
-              location.left + Math.round(location.width / 2),
-              location.top + Math.round(location.height / 2));
-          return false;
+        return true;
+      case 'forward':
+      case 'nextLine':
+        didNavigate = true;
+        unit = cursors.Unit.LINE;
+        current = current.move(cursors.Unit.LINE, Dir.FORWARD);
+        break;
+      case 'backward':
+      case 'previousLine':
+        dir = Dir.BACKWARD;
+        didNavigate = true;
+        unit = cursors.Unit.LINE;
+        current = current.move(cursors.Unit.LINE, dir);
+        break;
+      case 'nextButton':
+        dir = Dir.FORWARD;
+        pred = AutomationPredicate.button;
+        predErrorMsg = 'no_next_button';
+        break;
+      case 'previousButton':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.button;
+        predErrorMsg = 'no_previous_button';
+        break;
+      case 'nextCheckbox':
+        pred = AutomationPredicate.checkBox;
+        predErrorMsg = 'no_next_checkbox';
+        break;
+      case 'previousCheckbox':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.checkBox;
+        predErrorMsg = 'no_previous_checkbox';
+        break;
+      case 'nextComboBox':
+        pred = AutomationPredicate.comboBox;
+        predErrorMsg = 'no_next_combo_box';
+        break;
+      case 'previousComboBox':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.comboBox;
+        predErrorMsg = 'no_previous_combo_box';
+        break;
+      case 'nextEditText':
+        skipSettingSelection = true;
+        pred = AutomationPredicate.editText;
+        predErrorMsg = 'no_next_edit_text';
+        this.smartStickyMode_.startIgnoringRangeChanges();
+        break;
+      case 'previousEditText':
+        skipSettingSelection = true;
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.editText;
+        predErrorMsg = 'no_previous_edit_text';
+        this.smartStickyMode_.startIgnoringRangeChanges();
+        break;
+      case 'nextFormField':
+        skipSettingSelection = true;
+        pred = AutomationPredicate.formField;
+        predErrorMsg = 'no_next_form_field';
+        this.smartStickyMode_.startIgnoringRangeChanges();
+        break;
+      case 'previousFormField':
+        skipSettingSelection = true;
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.formField;
+        predErrorMsg = 'no_previous_form_field';
+        this.smartStickyMode_.startIgnoringRangeChanges();
+        break;
+      case 'previousGraphic':
+        skipSettingSelection = true;
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.image;
+        predErrorMsg = 'no_previous_graphic';
+        break;
+      case 'nextGraphic':
+        skipSettingSelection = true;
+        pred = AutomationPredicate.image;
+        predErrorMsg = 'no_next_graphic';
+        break;
+      case 'nextHeading':
+        pred = AutomationPredicate.heading;
+        predErrorMsg = 'no_next_heading';
+        break;
+      case 'nextHeading1':
+        pred = AutomationPredicate.makeHeadingPredicate(1);
+        predErrorMsg = 'no_next_heading_1';
+        break;
+      case 'nextHeading2':
+        pred = AutomationPredicate.makeHeadingPredicate(2);
+        predErrorMsg = 'no_next_heading_2';
+        break;
+      case 'nextHeading3':
+        pred = AutomationPredicate.makeHeadingPredicate(3);
+        predErrorMsg = 'no_next_heading_3';
+        break;
+      case 'nextHeading4':
+        pred = AutomationPredicate.makeHeadingPredicate(4);
+        predErrorMsg = 'no_next_heading_4';
+        break;
+      case 'nextHeading5':
+        pred = AutomationPredicate.makeHeadingPredicate(5);
+        predErrorMsg = 'no_next_heading_5';
+        break;
+      case 'nextHeading6':
+        pred = AutomationPredicate.makeHeadingPredicate(6);
+        predErrorMsg = 'no_next_heading_6';
+        break;
+      case 'previousHeading':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.heading;
+        predErrorMsg = 'no_previous_heading';
+        break;
+      case 'previousHeading1':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.makeHeadingPredicate(1);
+        predErrorMsg = 'no_previous_heading_1';
+        break;
+      case 'previousHeading2':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.makeHeadingPredicate(2);
+        predErrorMsg = 'no_previous_heading_2';
+        break;
+      case 'previousHeading3':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.makeHeadingPredicate(3);
+        predErrorMsg = 'no_previous_heading_3';
+        break;
+      case 'previousHeading4':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.makeHeadingPredicate(4);
+        predErrorMsg = 'no_previous_heading_4';
+        break;
+      case 'previousHeading5':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.makeHeadingPredicate(5);
+        predErrorMsg = 'no_previous_heading_5';
+        break;
+      case 'previousHeading6':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.makeHeadingPredicate(6);
+        predErrorMsg = 'no_previous_heading_6';
+        break;
+      case 'nextLink':
+        pred = AutomationPredicate.link;
+        predErrorMsg = 'no_next_link';
+        break;
+      case 'previousLink':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.link;
+        predErrorMsg = 'no_previous_link';
+        break;
+      case 'nextTable':
+        pred = AutomationPredicate.table;
+        predErrorMsg = 'no_next_table';
+        break;
+      case 'previousTable':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.table;
+        predErrorMsg = 'no_previous_table';
+        break;
+      case 'nextVisitedLink':
+        pred = AutomationPredicate.visitedLink;
+        predErrorMsg = 'no_next_visited_link';
+        break;
+      case 'previousVisitedLink':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.visitedLink;
+        predErrorMsg = 'no_previous_visited_link';
+        break;
+      case 'nextLandmark':
+        pred = AutomationPredicate.landmark;
+        predErrorMsg = 'no_next_landmark';
+        break;
+      case 'previousLandmark':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.landmark;
+        predErrorMsg = 'no_previous_landmark';
+        break;
+      case 'left':
+      case 'previousObject':
+        skipSettingSelection = true;
+        dir = Dir.BACKWARD;
+        // Falls through.
+      case 'right':
+      case 'nextObject':
+        skipSettingSelection = true;
+        didNavigate = true;
+        unit = cursors.Unit.NODE;
+        current = current.move(cursors.Unit.NODE, dir);
+        current = this.skipLabelOrDescriptionFor(current, dir);
+        break;
+      case 'previousGroup':
+        skipSync = true;
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.group;
+        break;
+      case 'nextGroup':
+        skipSync = true;
+        pred = AutomationPredicate.group;
+        break;
+      case 'previousPage':
+      case 'nextPage':
+        const root = AutomationUtil.getTopLevelRoot(current.start.node);
+        if (root && root.scrollY !== undefined) {
+          let page = Math.ceil(root.scrollY / root.location.height) || 1;
+          page = command === 'nextPage' ? page + 1 : page - 1;
+          ChromeVox.tts.stop();
+          root.setScrollOffset(0, page * root.location.height);
         }
-
-        while (actionNode.role === RoleType.INLINE_TEXT_BOX ||
-               actionNode.role === RoleType.STATIC_TEXT) {
-          actionNode = actionNode.parent;
-        }
-        if (actionNode.inPageLinkTarget) {
-          ChromeVoxState.instance.navigateToRange(
-              cursors.Range.fromNode(actionNode.inPageLinkTarget));
-        } else {
-          actionNode.doDefault();
-        }
-      }
-      // Skip all other processing; if focus changes, we should get an event
-      // for that.
-      return false;
-    case 'jumpToDetails': {
-      while (node && !node.details) {
-        node = node.parent;
-      }
-      if (node && node.details.length) {
-        // TODO currently can only jump to first detail.
-        current = cursors.Range.fromNode(node.details[0]);
-      }
-    } break;
-    case 'readFromHere':
-      ChromeVoxState.isReadingContinuously = true;
-      const continueReading = function() {
-        if (!ChromeVoxState.isReadingContinuously ||
-            !ChromeVoxState.instance.currentRange) {
-          return;
-        }
-
-        const prevRange = ChromeVoxState.instance.currentRange;
-        const newRange = ChromeVoxState.instance.currentRange.move(
-            cursors.Unit.NODE, Dir.FORWARD);
-
-        // Stop if we've wrapped back to the document.
-        const maybeDoc = newRange.start.node;
-        if (AutomationPredicate.root(maybeDoc)) {
-          ChromeVoxState.isReadingContinuously = false;
-          return;
-        }
-
-        ChromeVoxState.instance.setCurrentRange(newRange);
-        newRange.select();
-
-        const o = new Output()
-                      .withoutHints()
-                      .withRichSpeechAndBraille(
-                          ChromeVoxState.instance.currentRange, prevRange,
-                          OutputEventType.NAVIGATE)
-                      .onSpeechEnd(continueReading);
-
-        if (!o.hasSpeech) {
-          continueReading();
-          return;
-        }
-
-        o.go();
-      }.bind(this);
-
-      {
-        const startNode = ChromeVoxState.instance.currentRange.start.node;
-        const collapsedRange = cursors.Range.fromNode(startNode);
-        const o =
-            new Output()
-                .withoutHints()
-                .withRichSpeechAndBraille(
-                    collapsedRange, collapsedRange, OutputEventType.NAVIGATE)
-                .onSpeechEnd(continueReading);
-
-        if (o.hasSpeech) {
-          o.go();
-        } else {
-          continueReading();
-        }
-      }
-      return false;
-    case 'contextMenu':
-      EventGenerator.sendKeyPress(KeyCode.APPS);
-      break;
-    case 'showHeadingsList':
-      (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_heading')).send();
-      return false;
-    case 'showFormsList':
-      (new PanelCommand(
-           PanelCommandType.OPEN_MENUS, 'panel_menu_form_controls'))
-          .send();
-      return false;
-    case 'showLandmarksList':
-      (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_landmark')).send();
-      return false;
-    case 'showLinksList':
-      (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_link')).send();
-      return false;
-    case 'showActionsMenu':
-      (new PanelCommand(PanelCommandType.OPEN_MENUS, 'panel_menu_actions'))
-          .send();
-      return false;
-    case 'showTablesList':
-      (new PanelCommand(PanelCommandType.OPEN_MENUS, 'table_strategy')).send();
-      return false;
-    case 'toggleSearchWidget':
-      (new PanelCommand(PanelCommandType.SEARCH)).send();
-      return false;
-    case 'readCurrentTitle': {
-      let target = ChromeVoxState.instance.currentRange.start.node;
-      const output = new Output();
-
-      if (!target) {
         return false;
-      }
+      case 'previousSimilarItem':
+        dir = Dir.BACKWARD;
+        // Falls through.
+      case 'nextSimilarItem': {
+        skipSync = true;
+        const originalNode = node;
 
-      let firstWindow;
-      let rootViewWindow;
-      if (target.root && target.root.role === RoleType.DESKTOP) {
-        // Search for the first container with a name.
-        while (target && (!target.name || !AutomationPredicate.root(target))) {
-          target = target.parent;
+        // Scan upwards until we get a role we don't want to ignore.
+        while (node && AutomationPredicate.ignoreDuringJump(node)) {
+          node = node.parent;
         }
-      } else {
-        // Search for a root window with a title.
-        while (target) {
-          const isNamedWindow =
-              !!target.name && target.role === RoleType.WINDOW;
-          const isRootView = target.className === 'RootView';
-          if (isNamedWindow && !firstWindow) {
-            firstWindow = target;
+
+        const useNode = node || originalNode;
+        pred = AutomationPredicate.roles([node.role]);
+      } break;
+      case 'previousInvalidItem': {
+        dir = Dir.BACKWARD;
+        rootPred = AutomationPredicate.root;
+        pred = AutomationPredicate.isInvalid;
+        predErrorMsg = 'no_invalid_item';
+      } break;
+      case 'nextInvalidItem': {
+        pred = AutomationPredicate.isInvalid;
+        rootPred = AutomationPredicate.root;
+        predErrorMsg = 'no_invalid_item';
+      } break;
+      case 'nextList':
+        pred = AutomationPredicate.makeListPredicate(current.start.node);
+        predErrorMsg = 'no_next_list';
+        break;
+      case 'previousList':
+        dir = Dir.BACKWARD;
+        pred = AutomationPredicate.makeListPredicate(current.start.node);
+        predErrorMsg = 'no_previous_list';
+        skipInitialAncestry = false;
+        break;
+      case 'jumpToTop': {
+        const node = AutomationUtil.findNodePost(
+            current.start.node.root, Dir.FORWARD, AutomationPredicate.object);
+        if (node) {
+          current = cursors.Range.fromNode(node);
+        }
+        tryScrolling = false;
+      } break;
+      case 'jumpToBottom': {
+        const node = AutomationUtil.findLastNode(
+            current.start.node.root, AutomationPredicate.object);
+        if (node) {
+          current = cursors.Range.fromNode(node);
+        }
+        tryScrolling = false;
+      } break;
+      case 'forceClickOnCurrentItem':
+        if (ChromeVoxState.instance.currentRange) {
+          let actionNode = ChromeVoxState.instance.currentRange.start.node;
+          // Scan for a clickable, which overrides the |actionNode|.
+          let clickable = actionNode;
+          while (clickable && !clickable.clickable &&
+                 actionNode.root === clickable.root) {
+            clickable = clickable.parent;
+          }
+          if (clickable && actionNode.root === clickable.root) {
+            clickable.doDefault();
+            return false;
           }
 
-          if (isNamedWindow && isRootView) {
-            rootViewWindow = target;
-            break;
+          if (EventSourceState.get() === EventSourceType.TOUCH_GESTURE &&
+              actionNode.state.editable) {
+            // Dispatch a click to ensure the VK gets shown.
+            const location = actionNode.location;
+            EventGenerator.sendMouseClick(
+                location.left + Math.round(location.width / 2),
+                location.top + Math.round(location.height / 2));
+            return false;
           }
-          target = target.parent;
+
+          while (actionNode.role === RoleType.INLINE_TEXT_BOX ||
+                 actionNode.role === RoleType.STATIC_TEXT) {
+            actionNode = actionNode.parent;
+          }
+          if (actionNode.inPageLinkTarget) {
+            ChromeVoxState.instance.navigateToRange(
+                cursors.Range.fromNode(actionNode.inPageLinkTarget));
+          } else {
+            actionNode.doDefault();
+          }
         }
-      }
+        // Skip all other processing; if focus changes, we should get an event
+        // for that.
+        return false;
+      case 'jumpToDetails': {
+        while (node && !node.details) {
+          node = node.parent;
+        }
+        if (node && node.details.length) {
+          // TODO currently can only jump to first detail.
+          current = cursors.Range.fromNode(node.details[0]);
+        }
+      } break;
+      case 'readFromHere':
+        ChromeVoxState.isReadingContinuously = true;
+        const continueReading = function() {
+          if (!ChromeVoxState.isReadingContinuously ||
+              !ChromeVoxState.instance.currentRange) {
+            return;
+          }
 
-      // Re-target with preference for the root.
-      target = rootViewWindow || firstWindow || target;
+          const prevRange = ChromeVoxState.instance.currentRange;
+          const newRange = ChromeVoxState.instance.currentRange.move(
+              cursors.Unit.NODE, Dir.FORWARD);
 
-      if (!target) {
-        output.format('@no_title');
-      } else {
-        output.withString(target.name);
-      }
+          // Stop if we've wrapped back to the document.
+          const maybeDoc = newRange.start.node;
+          if (AutomationPredicate.root(maybeDoc)) {
+            ChromeVoxState.isReadingContinuously = false;
+            return;
+          }
 
-      output.go();
-    }
-      return false;
-    case 'readCurrentURL':
-      const output = new Output();
-      const target = ChromeVoxState.instance.currentRange.start.node.root;
-      output.withString(target.docUrl || '').go();
-      return false;
-    case 'toggleSelection':
-      if (!ChromeVoxState.instance.pageSel_) {
-        ChromeVoxState.instance.pageSel_ = ChromeVoxState.instance.currentRange;
-        DesktopAutomationHandler.instance.ignoreDocumentSelectionFromAction(
-            true);
-      } else {
-        const root = ChromeVoxState.instance.currentRange.start.node.root;
-        if (root && root.selectionStartObject && root.selectionEndObject) {
-          const sel = new cursors.Range(
-              new cursors.Cursor(
-                  root.selectionStartObject, root.selectionStartOffset),
-              new cursors.Cursor(
-                  root.selectionEndObject, root.selectionEndOffset));
+          ChromeVoxState.instance.setCurrentRange(newRange);
+          newRange.select();
+
+          const o = new Output()
+                        .withoutHints()
+                        .withRichSpeechAndBraille(
+                            ChromeVoxState.instance.currentRange, prevRange,
+                            OutputEventType.NAVIGATE)
+                        .onSpeechEnd(continueReading);
+
+          if (!o.hasSpeech) {
+            continueReading();
+            return;
+          }
+
+          o.go();
+        }.bind(this);
+
+        {
+          const startNode = ChromeVoxState.instance.currentRange.start.node;
+          const collapsedRange = cursors.Range.fromNode(startNode);
           const o =
               new Output()
-                  .format('@end_selection')
-                  .withSpeechAndBraille(sel, sel, OutputEventType.NAVIGATE)
-                  .go();
-          DesktopAutomationHandler.instance.ignoreDocumentSelectionFromAction(
-              false);
-        }
-        ChromeVoxState.instance.pageSel_ = null;
-        return false;
-      }
-      break;
-    case 'fullyDescribe':
-      const o = new Output();
-      o.withContextFirst()
-          .withRichSpeechAndBraille(current, null, OutputEventType.NAVIGATE)
-          .go();
-      return false;
-    case 'viewGraphicAsBraille':
-      CommandHandler.viewGraphicAsBraille_(current);
-      return false;
-    // Table commands.
-    case 'previousRow': {
-      skipSync = true;
-      dir = Dir.BACKWARD;
-      const tableOpts = {row: true, dir};
-      pred = AutomationPredicate.makeTableCellPredicate(
-          current.start.node, tableOpts);
-      predErrorMsg = 'no_cell_above';
-      rootPred = AutomationPredicate.table;
-      shouldWrap = false;
-    } break;
-    case 'previousCol': {
-      skipSync = true;
-      dir = Dir.BACKWARD;
-      const tableOpts = {col: true, dir};
-      pred = AutomationPredicate.makeTableCellPredicate(
-          current.start.node, tableOpts);
-      predErrorMsg = 'no_cell_left';
-      rootPred = AutomationPredicate.row;
-      shouldWrap = false;
-    } break;
-    case 'nextRow': {
-      skipSync = true;
-      const tableOpts = {row: true, dir};
-      pred = AutomationPredicate.makeTableCellPredicate(
-          current.start.node, tableOpts);
-      predErrorMsg = 'no_cell_below';
-      rootPred = AutomationPredicate.table;
-      shouldWrap = false;
-    } break;
-    case 'nextCol': {
-      skipSync = true;
-      const tableOpts = {col: true, dir};
-      pred = AutomationPredicate.makeTableCellPredicate(
-          current.start.node, tableOpts);
-      predErrorMsg = 'no_cell_right';
-      rootPred = AutomationPredicate.row;
-      shouldWrap = false;
-    } break;
-    case 'goToRowFirstCell':
-    case 'goToRowLastCell': {
-      skipSync = true;
-      while (node && node.role !== RoleType.ROW) {
-        node = node.parent;
-      }
-      if (!node) {
-        break;
-      }
-      const end = AutomationUtil.findNodePost(
-          node, command === 'goToRowLastCell' ? Dir.BACKWARD : Dir.FORWARD,
-          AutomationPredicate.leaf);
-      if (end) {
-        current = cursors.Range.fromNode(end);
-      }
-    } break;
-    case 'goToColFirstCell': {
-      skipSync = true;
-      while (node && node.role !== RoleType.TABLE) {
-        node = node.parent;
-      }
-      if (!node || !node.firstChild) {
-        return false;
-      }
-      const tableOpts = {col: true, dir, end: true};
-      pred = AutomationPredicate.makeTableCellPredicate(
-          current.start.node, tableOpts);
-      current = cursors.Range.fromNode(node.firstChild);
-      // Should not be outputted.
-      predErrorMsg = 'no_cell_above';
-      rootPred = AutomationPredicate.table;
-      shouldWrap = false;
-    } break;
-    case 'goToColLastCell': {
-      skipSync = true;
-      dir = Dir.BACKWARD;
-      while (node && node.role !== RoleType.TABLE) {
-        node = node.parent;
-      }
-      if (!node || !node.lastChild) {
-        return false;
-      }
-      const tableOpts = {col: true, dir, end: true};
-      pred = AutomationPredicate.makeTableCellPredicate(
-          current.start.node, tableOpts);
+                  .withoutHints()
+                  .withRichSpeechAndBraille(
+                      collapsedRange, collapsedRange, OutputEventType.NAVIGATE)
+                  .onSpeechEnd(continueReading);
 
-      // Try to start on the last cell of the table and allow
-      // matching that node.
-      let startNode = node.lastChild;
-      while (startNode.lastChild &&
-             !AutomationPredicate.cellLike(startNode.role)) {
-        startNode = startNode.lastChild;
-      }
-      current = cursors.Range.fromNode(startNode);
-      matchCurrent = true;
-
-      // Should not be outputted.
-      predErrorMsg = 'no_cell_below';
-      rootPred = AutomationPredicate.table;
-      shouldWrap = false;
-    } break;
-    case 'goToFirstCell':
-    case 'goToLastCell': {
-      skipSync = true;
-      while (node && node.role !== RoleType.TABLE) {
-        node = node.parent;
-      }
-      if (!node) {
-        break;
-      }
-      const end = AutomationUtil.findNodePost(
-          node, command === 'goToLastCell' ? Dir.BACKWARD : Dir.FORWARD,
-          AutomationPredicate.leaf);
-      if (end) {
-        current = cursors.Range.fromNode(end);
-      }
-    } break;
-
-    // These commands are only available when invoked from touch.
-    case 'nextAtGranularity':
-    case 'previousAtGranularity':
-      const backwards = command === 'previousAtGranularity';
-      switch (GestureCommandHandler.granularity) {
-        case GestureGranularity.CHARACTER:
-          command = backwards ? 'previousCharacter' : 'nextCharacter';
-          break;
-        case GestureGranularity.WORD:
-          command = backwards ? 'previousWord' : 'nextWord';
-          break;
-        case GestureGranularity.LINE:
-          command = backwards ? 'previousLine' : 'nextLine';
-          break;
-        case GestureGranularity.HEADING:
-          command = backwards ? 'previousHeading' : 'nextHeading';
-          break;
-        case GestureGranularity.LINK:
-          command = backwards ? 'previousLink' : 'nextLink';
-          break;
-        case GestureGranularity.FORM_FIELD_CONTROL:
-          command = backwards ? 'previousFormField' : 'nextFormField';
-          break;
-      }
-      CommandHandler.onCommand(command);
-      return false;
-    case 'announceRichTextDescription': {
-      const optSubs = [];
-      node.fontSize ? optSubs.push('font size: ' + node.fontSize) :
-                      optSubs.push('');
-      node.color ? optSubs.push(Color.getColorDescription(node.color)) :
-                   optSubs.push('');
-      node.bold ? optSubs.push(Msgs.getMsg('bold')) : optSubs.push('');
-      node.italic ? optSubs.push(Msgs.getMsg('italic')) : optSubs.push('');
-      node.underline ? optSubs.push(Msgs.getMsg('underline')) :
-                       optSubs.push('');
-      node.lineThrough ? optSubs.push(Msgs.getMsg('linethrough')) :
-                         optSubs.push('');
-      node.fontFamily ? optSubs.push('font family: ' + node.fontFamily) :
-                        optSubs.push('');
-
-      const richTextDescription = Msgs.getMsg('rich_text_attributes', optSubs);
-      new Output()
-          .withString(richTextDescription)
-          .withQueueMode(QueueMode.CATEGORY_FLUSH)
-          .go();
-    }
-      return false;
-    case 'readPhoneticPronunciation': {
-      // Get node info.
-      const index = ChromeVoxState.instance.currentRange.start.index;
-      const name = node.name;
-      // If there is no text to speak, inform the user and return early.
-      if (!name) {
-        new Output()
-            .withString(Msgs.getMsg('empty_name'))
-            .withQueueMode(QueueMode.CATEGORY_FLUSH)
-            .go();
-        return false;
-      }
-
-      // Get word start and end indices.
-      let wordStarts, wordEnds;
-      if (node.role === RoleType.INLINE_TEXT_BOX) {
-        wordStarts = node.wordStarts;
-        wordEnds = node.wordEnds;
-      } else {
-        wordStarts = node.nonInlineTextWordStarts;
-        wordEnds = node.nonInlineTextWordEnds;
-      }
-      // Find the word we want to speak phonetically. If index === -1, then the
-      // index represents an entire node.
-      let text = '';
-      if (index === -1) {
-        text = name;
-      } else {
-        for (let z = 0; z < wordStarts.length; ++z) {
-          if (wordStarts[z] <= index && wordEnds[z] > index) {
-            text = name.substring(wordStarts[z], wordEnds[z]);
-            break;
+          if (o.hasSpeech) {
+            o.go();
+          } else {
+            continueReading();
           }
         }
-      }
+        return false;
+      case 'contextMenu':
+        EventGenerator.sendKeyPress(KeyCode.APPS);
+        break;
+      case 'showHeadingsList':
+        (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_heading')).send();
+        return false;
+      case 'showFormsList':
+        (new PanelCommand(
+             PanelCommandType.OPEN_MENUS, 'panel_menu_form_controls'))
+            .send();
+        return false;
+      case 'showLandmarksList':
+        (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_landmark')).send();
+        return false;
+      case 'showLinksList':
+        (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_link')).send();
+        return false;
+      case 'showActionsMenu':
+        (new PanelCommand(PanelCommandType.OPEN_MENUS, 'panel_menu_actions'))
+            .send();
+        return false;
+      case 'showTablesList':
+        (new PanelCommand(PanelCommandType.OPEN_MENUS, 'table_strategy'))
+            .send();
+        return false;
+      case 'toggleSearchWidget':
+        (new PanelCommand(PanelCommandType.SEARCH)).send();
+        return false;
+      case 'readCurrentTitle': {
+        let target = ChromeVoxState.instance.currentRange.start.node;
+        const output = new Output();
 
-      const language = chrome.i18n.getUILanguage();
-      const phoneticText = PhoneticData.forText(text, language);
-      if (phoneticText) {
+        if (!target) {
+          return false;
+        }
+
+        let firstWindow;
+        let rootViewWindow;
+        if (target.root && target.root.role === RoleType.DESKTOP) {
+          // Search for the first container with a name.
+          while (target &&
+                 (!target.name || !AutomationPredicate.root(target))) {
+            target = target.parent;
+          }
+        } else {
+          // Search for a root window with a title.
+          while (target) {
+            const isNamedWindow =
+                !!target.name && target.role === RoleType.WINDOW;
+            const isRootView = target.className === 'RootView';
+            if (isNamedWindow && !firstWindow) {
+              firstWindow = target;
+            }
+
+            if (isNamedWindow && isRootView) {
+              rootViewWindow = target;
+              break;
+            }
+            target = target.parent;
+          }
+        }
+
+        // Re-target with preference for the root.
+        target = rootViewWindow || firstWindow || target;
+
+        if (!target) {
+          output.format('@no_title');
+        } else {
+          output.withString(target.name);
+        }
+
+        output.go();
+      }
+        return false;
+      case 'readCurrentURL':
+        const output = new Output();
+        const target = ChromeVoxState.instance.currentRange.start.node.root;
+        output.withString(target.docUrl || '').go();
+        return false;
+      case 'toggleSelection':
+        if (!ChromeVoxState.instance.pageSel_) {
+          ChromeVoxState.instance.pageSel_ =
+              ChromeVoxState.instance.currentRange;
+          DesktopAutomationHandler.instance.ignoreDocumentSelectionFromAction(
+              true);
+        } else {
+          const root = ChromeVoxState.instance.currentRange.start.node.root;
+          if (root && root.selectionStartObject && root.selectionEndObject) {
+            const sel = new cursors.Range(
+                new cursors.Cursor(
+                    root.selectionStartObject, root.selectionStartOffset),
+                new cursors.Cursor(
+                    root.selectionEndObject, root.selectionEndOffset));
+            const o =
+                new Output()
+                    .format('@end_selection')
+                    .withSpeechAndBraille(sel, sel, OutputEventType.NAVIGATE)
+                    .go();
+            DesktopAutomationHandler.instance.ignoreDocumentSelectionFromAction(
+                false);
+          }
+          ChromeVoxState.instance.pageSel_ = null;
+          return false;
+        }
+        break;
+      case 'fullyDescribe':
+        const o = new Output();
+        o.withContextFirst()
+            .withRichSpeechAndBraille(current, null, OutputEventType.NAVIGATE)
+            .go();
+        return false;
+      case 'viewGraphicAsBraille':
+        this.viewGraphicAsBraille_(current);
+        return false;
+      // Table commands.
+      case 'previousRow': {
+        skipSync = true;
+        dir = Dir.BACKWARD;
+        const tableOpts = {row: true, dir};
+        pred = AutomationPredicate.makeTableCellPredicate(
+            current.start.node, tableOpts);
+        predErrorMsg = 'no_cell_above';
+        rootPred = AutomationPredicate.table;
+        shouldWrap = false;
+      } break;
+      case 'previousCol': {
+        skipSync = true;
+        dir = Dir.BACKWARD;
+        const tableOpts = {col: true, dir};
+        pred = AutomationPredicate.makeTableCellPredicate(
+            current.start.node, tableOpts);
+        predErrorMsg = 'no_cell_left';
+        rootPred = AutomationPredicate.row;
+        shouldWrap = false;
+      } break;
+      case 'nextRow': {
+        skipSync = true;
+        const tableOpts = {row: true, dir};
+        pred = AutomationPredicate.makeTableCellPredicate(
+            current.start.node, tableOpts);
+        predErrorMsg = 'no_cell_below';
+        rootPred = AutomationPredicate.table;
+        shouldWrap = false;
+      } break;
+      case 'nextCol': {
+        skipSync = true;
+        const tableOpts = {col: true, dir};
+        pred = AutomationPredicate.makeTableCellPredicate(
+            current.start.node, tableOpts);
+        predErrorMsg = 'no_cell_right';
+        rootPred = AutomationPredicate.row;
+        shouldWrap = false;
+      } break;
+      case 'goToRowFirstCell':
+      case 'goToRowLastCell': {
+        skipSync = true;
+        while (node && node.role !== RoleType.ROW) {
+          node = node.parent;
+        }
+        if (!node) {
+          break;
+        }
+        const end = AutomationUtil.findNodePost(
+            node, command === 'goToRowLastCell' ? Dir.BACKWARD : Dir.FORWARD,
+            AutomationPredicate.leaf);
+        if (end) {
+          current = cursors.Range.fromNode(end);
+        }
+      } break;
+      case 'goToColFirstCell': {
+        skipSync = true;
+        while (node && node.role !== RoleType.TABLE) {
+          node = node.parent;
+        }
+        if (!node || !node.firstChild) {
+          return false;
+        }
+        const tableOpts = {col: true, dir, end: true};
+        pred = AutomationPredicate.makeTableCellPredicate(
+            current.start.node, tableOpts);
+        current = cursors.Range.fromNode(node.firstChild);
+        // Should not be outputted.
+        predErrorMsg = 'no_cell_above';
+        rootPred = AutomationPredicate.table;
+        shouldWrap = false;
+      } break;
+      case 'goToColLastCell': {
+        skipSync = true;
+        dir = Dir.BACKWARD;
+        while (node && node.role !== RoleType.TABLE) {
+          node = node.parent;
+        }
+        if (!node || !node.lastChild) {
+          return false;
+        }
+        const tableOpts = {col: true, dir, end: true};
+        pred = AutomationPredicate.makeTableCellPredicate(
+            current.start.node, tableOpts);
+
+        // Try to start on the last cell of the table and allow
+        // matching that node.
+        let startNode = node.lastChild;
+        while (startNode.lastChild &&
+               !AutomationPredicate.cellLike(startNode.role)) {
+          startNode = startNode.lastChild;
+        }
+        current = cursors.Range.fromNode(startNode);
+        matchCurrent = true;
+
+        // Should not be outputted.
+        predErrorMsg = 'no_cell_below';
+        rootPred = AutomationPredicate.table;
+        shouldWrap = false;
+      } break;
+      case 'goToFirstCell':
+      case 'goToLastCell': {
+        skipSync = true;
+        while (node && node.role !== RoleType.TABLE) {
+          node = node.parent;
+        }
+        if (!node) {
+          break;
+        }
+        const end = AutomationUtil.findNodePost(
+            node, command === 'goToLastCell' ? Dir.BACKWARD : Dir.FORWARD,
+            AutomationPredicate.leaf);
+        if (end) {
+          current = cursors.Range.fromNode(end);
+        }
+      } break;
+
+      // These commands are only available when invoked from touch.
+      case 'nextAtGranularity':
+      case 'previousAtGranularity':
+        const backwards = command === 'previousAtGranularity';
+        switch (GestureCommandHandler.granularity) {
+          case GestureGranularity.CHARACTER:
+            command = backwards ? 'previousCharacter' : 'nextCharacter';
+            break;
+          case GestureGranularity.WORD:
+            command = backwards ? 'previousWord' : 'nextWord';
+            break;
+          case GestureGranularity.LINE:
+            command = backwards ? 'previousLine' : 'nextLine';
+            break;
+          case GestureGranularity.HEADING:
+            command = backwards ? 'previousHeading' : 'nextHeading';
+            break;
+          case GestureGranularity.LINK:
+            command = backwards ? 'previousLink' : 'nextLink';
+            break;
+          case GestureGranularity.FORM_FIELD_CONTROL:
+            command = backwards ? 'previousFormField' : 'nextFormField';
+            break;
+        }
+        this.onCommand(command);
+        return false;
+      case 'announceRichTextDescription': {
+        const optSubs = [];
+        node.fontSize ? optSubs.push('font size: ' + node.fontSize) :
+                        optSubs.push('');
+        node.color ? optSubs.push(Color.getColorDescription(node.color)) :
+                     optSubs.push('');
+        node.bold ? optSubs.push(Msgs.getMsg('bold')) : optSubs.push('');
+        node.italic ? optSubs.push(Msgs.getMsg('italic')) : optSubs.push('');
+        node.underline ? optSubs.push(Msgs.getMsg('underline')) :
+                         optSubs.push('');
+        node.lineThrough ? optSubs.push(Msgs.getMsg('linethrough')) :
+                           optSubs.push('');
+        node.fontFamily ? optSubs.push('font family: ' + node.fontFamily) :
+                          optSubs.push('');
+
+        const richTextDescription =
+            Msgs.getMsg('rich_text_attributes', optSubs);
         new Output()
-            .withString(phoneticText)
+            .withString(richTextDescription)
             .withQueueMode(QueueMode.CATEGORY_FLUSH)
             .go();
       }
-    }
-      return false;
-    case 'readLinkURL': {
-      const rootNode = node.root;
-      while (node && !node.url) {
-        // URL could be an ancestor of current range.
-        node = node.parent;
-      }
-      // Announce node's URL if it's not the root node; we don't want to
-      // announce the URL of the current page.
-      const url = (node && node !== rootNode) ? node.url : '';
-      new Output()
-          .withString(
-              url ? Msgs.getMsg('url_behind_link', [url]) :
-                    Msgs.getMsg('no_url_found'))
-          .withQueueMode(QueueMode.CATEGORY_FLUSH)
-          .go();
-    }
-      return false;
-    case 'logLanguageInformationForCurrentNode': {
-      if (!CommandHandler.languageLoggingEnabled_) {
         return false;
-      }
+      case 'readPhoneticPronunciation': {
+        // Get node info.
+        const index = ChromeVoxState.instance.currentRange.start.index;
+        const name = node.name;
+        // If there is no text to speak, inform the user and return early.
+        if (!name) {
+          new Output()
+              .withString(Msgs.getMsg('empty_name'))
+              .withQueueMode(QueueMode.CATEGORY_FLUSH)
+              .go();
+          return false;
+        }
 
-      const outString = `
+        // Get word start and end indices.
+        let wordStarts, wordEnds;
+        if (node.role === RoleType.INLINE_TEXT_BOX) {
+          wordStarts = node.wordStarts;
+          wordEnds = node.wordEnds;
+        } else {
+          wordStarts = node.nonInlineTextWordStarts;
+          wordEnds = node.nonInlineTextWordEnds;
+        }
+        // Find the word we want to speak phonetically. If index === -1, then
+        // the index represents an entire node.
+        let text = '';
+        if (index === -1) {
+          text = name;
+        } else {
+          for (let z = 0; z < wordStarts.length; ++z) {
+            if (wordStarts[z] <= index && wordEnds[z] > index) {
+              text = name.substring(wordStarts[z], wordEnds[z]);
+              break;
+            }
+          }
+        }
+
+        const language = chrome.i18n.getUILanguage();
+        const phoneticText = PhoneticData.forText(text, language);
+        if (phoneticText) {
+          new Output()
+              .withString(phoneticText)
+              .withQueueMode(QueueMode.CATEGORY_FLUSH)
+              .go();
+        }
+      }
+        return false;
+      case 'readLinkURL': {
+        const rootNode = node.root;
+        while (node && !node.url) {
+          // URL could be an ancestor of current range.
+          node = node.parent;
+        }
+        // Announce node's URL if it's not the root node; we don't want to
+        // announce the URL of the current page.
+        const url = (node && node !== rootNode) ? node.url : '';
+        new Output()
+            .withString(
+                url ? Msgs.getMsg('url_behind_link', [url]) :
+                      Msgs.getMsg('no_url_found'))
+            .withQueueMode(QueueMode.CATEGORY_FLUSH)
+            .go();
+      }
+        return false;
+      case 'logLanguageInformationForCurrentNode': {
+        if (!this.languageLoggingEnabled_) {
+          return false;
+        }
+
+        const outString = `
       Language information for node
       Name: ${node.name}
       Detected language: ${node.detectedLanguage || 'None'}
       Author language: ${node.language || 'None'}
       `;
-      new Output()
-          .withString(outString)
-          .withQueueMode(QueueMode.CATEGORY_FLUSH)
-          .go();
-      const annotation = node.languageAnnotationForStringAttribute('name');
-      const logString = outString.concat(`Language spans:
+        new Output()
+            .withString(outString)
+            .withQueueMode(QueueMode.CATEGORY_FLUSH)
+            .go();
+        const annotation = node.languageAnnotationForStringAttribute('name');
+        const logString = outString.concat(`Language spans:
         ${JSON.stringify(annotation)}`);
-      console.error(logString);
-      LogStore.getInstance().writeTextLog(logString, LogStore.LogType.TEXT);
+        console.error(logString);
+        LogStore.getInstance().writeTextLog(logString, LogStore.LogType.TEXT);
+      }
+        return false;
+      default:
+        return true;
     }
-      return false;
-    default:
-      return true;
-  }
 
-  if (didNavigate) {
-    chrome.metricsPrivate.recordUserAction('Accessibility.ChromeVox.Navigate');
-  }
+    if (didNavigate) {
+      chrome.metricsPrivate.recordUserAction(
+          'Accessibility.ChromeVox.Navigate');
+    }
 
-  if (pred) {
-    chrome.metricsPrivate.recordUserAction('Accessibility.ChromeVox.Jump');
+    if (pred) {
+      chrome.metricsPrivate.recordUserAction('Accessibility.ChromeVox.Jump');
 
-    let bound = current.getBound(dir).node;
-    if (bound) {
-      let node = null;
+      let bound = current.getBound(dir).node;
+      if (bound) {
+        let node = null;
 
-      if (matchCurrent && pred(bound)) {
-        node = bound;
-      }
-
-      if (!node) {
-        node = AutomationUtil.findNextNode(
-            bound, dir, pred, {skipInitialAncestry, root: rootPred});
-      }
-
-      if (node && !skipSync) {
-        node = AutomationUtil.findNodePre(
-                   node, Dir.FORWARD, AutomationPredicate.object) ||
-            node;
-      }
-
-      if (node) {
-        current = cursors.Range.fromNode(node);
-      } else {
-        ChromeVox.earcons.playEarcon(Earcon.WRAP);
-        if (!shouldWrap) {
-          if (predErrorMsg) {
-            new Output()
-                .withString(Msgs.getMsg(predErrorMsg))
-                .withQueueMode(QueueMode.FLUSH)
-                .go();
-          }
-          CommandHandler.onFinishCommand();
-          return false;
+        if (matchCurrent && pred(bound)) {
+          node = bound;
         }
 
-        let root = bound;
-        while (root && !AutomationPredicate.rootOrEditableRoot(root)) {
-          root = root.parent;
+        if (!node) {
+          node = AutomationUtil.findNextNode(
+              bound, dir, pred, {skipInitialAncestry, root: rootPred});
         }
 
-        if (!root) {
-          root = bound.root;
-        }
-
-        if (dir === Dir.FORWARD) {
-          bound = root;
-        } else {
-          bound = AutomationUtil.findNodePost(
-                      root, dir, AutomationPredicate.leaf) ||
-              bound;
-        }
-        node = AutomationUtil.findNextNode(bound, dir, pred, {root: rootPred});
-
         if (node && !skipSync) {
           node = AutomationUtil.findNodePre(
                      node, Dir.FORWARD, AutomationPredicate.object) ||
@@ -1247,289 +1205,315 @@
 
         if (node) {
           current = cursors.Range.fromNode(node);
-        } else if (predErrorMsg) {
-          new Output()
-              .withString(Msgs.getMsg(predErrorMsg))
-              .withQueueMode(QueueMode.FLUSH)
-              .go();
-          CommandHandler.onFinishCommand();
-          return false;
+        } else {
+          ChromeVox.earcons.playEarcon(Earcon.WRAP);
+          if (!shouldWrap) {
+            if (predErrorMsg) {
+              new Output()
+                  .withString(Msgs.getMsg(predErrorMsg))
+                  .withQueueMode(QueueMode.FLUSH)
+                  .go();
+            }
+            this.onFinishCommand();
+            return false;
+          }
+
+          let root = bound;
+          while (root && !AutomationPredicate.rootOrEditableRoot(root)) {
+            root = root.parent;
+          }
+
+          if (!root) {
+            root = bound.root;
+          }
+
+          if (dir === Dir.FORWARD) {
+            bound = root;
+          } else {
+            bound = AutomationUtil.findNodePost(
+                        root, dir, AutomationPredicate.leaf) ||
+                bound;
+          }
+          node =
+              AutomationUtil.findNextNode(bound, dir, pred, {root: rootPred});
+
+          if (node && !skipSync) {
+            node = AutomationUtil.findNodePre(
+                       node, Dir.FORWARD, AutomationPredicate.object) ||
+                node;
+          }
+
+          if (node) {
+            current = cursors.Range.fromNode(node);
+          } else if (predErrorMsg) {
+            new Output()
+                .withString(Msgs.getMsg(predErrorMsg))
+                .withQueueMode(QueueMode.FLUSH)
+                .go();
+            this.onFinishCommand();
+            return false;
+          }
         }
       }
     }
-  }
 
-  if (tryScrolling &&
-      !AutoScrollHandler.getInstance().onCommandNavigation(
-          current, dir, pred, unit, speechProps, rootPred, () => {
-            CommandHandler.onCommand(command);
-            CommandHandler.onFinishCommand();
-          })) {
-    CommandHandler.onFinishCommand();
+    if (tryScrolling &&
+        !AutoScrollHandler.getInstance().onCommandNavigation(
+            current, dir, pred, unit, speechProps, rootPred, () => {
+              this.onCommand(command);
+              this.onFinishCommand();
+            })) {
+      this.onFinishCommand();
+      return false;
+    }
+
+    if (current) {
+      if (current.wrapped) {
+        ChromeVox.earcons.playEarcon(Earcon.WRAP);
+      }
+
+      ChromeVoxState.instance.navigateToRange(
+          current, undefined, speechProps, skipSettingSelection);
+    }
+
+    this.onFinishCommand();
     return false;
   }
 
-  if (current) {
-    if (current.wrapped) {
-      ChromeVox.earcons.playEarcon(Earcon.WRAP);
+  /**
+   * Finishes processing of a command.
+   */
+  onFinishCommand() {
+    this.smartStickyMode_.stopIgnoringRangeChanges();
+  }
+
+  /**
+   * Increase or decrease a speech property and make an announcement.
+   * @param {string} propertyName The name of the property to change.
+   * @param {boolean} increase If true, increases the property value by one
+   *     step size, otherwise decreases.
+   * @private
+   */
+  increaseOrDecreaseSpeechProperty_(propertyName, increase) {
+    ChromeVox.tts.increaseOrDecreaseProperty(propertyName, increase);
+  }
+
+  /**
+   * Called when an image frame is received on a node.
+   * @param {!(AutomationEvent|CustomAutomationEvent)} event The event.
+   * @private
+   */
+  onImageFrameUpdated_(event) {
+    const target = event.target;
+    if (target !== this.imageNode_) {
+      return;
     }
 
-    ChromeVoxState.instance.navigateToRange(
-        current, undefined, speechProps, skipSettingSelection);
+    if (!AutomationUtil.isDescendantOf(
+            ChromeVoxState.instance.currentRange.start.node, this.imageNode_)) {
+      this.imageNode_.removeEventListener(
+          EventType.IMAGE_FRAME_UPDATED, this.onImageFrameUpdated_, false);
+      this.imageNode_ = null;
+      return;
+    }
+
+    if (target.imageDataUrl) {
+      ChromeVox.braille.writeRawImage(target.imageDataUrl);
+      ChromeVox.braille.freeze();
+    }
   }
 
-  CommandHandler.onFinishCommand();
-  return false;
-};
+  /**
+   * Handle the command to view the first graphic within the current range
+   * as braille.
+   * @param {!cursors.Range} current The current range.
+   * @private
+   */
+  viewGraphicAsBraille_(current) {
+    if (this.imageNode_) {
+      this.imageNode_.removeEventListener(
+          EventType.IMAGE_FRAME_UPDATED, this.onImageFrameUpdated_, false);
+      this.imageNode_ = null;
+    }
 
-/**
- * Finishes processing of a command.
- */
-CommandHandler.onFinishCommand = function() {
-  CommandHandler.smartStickyMode_.stopIgnoringRangeChanges();
-};
+    // Find the first node within the current range that supports image data.
+    const imageNode = AutomationUtil.findNodePost(
+        current.start.node, Dir.FORWARD, AutomationPredicate.supportsImageData);
+    if (!imageNode) {
+      return;
+    }
 
-/**
- * Increase or decrease a speech property and make an announcement.
- * @param {string} propertyName The name of the property to change.
- * @param {boolean} increase If true, increases the property value by one
- *     step size, otherwise decreases.
- * @private
- */
-CommandHandler.increaseOrDecreaseSpeechProperty_ = function(
-    propertyName, increase) {
-  ChromeVox.tts.increaseOrDecreaseProperty(propertyName, increase);
-};
-
-/**
- * To support viewGraphicAsBraille_(), the current image node.
- * @type {AutomationNode?};
- */
-CommandHandler.imageNode_;
-
-/**
- * Called when an image frame is received on a node.
- * @param {!(AutomationEvent|CustomAutomationEvent)} event The event.
- * @private
- */
-CommandHandler.onImageFrameUpdated_ = function(event) {
-  const target = event.target;
-  if (target !== CommandHandler.imageNode_) {
-    return;
+    imageNode.addEventListener(
+        EventType.IMAGE_FRAME_UPDATED, this.onImageFrameUpdated_, false);
+    this.imageNode_ = imageNode;
+    if (imageNode.imageDataUrl) {
+      const event = new CustomAutomationEvent(
+          EventType.IMAGE_FRAME_UPDATED, imageNode, {eventFrom: 'page'});
+      this.onImageFrameUpdated_(event);
+    } else {
+      imageNode.getImageData(0, 0);
+    }
   }
 
-  if (!AutomationUtil.isDescendantOf(
-          ChromeVoxState.instance.currentRange.start.node,
-          CommandHandler.imageNode_)) {
-    CommandHandler.imageNode_.removeEventListener(
-        EventType.IMAGE_FRAME_UPDATED, CommandHandler.onImageFrameUpdated_,
-        false);
-    CommandHandler.imageNode_ = null;
-    return;
-  }
-
-  if (target.imageDataUrl) {
-    ChromeVox.braille.writeRawImage(target.imageDataUrl);
-    ChromeVox.braille.freeze();
-  }
-};
-
-/**
- * Handle the command to view the first graphic within the current range
- * as braille.
- * @param {!cursors.Range} current The current range.
- * @private
- */
-CommandHandler.viewGraphicAsBraille_ = function(current) {
-  if (CommandHandler.imageNode_) {
-    CommandHandler.imageNode_.removeEventListener(
-        EventType.IMAGE_FRAME_UPDATED, CommandHandler.onImageFrameUpdated_,
-        false);
-    CommandHandler.imageNode_ = null;
-  }
-
-  // Find the first node within the current range that supports image data.
-  const imageNode = AutomationUtil.findNodePost(
-      current.start.node, Dir.FORWARD, AutomationPredicate.supportsImageData);
-  if (!imageNode) {
-    return;
-  }
-
-  imageNode.addEventListener(
-      EventType.IMAGE_FRAME_UPDATED, CommandHandler.onImageFrameUpdated_,
-      false);
-  CommandHandler.imageNode_ = imageNode;
-  if (imageNode.imageDataUrl) {
-    const event = new CustomAutomationEvent(
-        EventType.IMAGE_FRAME_UPDATED, imageNode, {eventFrom: 'page'});
-    CommandHandler.onImageFrameUpdated_(event);
-  } else {
-    imageNode.getImageData(0, 0);
-  }
-};
-
-/**
- * Provides a partial mapping from ChromeVox key combinations to
- * Search-as-a-function key as seen in Chrome OS documentation.
- * @param {string} command
- * @return {boolean} True if the command should propagate.
- * @private
- */
-CommandHandler.onEditCommand_ = function(command) {
-  if (ChromeVox.isStickyModeOn()) {
-    return true;
-  }
-
-  const textEditHandler = DesktopAutomationHandler.instance.textEditHandler;
-  if (!textEditHandler ||
-      !AutomationUtil.isDescendantOf(
-          ChromeVoxState.instance.currentRange.start.node,
-          textEditHandler.node)) {
-    return true;
-  }
-
-  // Skip customized keys for read only text fields.
-  if (textEditHandler.node.restriction ===
-      chrome.automation.Restriction.READ_ONLY) {
-    return true;
-  }
-
-  // Skips customized keys if they get suppressed in speech.
-  if (AutomationPredicate.shouldOnlyOutputSelectionChangeInBraille(
-          textEditHandler.node)) {
-    return true;
-  }
-
-  const isMultiline = AutomationPredicate.multiline(textEditHandler.node);
-  switch (command) {
-    case 'previousCharacter':
-      EventGenerator.sendKeyPress(KeyCode.HOME, {shift: true});
-      break;
-    case 'nextCharacter':
-      EventGenerator.sendKeyPress(KeyCode.END, {shift: true});
-      break;
-    case 'previousWord':
-      EventGenerator.sendKeyPress(KeyCode.HOME, {shift: true, ctrl: true});
-      break;
-    case 'nextWord':
-      EventGenerator.sendKeyPress(KeyCode.END, {shift: true, ctrl: true});
-      break;
-    case 'previousObject':
-      if (!isMultiline) {
-        return true;
-      }
-
-      if (textEditHandler.isSelectionOnFirstLine()) {
-        ChromeVoxState.instance.setCurrentRange(
-            cursors.Range.fromNode(textEditHandler.node));
-        return true;
-      }
-      EventGenerator.sendKeyPress(KeyCode.HOME);
-      break;
-    case 'nextObject':
-      if (!isMultiline) {
-        return true;
-      }
-
-      if (textEditHandler.isSelectionOnLastLine()) {
-        textEditHandler.moveToAfterEditText();
-        return false;
-      }
-
-      EventGenerator.sendKeyPress(KeyCode.END);
-      break;
-    case 'previousLine':
-      if (!isMultiline) {
-        return true;
-      }
-      if (textEditHandler.isSelectionOnFirstLine()) {
-        ChromeVoxState.instance.setCurrentRange(
-            cursors.Range.fromNode(textEditHandler.node));
-        return true;
-      }
-      EventGenerator.sendKeyPress(KeyCode.PRIOR);
-      break;
-    case 'nextLine':
-      if (!isMultiline) {
-        return true;
-      }
-
-      if (textEditHandler.isSelectionOnLastLine()) {
-        textEditHandler.moveToAfterEditText();
-        return false;
-      }
-      EventGenerator.sendKeyPress(KeyCode.NEXT);
-      break;
-    case 'jumpToTop':
-      EventGenerator.sendKeyPress(KeyCode.HOME, {ctrl: true});
-      break;
-    case 'jumpToBottom':
-      EventGenerator.sendKeyPress(KeyCode.END, {ctrl: true});
-      break;
-    default:
+  /**
+   * Provides a partial mapping from ChromeVox key combinations to
+   * Search-as-a-function key as seen in Chrome OS documentation.
+   * @param {string} command
+   * @return {boolean} True if the command should propagate.
+   * @private
+   */
+  onEditCommand_(command) {
+    if (ChromeVox.isStickyModeOn()) {
       return true;
-  }
-  return false;
-};
+    }
 
-/**
- * A helper to object navigation to skip all static text nodes who have
- * label/description for on ancestor nodes.
- * @param {cursors.Range} current
- * @param {Dir} dir
- * @return {cursors.Range} The resulting range.
- */
-CommandHandler.skipLabelOrDescriptionFor = function(current, dir) {
-  if (!current) {
-    return null;
+    const textEditHandler = DesktopAutomationHandler.instance.textEditHandler;
+    if (!textEditHandler ||
+        !AutomationUtil.isDescendantOf(
+            ChromeVoxState.instance.currentRange.start.node,
+            textEditHandler.node)) {
+      return true;
+    }
+
+    // Skip customized keys for read only text fields.
+    if (textEditHandler.node.restriction ===
+        chrome.automation.Restriction.READ_ONLY) {
+      return true;
+    }
+
+    // Skips customized keys if they get suppressed in speech.
+    if (AutomationPredicate.shouldOnlyOutputSelectionChangeInBraille(
+            textEditHandler.node)) {
+      return true;
+    }
+
+    const isMultiline = AutomationPredicate.multiline(textEditHandler.node);
+    switch (command) {
+      case 'previousCharacter':
+        EventGenerator.sendKeyPress(KeyCode.HOME, {shift: true});
+        break;
+      case 'nextCharacter':
+        EventGenerator.sendKeyPress(KeyCode.END, {shift: true});
+        break;
+      case 'previousWord':
+        EventGenerator.sendKeyPress(KeyCode.HOME, {shift: true, ctrl: true});
+        break;
+      case 'nextWord':
+        EventGenerator.sendKeyPress(KeyCode.END, {shift: true, ctrl: true});
+        break;
+      case 'previousObject':
+        if (!isMultiline) {
+          return true;
+        }
+
+        if (textEditHandler.isSelectionOnFirstLine()) {
+          ChromeVoxState.instance.setCurrentRange(
+              cursors.Range.fromNode(textEditHandler.node));
+          return true;
+        }
+        EventGenerator.sendKeyPress(KeyCode.HOME);
+        break;
+      case 'nextObject':
+        if (!isMultiline) {
+          return true;
+        }
+
+        if (textEditHandler.isSelectionOnLastLine()) {
+          textEditHandler.moveToAfterEditText();
+          return false;
+        }
+
+        EventGenerator.sendKeyPress(KeyCode.END);
+        break;
+      case 'previousLine':
+        if (!isMultiline) {
+          return true;
+        }
+        if (textEditHandler.isSelectionOnFirstLine()) {
+          ChromeVoxState.instance.setCurrentRange(
+              cursors.Range.fromNode(textEditHandler.node));
+          return true;
+        }
+        EventGenerator.sendKeyPress(KeyCode.PRIOR);
+        break;
+      case 'nextLine':
+        if (!isMultiline) {
+          return true;
+        }
+
+        if (textEditHandler.isSelectionOnLastLine()) {
+          textEditHandler.moveToAfterEditText();
+          return false;
+        }
+        EventGenerator.sendKeyPress(KeyCode.NEXT);
+        break;
+      case 'jumpToTop':
+        EventGenerator.sendKeyPress(KeyCode.HOME, {ctrl: true});
+        break;
+      case 'jumpToBottom':
+        EventGenerator.sendKeyPress(KeyCode.END, {ctrl: true});
+        break;
+      default:
+        return true;
+    }
+    return false;
   }
 
-  // Keep moving past all nodes acting as labels or descriptions.
-  while (current && current.start && current.start.node &&
-         current.start.node.role === RoleType.STATIC_TEXT) {
-    // We must scan upwards as any ancestor might have a label or description.
-    let ancestor = current.start.node;
-    while (ancestor) {
-      if ((ancestor.labelFor && ancestor.labelFor.length > 0) ||
-          (ancestor.descriptionFor && ancestor.descriptionFor.length > 0)) {
+  /** @override */
+
+  skipLabelOrDescriptionFor(current, dir) {
+    if (!current) {
+      return null;
+    }
+
+    // Keep moving past all nodes acting as labels or descriptions.
+    while (current && current.start && current.start.node &&
+           current.start.node.role === RoleType.STATIC_TEXT) {
+      // We must scan upwards as any ancestor might have a label or description.
+      let ancestor = current.start.node;
+      while (ancestor) {
+        if ((ancestor.labelFor && ancestor.labelFor.length > 0) ||
+            (ancestor.descriptionFor && ancestor.descriptionFor.length > 0)) {
+          break;
+        }
+        ancestor = ancestor.parent;
+      }
+      if (ancestor) {
+        current = current.move(cursors.Unit.NODE, dir);
+      } else {
         break;
       }
-      ancestor = ancestor.parent;
     }
-    if (ancestor) {
-      current = current.move(cursors.Unit.NODE, dir);
-    } else {
-      break;
-    }
+
+    return current;
   }
 
-  return current;
-};
+  /**
+   * Performs global initialization.
+   */
+  init() {
+    ChromeVoxKbHandler.commandHandler = this.onCommand.bind(this);
 
-/**
- * Performs global initialization.
- */
-CommandHandler.init = function() {
-  ChromeVoxKbHandler.commandHandler = CommandHandler.onCommand;
+    chrome.commandLinePrivate.hasSwitch(
+        'enable-experimental-accessibility-language-detection', (enabled) => {
+          if (enabled) {
+            this.languageLoggingEnabled_ = true;
+          }
+        });
+    chrome.commandLinePrivate.hasSwitch(
+        'enable-experimental-accessibility-language-detection-dynamic',
+        (enabled) => {
+          if (enabled) {
+            this.languageLoggingEnabled_ = true;
+          }
+        });
 
-  chrome.commandLinePrivate.hasSwitch(
-      'enable-experimental-accessibility-language-detection', (enabled) => {
-        if (enabled) {
-          CommandHandler.languageLoggingEnabled_ = true;
-        }
-      });
-  chrome.commandLinePrivate.hasSwitch(
-      'enable-experimental-accessibility-language-detection-dynamic',
-      (enabled) => {
-        if (enabled) {
-          CommandHandler.languageLoggingEnabled_ = true;
-        }
-      });
+    chrome.chromeosInfoPrivate.get(['sessionType'], (result) => {
+      /** @type {boolean} */
+      this.isKioskSession_ = result['sessionType'] ===
+          chrome.chromeosInfoPrivate.SessionType.KIOSK;
+    });
+  }
+}
 
-  chrome.chromeosInfoPrivate.get(['sessionType'], (result) => {
-    /** @type {boolean} */
-    CommandHandler.isKioskSession_ =
-        result['sessionType'] === chrome.chromeosInfoPrivate.SessionType.KIOSK;
-  });
-};
-});  // goog.scope
+CommandHandlerInterface.instance = new CommandHandler();
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler_interface.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler_interface.js
new file mode 100644
index 0000000..d8e8f1cac
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler_interface.js
@@ -0,0 +1,28 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.provide('CommandHandlerInterface');
+
+CommandHandlerInterface = class {
+  /**
+   * Handles ChromeVox commands.
+   * @param {string} command
+   * @return {boolean} True if the command should propagate.
+   */
+  onCommand(command) {}
+
+  /**
+   * A helper to object navigation to skip all static text nodes who have
+   * label/description for on ancestor nodes.
+   * @param {cursors.Range} current
+   * @param {constants.Dir} dir
+   * @return {cursors.Range} The resulting range.
+   */
+  skipLabelOrDescriptionFor(current, dir) {}
+};
+
+/**
+ * @type {CommandHandlerInterface}
+ */
+CommandHandlerInterface.instance;
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler.js
index 669d1d8..8e2e17f 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler.js
@@ -12,7 +12,7 @@
 goog.require('AutomationObjectConstructorInstaller');
 goog.require('BaseAutomationHandler');
 goog.require('ChromeVoxState');
-goog.require('CommandHandler');
+goog.require('CommandHandlerInterface');
 goog.require('CustomAutomationEvent');
 goog.require('editing.TextEditHandler');
 
@@ -418,7 +418,7 @@
         ChromeVoxState.instance.setCurrentRange(
             cursors.Range.fromNode(evt.target));
         ChromeVox.tts.stop();
-        CommandHandler.onCommand('readFromHere');
+        CommandHandlerInterface.instance.onCommand('readFromHere');
         return;
       }
 
@@ -513,7 +513,8 @@
 
       // Sync ChromeVox range with selection.
       if (!ChromeVoxState.isReadingContinuously) {
-        ChromeVoxState.instance.setCurrentRange(selectedRange);
+        ChromeVoxState.instance.setCurrentRange(
+            selectedRange, true /* from editing */);
       }
     }
     this.textEditHandler_.onEvent(evt);
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler_test.js
index cb5620c..0d2ccb5 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler_test.js
@@ -12,6 +12,8 @@
 ChromeVoxDesktopAutomationHandlerTest = class extends ChromeVoxNextE2ETest {
   /** @override */
   async setUpDeferred() {
+    await super.setUpDeferred();
+
     window.press = this.press;
 
     await new Promise(r => {
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing/editing.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing/editing.js
index 47fcf54..d62d186 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing/editing.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing/editing.js
@@ -11,6 +11,7 @@
 
 goog.require('AutomationTreeWalker');
 goog.require('AutomationUtil');
+goog.require('Color');
 goog.require('IntentHandler');
 goog.require('Output');
 goog.require('OutputEventType');
@@ -976,7 +977,7 @@
   }
 
   /** @override */
-  onCurrentRangeChanged(range) {
+  onCurrentRangeChanged(range, opt_fromEditing) {
     const inputType = range && range.start.node.inputType;
     if (inputType === 'email' || inputType === 'url') {
       BrailleBackground.getInstance().getTranslatorManager().refresh(
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/gesture_command_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/gesture_command_handler.js
index 87f89a0..0d5211c 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/gesture_command_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/gesture_command_handler.js
@@ -9,7 +9,7 @@
 goog.provide('GestureCommandHandler');
 
 goog.require('ChromeVoxState');
-goog.require('CommandHandler');
+goog.require('CommandHandlerInterface');
 goog.require('EventGenerator');
 goog.require('EventSourceState');
 goog.require('GestureCommandData');
@@ -113,7 +113,7 @@
 
   const command = commandData.command;
   if (command) {
-    CommandHandler.onCommand(command);
+    CommandHandlerInterface.instance.onCommand(command);
   }
 };
 
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/live_regions_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/live_regions_test.js
index 5f86e60..703f9fd 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/live_regions_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/live_regions_test.js
@@ -12,6 +12,8 @@
  */
 ChromeVoxLiveRegionsTest = class extends ChromeVoxNextE2ETest {
   async setUpDeferred() {
+    await super.setUpDeferred();
+
     window.TreeChangeType = chrome.automation.TreeChangeType;
     await importModule('LiveRegions', '/chromevox/background/live_regions.js');
   }
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/loader.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/loader.js
index 0cba40b8..b64c921 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/loader.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/loader.js
@@ -15,7 +15,8 @@
 goog.require('ChromeVoxBackground');
 goog.require('ChromeVoxEditableTextBase');
 goog.require('ChromeVoxState');
-goog.require('CommandHandler');
+goog.require('CommandHandlerInterface');
+goog.require('CommandStore');
 goog.require('DesktopAutomationHandler');
 goog.require('ExtensionBridge');
 goog.require('GestureCommandHandler');
@@ -27,5 +28,6 @@
 goog.require('OutputEventType');
 goog.require('PanelCommand');
 goog.require('PhoneticData');
+goog.require('SmartStickyMode');
 goog.require('constants');
 goog.require('cursors.Cursor');
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/range_automation_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/range_automation_handler.js
index 1c3b573d..ae657c6 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/range_automation_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/range_automation_handler.js
@@ -34,8 +34,9 @@
 
   /**
    * @param {cursors.Range} newRange
+   * @param {boolean=} opt_fromEditing
    */
-  onCurrentRangeChanged(newRange) {
+  onCurrentRangeChanged(newRange, opt_fromEditing) {
     if (this.node_) {
       this.removeAllListeners();
       this.node_ = undefined;
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/smart_sticky_mode.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/smart_sticky_mode.js
index b44472b..995049a 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/smart_sticky_mode.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/smart_sticky_mode.js
@@ -33,9 +33,9 @@
   }
 
   /** @override */
-  onCurrentRangeChanged(newRange) {
+  onCurrentRangeChanged(newRange, opt_fromEditing) {
     if (!newRange || this.ignoreRangeChanges_ ||
-        ChromeVoxState.isReadingContinuously ||
+        ChromeVoxState.isReadingContinuously || opt_fromEditing ||
         localStorage['smartStickyMode'] !== 'true') {
       return;
     }
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/user_action_monitor.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/user_action_monitor.js
index d230406..a98d1a2f 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/user_action_monitor.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/user_action_monitor.js
@@ -326,6 +326,6 @@
    * @private
    */
   static onCommand_(command) {
-    CommandHandler.onCommand(command);
+    CommandHandlerInterface.instance.onCommand(command);
   }
-};
\ No newline at end of file
+};
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/common/tts_background_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/common/tts_background_test.js
index 1725efb..a7316e3 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/common/tts_background_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/common/tts_background_test.js
@@ -538,7 +538,7 @@
 TEST_F('ChromeVoxTtsBackgroundTest', 'ResetTtsSettingsClearsVoice', function() {
   this.newCallback(async () => {
     ChromeVox.tts.ttsEngines_[0].currentVoice = '';
-    CommandHandler.onCommand('resetTextToSpeechSettings');
+    CommandHandlerInterface.instance.onCommand('resetTextToSpeechSettings');
     await new Promise(r => {
       ChromeVox.tts.speak = textString => {
         if (textString === 'Reset text to speech settings to default values') {
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/learn_mode/learn_mode_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/learn_mode/learn_mode_test.js
index ab8da2f..5530aa1 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/learn_mode/learn_mode_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/learn_mode/learn_mode_test.js
@@ -37,7 +37,7 @@
 
       desktop.addEventListener(
           chrome.automation.EventType.LOAD_COMPLETE, listener);
-      CommandHandler.onCommand('showKbExplorerPage');
+      CommandHandlerInterface.instance.onCommand('showKbExplorerPage');
     });
   }
 
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/options/options_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/options/options_test.js
index 17583dc..ff97fe4 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/options/options_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/options/options_test.js
@@ -29,7 +29,7 @@
             mockFeedback.expectSpeech('ChromeVox Options');
             callback(mockFeedback, evt);
           });
-      CommandHandler.onCommand('showOptionsPage');
+      CommandHandlerInterface.instance.onCommand('showOptionsPage');
     });
   }
 
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel.js b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel.js
index 2372c73ba..e111a96c 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel.js
@@ -429,8 +429,9 @@
           menu.addMenuItem(
               binding.title, keyText, brailleText, gestureText, function() {
                 const CommandHandler =
-                    chrome.extension.getBackgroundPage()['CommandHandler'];
-                CommandHandler['onCommand'](binding.command);
+                    chrome.extension
+                        .getBackgroundPage()['CommandHandlerInterface'];
+                CommandHandler.instance.onCommand(binding.command);
               }, binding.command);
         }
       });
@@ -463,8 +464,9 @@
           touchMenu.addMenuItem(
               item.titleText, '', '', item.gestureText, function() {
                 const CommandHandler =
-                    chrome.extension.getBackgroundPage()['CommandHandler'];
-                CommandHandler['onCommand'](item.command);
+                    chrome.extension
+                        .getBackgroundPage()['CommandHandlerInterface'];
+                CommandHandler.instance.onCommand(item.command);
               }, item.command);
         }
       }
@@ -1214,8 +1216,8 @@
       chromeVoxStateInstance.destroyUserActionMonitor();
     });
     $('chromevox-tutorial').addEventListener('requestfullydescribe', (evt) => {
-      const commandHandler = backgroundPage['CommandHandler'];
-      commandHandler.onCommand('fullyDescribe');
+      const commandHandler = backgroundPage['CommandHandlerInterface'];
+      commandHandler.instance.onCommand('fullyDescribe');
     });
     $('chromevox-tutorial').addEventListener('requestearcon', (evt) => {
       const earconId = evt.detail.earconId;
@@ -1302,7 +1304,7 @@
 Panel.PanelStateObserver = class {
   constructor() {}
 
-  onCurrentRangeChanged(range) {
+  onCurrentRangeChanged(range, opt_fromEditing) {
     if (Panel.mode_ === Panel.Mode.FULLSCREEN_TUTORIAL) {
       if (Panel.tutorial && Panel.tutorial.restartNudges) {
         Panel.tutorial.restartNudges();
@@ -1374,6 +1376,7 @@
   // it in in every case. (fullscreen/focus turns the state off, collapse
   // turns it back on).
   if (Panel.originalStickyState_) {
-    bkgnd['CommandHandler']['onCommand']('toggleStickyMode');
+    bkgnd['CommandHandlerInterface']['instance']['onCommand'](
+        'toggleStickyMode');
   }
 }, false);
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_test.js
index 8e57d61..528fd10 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_test.js
@@ -103,7 +103,7 @@
 // TODO(https://crbug.com/1299765): Re-enable once flaky timeouts are fixed.
 TEST_F('ChromeVoxPanelTest', 'DISABLED_LinkMenu', function() {
   this.runWithLoadedTree(this.linksDoc, async function(root) {
-    CommandHandler.onCommand('showLinksList');
+    CommandHandlerInterface.instance.onCommand('showLinksList');
     await this.waitForMenu('role_link');
     this.fireMockEvent('ArrowLeft')();
     this.assertActiveMenuItem('role_landmark', 'No items');
@@ -117,7 +117,7 @@
 TEST_F('ChromeVoxPanelTest', 'FormControlsMenu', function() {
   this.runWithLoadedTree(
       `<button>Cancel</button><button>OK</button>`, async function(root) {
-        CommandHandler.onCommand('showFormsList');
+        CommandHandlerInterface.instance.onCommand('showFormsList');
         await this.waitForMenu('panel_menu_form_controls');
         this.fireMockEvent('ArrowDown')();
         this.assertActiveMenuItem('panel_menu_form_controls', 'OK Button');
@@ -191,7 +191,7 @@
     localStorage['languageSwitching'] = 'true';
     this.getPanelWindow().LocaleOutputHelper.instance.availableVoices_ =
         [{'lang': 'en-US'}, {'lang': 'es-ES'}];
-    CommandHandler.onCommand('showFormsList');
+    CommandHandlerInterface.instance.onCommand('showFormsList');
     await this.waitForMenu('panel_menu_form_controls');
     this.fireMockEvent('ArrowDown')();
     this.assertActiveMenuItem(
@@ -203,7 +203,7 @@
 
 TEST_F('ChromeVoxPanelTest', 'ActionsMenu', function() {
   this.runWithLoadedTree(this.linksDoc, async function(root) {
-    CommandHandler.onCommand('showActionsMenu');
+    CommandHandlerInterface.instance.onCommand('showActionsMenu');
     await this.waitForMenu('panel_menu_actions');
     this.fireMockEvent('ArrowDown')();
     this.assertActiveMenuItem('panel_menu_actions', 'Start Or End Selection');
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/tutorial_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/tutorial_test.js
index 42b75b92..c84dcd0 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/tutorial_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/tutorial_test.js
@@ -572,16 +572,16 @@
       });
     };
     restart = false;
-    CommandHandler.onCommand('nextObject');
+    CommandHandlerInterface.instance.onCommand('nextObject');
     await waitForRestartNudges();
     // Show a lesson.
     tutorial.curriculum = 'essential_keys';
     tutorial.showLesson_(0);
     restart = false;
-    CommandHandler.onCommand('nextObject');
+    CommandHandlerInterface.instance.onCommand('nextObject');
     await waitForRestartNudges();
     restart = false;
-    CommandHandler.onCommand('nextObject');
+    CommandHandlerInterface.instance.onCommand('nextObject');
     await waitForRestartNudges();
   });
 });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/testing/chromevox_next_e2e_test_base.js b/chrome/browser/resources/chromeos/accessibility/chromevox/testing/chromevox_next_e2e_test_base.js
index d28c621..9776fa2 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/testing/chromevox_next_e2e_test_base.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/testing/chromevox_next_e2e_test_base.js
@@ -87,7 +87,7 @@
    */
   doCmd(cmd) {
     return () => {
-      CommandHandler.onCommand(cmd);
+      CommandHandlerInterface.instance.onCommand(cmd);
     };
   }
 
@@ -113,10 +113,17 @@
   }
 
   /** @override */
+  async setUpDeferred() {
+    await super.setUpDeferred();
+    await importModule(
+        'CommandHandler', '/chromevox/background/command_handler.js');
+  }
+
+  /** @override */
   runWithLoadedTree(doc, callback, opt_params = {}) {
     callback = this.newCallback(callback);
     const wrappedCallback = (node) => {
-      CommandHandler.onCommand('nextObject');
+      CommandHandlerInterface.instance.onCommand('nextObject');
       callback(node);
     };
 
diff --git a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.html b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.html
index f72133cb9..41e5b7f 100644
--- a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.html
+++ b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.html
@@ -251,60 +251,60 @@
       </div>
       <div id="dialogBody" slot="body">
         <template is="dom-if" if="[[!hasStartedSetupAttempt_]]" restamp>
+          <div id="start-setup-description">
+            <div id="half-container">
+              <div id="illustration"></div>
+            </div>
+            <div id="half-container">
+              <div id="feature-description">
+                <template is="dom-if" if="[[shouldShowNotificationItem_]]"
+                    restamp>
+                  <div id="feature-details-container">
+                    <iron-icon id="feature-icon"
+                        icon="os-settings:multidevice-better-together-suite">
+                    </iron-icon>
+                    <div id="feature-details-description">
+                      <div id="feature-title">
+                        $i18n{multidevicePermissionsSetupNotificationsTitle}
+                      </div>
+                      <div id="feature-subtitle">
+                        $i18n{multidevicePermissionsSetupNotificationsSummary}
+                       </div>
+                    </div>
+                  </div>
+                </template>
+                <template is="dom-if" if="[[shouldShowAppsItem_]]" restamp>
+                  <div id="feature-details-container">
+                    <iron-icon id="feature-icon"
+                        icon="os-settings:multidevice-better-together-suite">
+                    </iron-icon>
+                    <div id="feature-details-description">
+                      <div id="feature-title">
+                        $i18n{multidevicePermissionsSetupAppsTitle}
+                      </div>
+                      <div id="feature-subtitle">
+                         $i18n{multidevicePermissionsSetupAppsSummary}
+                      </div>
+                    </div>
+                  </div>
+                </template>
+              </div>
+            </div>
+          </div>
+        </template>
+        <template is="dom-if" if="[[hasStartedSetupAttempt_]]" restamp>
           <template is="dom-if" if="[[shouldShowScreenLockInstructions_(flowState_)]]" restamp>
             <settings-multidevice-screen-lock-subpage
                 is-screen-lock-enabled="{{isScreenLockEnabled_}}"
                 is-password-dialog-showing="{{isPasswordDialogShowing}}">
             </settings-multidevice-screen-lock-subpage>
-          </template>
+          </template> 
           <template is="dom-if" if="[[!shouldShowScreenLockInstructions_(flowState_)]]" restamp>
-            <div id="start-setup-description">
-              <div id="half-container">
-                <div id="illustration"></div>
-              </div>
-              <div id="half-container">
-                <div id="feature-description">
-                  <template is="dom-if" if="[[shouldShowNotificationItem_]]"
-                      restamp>
-                    <div id="feature-details-container">
-                      <iron-icon id="feature-icon"
-                          icon="os-settings:multidevice-better-together-suite">
-                      </iron-icon>
-                      <div id="feature-details-description">
-                        <div id="feature-title">
-                          $i18n{multidevicePermissionsSetupNotificationsTitle}
-                        </div>
-                        <div id="feature-subtitle">
-                          $i18n{multidevicePermissionsSetupNotificationsSummary}
-                        </div>
-                      </div>
-                    </div>
-                  </template>
-                  <template is="dom-if" if="[[shouldShowAppsItem_]]" restamp>
-                    <div id="feature-details-container">
-                      <iron-icon id="feature-icon"
-                          icon="os-settings:multidevice-better-together-suite">
-                      </iron-icon>
-                      <div id="feature-details-description">
-                        <div id="feature-title">
-                          $i18n{multidevicePermissionsSetupAppsTitle}
-                        </div>
-                        <div id="feature-subtitle">
-                          $i18n{multidevicePermissionsSetupAppsSummary}
-                        </div>
-                      </div>
-                    </div>
-                  </template>
-                </div>
-              </div>
-            </div>
-          </template>
-        </template>
-        <template is="dom-if" if="[[hasStartedSetupAttempt_]]" restamp>
-          <div id="illustration"></div>
-          <template is="dom-if" if="[[description_]]" restamp>
-            <localized-link localized-string="[[description_]]">
-            </localized-link>
+            <div id="illustration"></div>
+            <template is="dom-if" if="[[description_]]" restamp>
+              <localized-link localized-string="[[description_]]">
+              </localized-link>
+            </template>
           </template>
         </template>
         <template is="dom-if" if="[[shouldShowSetupInstructionsSeparately_]]"
@@ -351,6 +351,14 @@
               $i18n{next}
             </cr-button>
           </template>
+          <template is="dom-if" if="[[hasStartedSetupAttempt_]]" restamp>
+            <template is="dom-if" if="[[shouldShowScreenLockInstructions_(flowState_)]]" restamp>
+              <cr-button id="getStartedButton" class="action-button"
+                  on-click="nextPage_">
+                $i18n{next}
+              </cr-button>
+            </template>
+          </template>
           <template is="dom-if" if="[[shouldShowTryAgainButton_(setupState_)]]"
               restamp>
             <cr-button id="tryAgainButton" class="action-button"
diff --git a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.js b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.js
index 93045dad..0be342f3 100644
--- a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.js
+++ b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.js
@@ -276,9 +276,8 @@
 
   /** @private */
   nextPage_() {
-    const isScreenLockRequired = loadTimeData.getBoolean('isEcheAppEnabled') &&
-        loadTimeData.getBoolean('isPhoneScreenLockEnabled') &&
-        !loadTimeData.getBoolean('isChromeosScreenLockEnabled');
+    const isScreenLockRequired =
+        this.isScreenLockRequired_();
     switch (this.flowState_) {
       case SetupFlowStatus.INTRO:
         if (isScreenLockRequired) {
@@ -459,4 +458,18 @@
          this.phonePermissionSetupMode ===
              PhoneHubPermissionsSetupMode.APPS_SETUP_MODE);
   },
+
+  /**
+   * @return {boolean}
+   * @private
+   */
+  isScreenLockRequired_() {
+    return loadTimeData.getBoolean('isEcheAppEnabled') &&
+        loadTimeData.getBoolean('isPhoneScreenLockEnabled') &&
+        !loadTimeData.getBoolean('isChromeosScreenLockEnabled') &&
+        (this.phonePermissionSetupMode ===
+             PhoneHubPermissionsSetupMode.ALL_PERMISSIONS_SETUP_MODE ||
+         this.phonePermissionSetupMode ===
+             PhoneHubPermissionsSetupMode.APPS_SETUP_MODE);
+  },
 });
diff --git a/chrome/browser/resources/settings/chromeos/os_settings.js b/chrome/browser/resources/settings/chromeos/os_settings.js
index 81a2d110..a0cd86f 100644
--- a/chrome/browser/resources/settings/chromeos/os_settings.js
+++ b/chrome/browser/resources/settings/chromeos/os_settings.js
@@ -124,7 +124,7 @@
 export {MultiDeviceBrowserProxy, MultiDeviceBrowserProxyImpl} from './multidevice_page/multidevice_browser_proxy.m.js';
 export {MultiDeviceFeature, MultiDeviceFeatureState, MultiDevicePageContentData, MultiDeviceSettingsMode, PhoneHubNotificationAccessProhibitedReason, PhoneHubNotificationAccessStatus, PhoneHubPermissionsSetupMode, SmartLockSignInEnabledState} from './multidevice_page/multidevice_constants.m.js';
 export {NotificationAccessSetupOperationStatus} from './multidevice_page/multidevice_notification_access_setup_dialog.m.js';
-export {PermissionsSetupStatus} from './multidevice_page/multidevice_permissions_setup_dialog.m.js';
+export {PermissionsSetupStatus, SetupFlowStatus} from './multidevice_page/multidevice_permissions_setup_dialog.m.js';
 export {Account, NearbyAccountManagerBrowserProxy, NearbyAccountManagerBrowserProxyImpl} from './nearby_share_page/nearby_account_manager_browser_proxy.js';
 export {getReceiveManager, observeReceiveManager, setReceiveManagerForTesting} from './nearby_share_page/nearby_share_receive_manager.js';
 export {dataUsageStringToEnum, NearbyShareDataUsage} from './nearby_share_page/types.js';
diff --git a/chrome/browser/resources/settings/chromeos/os_settings_menu/os_settings_menu.html b/chrome/browser/resources/settings/chromeos/os_settings_menu/os_settings_menu.html
index 94d7099..3e4bd479 100644
--- a/chrome/browser/resources/settings/chromeos/os_settings_menu/os_settings_menu.html
+++ b/chrome/browser/resources/settings/chromeos/os_settings_menu/os_settings_menu.html
@@ -3,7 +3,7 @@
     /* The tap target extends slightly above each visible menu item. */
     --tap-target-padding: 3px;
     /* Width of the keyboard focus border. */
-    --focus-border-width: 1px;
+    --focus-border-width: 2px;
     box-sizing: border-box;
     display: block;
     padding-bottom: 2px;
@@ -100,7 +100,8 @@
   }
 
   :host-context(.focus-outline-visible) #advancedButton:focus {
-    outline: auto 5px -webkit-focus-ring-color;
+    border-radius: 0 20px 20px 0;
+    outline: var(--focus-border-width) solid var(--cros-focus-ring-color);
   }
 
   #advancedButton > span {
diff --git a/chrome/browser/speech/on_device_speech_recognizer_browsertest.cc b/chrome/browser/speech/on_device_speech_recognizer_browsertest.cc
index 70f6188..3c57417 100644
--- a/chrome/browser/speech/on_device_speech_recognizer_browsertest.cc
+++ b/chrome/browser/speech/on_device_speech_recognizer_browsertest.cc
@@ -116,6 +116,8 @@
     mock_speech_delegate_ =
         std::make_unique<testing::StrictMock<MockSpeechRecognizerDelegate>>();
     // Fake that SODA is installed.
+    speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting(
+        speech::LanguageCode::kEnUs);
     speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
   }
 
diff --git a/chrome/browser/speech/speech_recognition_client_browser_interface.cc b/chrome/browser/speech/speech_recognition_client_browser_interface.cc
index ad3c368..510ade71 100644
--- a/chrome/browser/speech/speech_recognition_client_browser_interface.cc
+++ b/chrome/browser/speech/speech_recognition_client_browser_interface.cc
@@ -58,7 +58,10 @@
   OnSpeechRecognitionAvailabilityChanged();
 }
 
-void SpeechRecognitionClientBrowserInterface::OnSodaInstalled() {
+void SpeechRecognitionClientBrowserInterface::OnSodaInstalled(
+    speech::LanguageCode language_code) {
+  if (!prefs::IsLanguageCodeForLiveCaption(language_code, profile_prefs_))
+    return;
   NotifyObservers(profile_prefs_->GetBoolean(prefs::kLiveCaptionEnabled));
 }
 
diff --git a/chrome/browser/speech/speech_recognition_client_browser_interface.h b/chrome/browser/speech/speech_recognition_client_browser_interface.h
index 6bb65ee..9a302e7 100644
--- a/chrome/browser/speech/speech_recognition_client_browser_interface.h
+++ b/chrome/browser/speech/speech_recognition_client_browser_interface.h
@@ -45,9 +45,10 @@
           pending_remote) override;
 
   // SodaInstaller::Observer:
-  void OnSodaInstalled() override;
-  void OnSodaProgress(int combined_progress) override {}
-  void OnSodaError() override {}
+  void OnSodaInstalled(speech::LanguageCode language_code) override;
+  void OnSodaProgress(speech::LanguageCode language_code,
+                      int progress) override {}
+  void OnSodaError(speech::LanguageCode language_code) override {}
 
  private:
   void OnSpeechRecognitionAvailabilityChanged();
diff --git a/chrome/browser/speech/speech_recognition_test_helper.cc b/chrome/browser/speech/speech_recognition_test_helper.cc
index ec3e67aa..5e56dd2a 100644
--- a/chrome/browser/speech/speech_recognition_test_helper.cc
+++ b/chrome/browser/speech/speech_recognition_test_helper.cc
@@ -39,6 +39,8 @@
 void SpeechRecognitionTestHelper::SetUpOnDeviceRecognition(Profile* profile) {
   // Fake that SODA is installed so SpeechRecognitionPrivate uses
   // OnDeviceSpeechRecognizer.
+  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting(
+      speech::LanguageCode::kEnUs);
   speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
   CrosSpeechRecognitionServiceFactory::GetInstanceForTest()
       ->SetTestingFactoryAndUse(
diff --git a/chrome/browser/ui/ash/projector/projector_client_impl_unittest.cc b/chrome/browser/ui/ash/projector/projector_client_impl_unittest.cc
index 1842607..97be4722 100644
--- a/chrome/browser/ui/ash/projector/projector_client_impl_unittest.cc
+++ b/chrome/browser/ui/ash/projector/projector_client_impl_unittest.cc
@@ -95,6 +95,7 @@
     SetLocale(kEnglishLocale);
     soda_installer_ = std::make_unique<MockSodaInstaller>();
     soda_installer_->NotifySodaInstalledForTesting();
+    soda_installer_->NotifySodaInstalledForTesting(speech::LanguageCode::kEnUs);
     projector_client_ =
         std::make_unique<ProjectorClientImpl>(&projector_controller_);
   }
diff --git a/chrome/browser/ui/ash/projector/projector_soda_installation_controller.cc b/chrome/browser/ui/ash/projector/projector_soda_installation_controller.cc
index 081ea989..4847fd3 100644
--- a/chrome/browser/ui/ash/projector/projector_soda_installation_controller.cc
+++ b/chrome/browser/ui/ash/projector/projector_soda_installation_controller.cc
@@ -89,25 +89,38 @@
   return speech::SodaInstaller::GetInstance()->IsSodaInstalled(language_code);
 }
 
-void ProjectorSodaInstallationController::OnSodaInstalled() {
-  auto* soda_installer = speech::SodaInstaller::GetInstance();
-  // Make sure that both SODA binary and the locale language are available
-  // before notifying that on device speech recognition is available.
-  if (!soda_installer->IsSodaInstalled(speech::GetLanguageCode(GetLocale())))
+void ProjectorSodaInstallationController::OnSodaInstalled(
+    speech::LanguageCode language_code) {
+  // Check that language code matches the selected language for projector.
+  if (language_code != speech::GetLanguageCode(GetLocale()))
     return;
-
   projector_controller_->OnSpeechRecognitionAvailabilityChanged(
       ash::SpeechRecognitionAvailability::kAvailable);
   app_client_->OnSodaInstalled();
 }
 
-void ProjectorSodaInstallationController::OnSodaError() {
+void ProjectorSodaInstallationController::OnSodaError(
+    speech::LanguageCode language_code) {
+  // Check that language code matches the selected language for projector or is
+  // LanguageCode::kNone (signifying the SODA binary failed).
+  if (language_code != speech::GetLanguageCode(GetLocale()) &&
+      language_code != speech::LanguageCode::kNone) {
+    return;
+  }
+
   projector_controller_->OnSpeechRecognitionAvailabilityChanged(
       ash::SpeechRecognitionAvailability::kSodaInstallationError);
   app_client_->OnSodaInstallError();
 }
 
 void ProjectorSodaInstallationController::OnSodaProgress(
-    int combined_progress) {
-  app_client_->OnSodaInstallProgress(combined_progress);
+    speech::LanguageCode language_code,
+    int progress) {
+  // Check that language code matches the selected language for projector or is
+  // LanguageCode::kNone (signifying the SODA binary has progress).
+  if (language_code != speech::GetLanguageCode(GetLocale()) &&
+      language_code != speech::LanguageCode::kNone) {
+    return;
+  }
+  app_client_->OnSodaInstallProgress(progress);
 }
diff --git a/chrome/browser/ui/ash/projector/projector_soda_installation_controller.h b/chrome/browser/ui/ash/projector/projector_soda_installation_controller.h
index 4fe6a8b9..6adfa1a1 100644
--- a/chrome/browser/ui/ash/projector/projector_soda_installation_controller.h
+++ b/chrome/browser/ui/ash/projector/projector_soda_installation_controller.h
@@ -46,15 +46,10 @@
 
  protected:
   // speech::SodaInstaller::Observer:
-  void OnSodaInstalled() override;
-  void OnSodaLanguagePackInstalled(
-      speech::LanguageCode language_code) override {}
-  void OnSodaError() override;
-  void OnSodaLanguagePackError(speech::LanguageCode language_code) override {}
-  void OnSodaProgress(int combined_progress) override;
-  void OnSodaLanguagePackProgress(int language_progress,
-                                  speech::LanguageCode language_code) override {
-  }
+  void OnSodaInstalled(speech::LanguageCode language_code) override;
+  void OnSodaError(speech::LanguageCode language_code) override;
+  void OnSodaProgress(speech::LanguageCode language_code,
+                      int progress) override;
 
   ash::ProjectorAppClient* const app_client_;
   ash::ProjectorController* const projector_controller_;
diff --git a/chrome/browser/ui/ash/projector/projector_soda_installation_controller_unittest.cc b/chrome/browser/ui/ash/projector/projector_soda_installation_controller_unittest.cc
index e984ab5..824113b 100644
--- a/chrome/browser/ui/ash/projector/projector_soda_installation_controller_unittest.cc
+++ b/chrome/browser/ui/ash/projector/projector_soda_installation_controller_unittest.cc
@@ -100,6 +100,8 @@
   }
 
   MockSodaInstaller* soda_installer() { return soda_installer_.get(); }
+  speech::LanguageCode en_us() { return speech::LanguageCode::kEnUs; }
+  speech::LanguageCode fr_fr() { return speech::LanguageCode::kFrFr; }
 
  private:
   content::BrowserTaskEnvironment task_environment_;
@@ -122,25 +124,21 @@
       .WillByDefault(
           testing::Return(std::vector<std::string>({kEnglishLocale})));
 
-  EXPECT_TRUE(soda_installation_controller()->ShouldDownloadSoda(
-      speech::LanguageCode::kEnUs));
+  EXPECT_TRUE(soda_installation_controller()->ShouldDownloadSoda(en_us()));
 
   // Other languages other than English are not currently supported.
-  EXPECT_FALSE(soda_installation_controller()->ShouldDownloadSoda(
-      speech::LanguageCode::kFrFr));
+  EXPECT_FALSE(soda_installation_controller()->ShouldDownloadSoda(fr_fr()));
 }
 
 TEST_F(ProjectorSodaInstallationControllerTest, IsSpeechRecognitionAvailable) {
   SetLocale(kEnglishLocale);
-  EXPECT_FALSE(soda_installation_controller()->IsSodaAvailable(
-      speech::LanguageCode::kEnUs));
+  EXPECT_FALSE(soda_installation_controller()->IsSodaAvailable(en_us()));
 
   EXPECT_CALL(app_client(), OnSodaInstalled()).Times(1);
   speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
-  EXPECT_TRUE(soda_installation_controller()->IsSodaAvailable(
-      speech::LanguageCode::kEnUs));
-  EXPECT_FALSE(soda_installation_controller()->IsSodaAvailable(
-      speech::LanguageCode::kFrFr));
+  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting(en_us());
+  EXPECT_TRUE(soda_installation_controller()->IsSodaAvailable(en_us()));
+  EXPECT_FALSE(soda_installation_controller()->IsSodaAvailable(fr_fr()));
 }
 
 TEST_F(ProjectorSodaInstallationControllerTest, InstallSoda) {
@@ -153,13 +151,13 @@
 
   EXPECT_CALL(app_client(), OnSodaInstalled()).Times(1);
   speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
+  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting(en_us());
 }
 
 TEST_F(ProjectorSodaInstallationControllerTest, OnSodaInstallProgress) {
   SetLocale(kEnglishLocale);
   EXPECT_CALL(app_client(), OnSodaInstallProgress(50)).Times(1);
-  speech::SodaInstaller::GetInstance()->NotifySodaDownloadProgressForTesting(
-      50);
+  speech::SodaInstaller::GetInstance()->NotifySodaProgressForTesting(50);
 }
 
 TEST_F(ProjectorSodaInstallationControllerTest, OnSodaInstallError) {
diff --git a/chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar.h b/chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar.h
index f428c1d..0f4f2192 100644
--- a/chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar.h
+++ b/chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar.h
@@ -52,7 +52,6 @@
 @property(readonly, class) NSImage* starActiveIcon;
 @property(readonly, class) NSImage* navigateStopIcon;
 @property(readonly, class) NSImage* reloadIcon;
-@property(readonly, class) NSString* homeItemIdentifier;
 
 // Returns the bridge object that BrowserWindowDefaultTouchBar uses to receive
 // notifications.
diff --git a/chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar.mm b/chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar.mm
index 6685d2f..57b35c6 100644
--- a/chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar.mm
+++ b/chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar.mm
@@ -283,13 +283,6 @@
 
   // The starred button in the touch bar.
   base::scoped_nsobject<NSButton> _starredButton;
-
-  // The last created BrowserWindowDefaultTouchBar (cached until it needs a
-  // rebuild).
-  base::scoped_nsobject<NSTouchBar> _touchBar;
-
-  // The existence of the Home button in the Touch Bar.
-  bool _touchBarHasHomeButton;
 }
 
 // Creates and returns a touch bar for tab non-fullscreen mode.
@@ -441,45 +434,37 @@
 }
 
 - (NSTouchBar*)createTabTouchBar {
-  bool showHomeButton = _notificationBridge->show_home_button();
+  base::scoped_nsobject<NSTouchBar> touchBar([[NSTouchBar alloc] init]);
+  [touchBar
+      setCustomizationIdentifier:ui::GetTouchBarId(kBrowserWindowTouchBarId)];
+  [touchBar setDelegate:self];
 
-  if (!_touchBar || _touchBarHasHomeButton != showHomeButton) {
-    _touchBar.reset([[NSTouchBar alloc] init]);
-    [_touchBar
-        setCustomizationIdentifier:ui::GetTouchBarId(kBrowserWindowTouchBarId)];
-    [_touchBar setDelegate:self];
+  NSMutableArray<NSString*>* customIdentifiers = [NSMutableArray array];
+  NSMutableArray<NSString*>* defaultIdentifiers = [NSMutableArray array];
 
-    NSMutableArray<NSString*>* customizationIdentifiers =
-        [NSMutableArray array];
-    NSMutableArray<NSString*>* defaultIdentifiers = [NSMutableArray array];
+  NSArray<NSString*>* touchBarItems = @[
+    kBackTouchId, kForwardTouchId, kReloadOrStopTouchId, kHomeTouchId,
+    kSearchTouchId, kStarTouchId, kNewTabTouchId
+  ];
 
-    NSArray<NSString*>* touchBarItemIdentifiers = @[
-      kBackTouchId, kForwardTouchId, kReloadOrStopTouchId, kHomeTouchId,
-      kSearchTouchId, kStarTouchId, kNewTabTouchId
-    ];
+  for (NSString* item in touchBarItems) {
+    NSString* itemIdentifier =
+        ui::GetTouchBarItemId(kBrowserWindowTouchBarId, item);
+    [customIdentifiers addObject:itemIdentifier];
 
-    for (NSString* itemIdentifier in touchBarItemIdentifiers) {
-      NSString* fullIdentifier =
-          ui::GetTouchBarItemId(kBrowserWindowTouchBarId, itemIdentifier);
-      [customizationIdentifiers addObject:fullIdentifier];
+    // Don't add the home button if it's not shown in the toolbar.
+    if (item == kHomeTouchId && !_notificationBridge->show_home_button())
+      continue;
 
-      // Don't add the home button if it's not shown in the toolbar.
-      if (itemIdentifier == kHomeTouchId && !showHomeButton) {
-        continue;
-      }
-
-      [defaultIdentifiers addObject:fullIdentifier];
-    }
-
-    [customizationIdentifiers addObject:NSTouchBarItemIdentifierFlexibleSpace];
-
-    [_touchBar setDefaultItemIdentifiers:defaultIdentifiers];
-    [_touchBar setCustomizationAllowedItemIdentifiers:customizationIdentifiers];
-
-    _touchBarHasHomeButton = showHomeButton;
+    [defaultIdentifiers addObject:itemIdentifier];
   }
 
-  return _touchBar.get();
+  [customIdentifiers addObject:NSTouchBarItemIdentifierFlexibleSpace];
+
+  [touchBar setDefaultItemIdentifiers:defaultIdentifiers];
+  [touchBar setCustomizationAllowedItemIdentifiers:customIdentifiers];
+
+  return touchBar.autorelease();
 }
 
 - (NSTouchBar*)createTabFullscreenTouchBar {
@@ -646,10 +631,6 @@
   return _starDefaultIcon->get();
 }
 
-+ (NSString*)homeItemIdentifier {
-  return ui::GetTouchBarItemId(kBrowserWindowTouchBarId, kHomeTouchId);
-}
-
 + (NSImage*)starActiveIcon {
   static const base::NoDestructor<base::scoped_nsobject<NSImage>>
       _starActiveIcon([]() {
diff --git a/chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar_unittest.mm b/chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar_unittest.mm
index 2d522b3ce..02144e4 100644
--- a/chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar_unittest.mm
+++ b/chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar_unittest.mm
@@ -55,16 +55,6 @@
     command_updater_->UpdateCommandEnabled(id, enabled);
   }
 
-  bool ShowsHomeButton() {
-    return browser()->profile()->GetPrefs()->GetBoolean(prefs::kShowHomeButton);
-  }
-
-  void SetShowHomeButton(bool flag) {
-    browser()->profile()->GetPrefs()->SetBoolean(prefs::kShowHomeButton, flag);
-    browser()->profile()->GetPrefs()->ChangePrefValueStore(nullptr, nullptr,
-                                                           nullptr, nullptr);
-  }
-
   void TearDown() override {
     if (@available(macOS 10.12.2, *)) {
       touch_bar_.get().browser = nullptr;
@@ -263,31 +253,3 @@
                 l10n_util::GetNSString(IDS_ACCNAME_FORWARD));
   }
 }
-
-// Tests that the home button in the Touch Bar is in sync with the setting.
-TEST_F(BrowserWindowDefaultTouchBarUnitTest, HomeUpdate) {
-  if (@available(macOS 10.12.2, *)) {
-    NSTouchBar* touch_bar = [touch_bar_ makeTouchBar];
-
-    // Save the current state before we start mucking with preferences.
-    bool home_button_showing = ShowsHomeButton();
-
-    SetShowHomeButton(false);
-    touch_bar = [touch_bar_ makeTouchBar];
-
-    NSString* home_identifier =
-        [BrowserWindowDefaultTouchBar homeItemIdentifier];
-
-    EXPECT_FALSE(
-        [[touch_bar defaultItemIdentifiers] containsObject:home_identifier]);
-
-    SetShowHomeButton(true);
-    touch_bar = [touch_bar_ makeTouchBar];
-
-    EXPECT_TRUE(
-        [[touch_bar defaultItemIdentifiers] containsObject:home_identifier]);
-
-    // Restore the original state.
-    SetShowHomeButton(home_button_showing);
-  }
-}
diff --git a/chrome/browser/ui/user_education/tutorial/tutorial.cc b/chrome/browser/ui/user_education/tutorial/tutorial.cc
index 4c2fc093..4f6b3a18 100644
--- a/chrome/browser/ui/user_education/tutorial/tutorial.cc
+++ b/chrome/browser/ui/user_education/tutorial/tutorial.cc
@@ -13,6 +13,7 @@
 #include "chrome/browser/ui/user_education/help_bubble_params.h"
 #include "chrome/browser/ui/user_education/tutorial/tutorial_description.h"
 #include "chrome/browser/ui/user_education/tutorial/tutorial_service.h"
+#include "components/vector_icons/vector_icons.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "ui/base/interaction/element_identifier.h"
 #include "ui/base/interaction/element_tracker.h"
@@ -196,6 +197,8 @@
         if (!is_last_step_) {
           params.timeout = base::TimeDelta();
           params.dismiss_callback = abort_callback;
+        } else {
+          params.body_icon = &vector_icons::kCelebrationIcon;
         }
 
         std::unique_ptr<HelpBubble> bubble =
diff --git a/chrome/browser/ui/views/global_media_controls/media_dialog_view.cc b/chrome/browser/ui/views/global_media_controls/media_dialog_view.cc
index 45d4926..01ab3b18 100644
--- a/chrome/browser/ui/views/global_media_controls/media_dialog_view.cc
+++ b/chrome/browser/ui/views/global_media_controls/media_dialog_view.cc
@@ -373,12 +373,21 @@
   live_caption_button_->SetIsOn(enabled);
 }
 
-void MediaDialogView::OnSodaInstalled() {
+void MediaDialogView::OnSodaInstalled(speech::LanguageCode language_code) {
+  if (!prefs::IsLanguageCodeForLiveCaption(language_code, profile_->GetPrefs()))
+    return;
   speech::SodaInstaller::GetInstance()->RemoveObserver(this);
   live_caption_title_->SetText(GetLiveCaptionTitle(profile_->GetPrefs()));
 }
 
-void MediaDialogView::OnSodaError() {
+void MediaDialogView::OnSodaError(speech::LanguageCode language_code) {
+  // Check that language code matches the selected language for Live Caption or
+  // is LanguageCode::kNone (signifying the SODA binary failed).
+  if (!prefs::IsLanguageCodeForLiveCaption(language_code,
+                                           profile_->GetPrefs()) &&
+      language_code != speech::LanguageCode::kNone) {
+    return;
+  }
   if (!base::FeatureList::IsEnabled(media::kLiveCaptionMultiLanguage)) {
     ToggleLiveCaption(false);
   }
@@ -387,10 +396,17 @@
       IDS_GLOBAL_MEDIA_CONTROLS_LIVE_CAPTION_DOWNLOAD_ERROR));
 }
 
-void MediaDialogView::OnSodaProgress(int combined_progress) {
+void MediaDialogView::OnSodaProgress(speech::LanguageCode language_code,
+                                     int progress) {
+  // Check that language code matches the selected language for Live Caption or
+  // is LanguageCode::kNone (signifying the SODA binary has progress).
+  if (!prefs::IsLanguageCodeForLiveCaption(language_code,
+                                           profile_->GetPrefs()) &&
+      language_code != speech::LanguageCode::kNone) {
+    return;
+  }
   live_caption_title_->SetText(l10n_util::GetStringFUTF16Int(
-      IDS_GLOBAL_MEDIA_CONTROLS_LIVE_CAPTION_DOWNLOAD_PROGRESS,
-      combined_progress));
+      IDS_GLOBAL_MEDIA_CONTROLS_LIVE_CAPTION_DOWNLOAD_PROGRESS, progress));
 }
 
 std::unique_ptr<global_media_controls::MediaItemUIView>
diff --git a/chrome/browser/ui/views/global_media_controls/media_dialog_view.h b/chrome/browser/ui/views/global_media_controls/media_dialog_view.h
index a46b09a..a50139a 100644
--- a/chrome/browser/ui/views/global_media_controls/media_dialog_view.h
+++ b/chrome/browser/ui/views/global_media_controls/media_dialog_view.h
@@ -127,9 +127,10 @@
   void UpdateBubbleSize();
 
   // SodaInstaller::Observer overrides:
-  void OnSodaInstalled() override;
-  void OnSodaError() override;
-  void OnSodaProgress(int combined_progress) override;
+  void OnSodaInstalled(speech::LanguageCode language_code) override;
+  void OnSodaError(speech::LanguageCode language_code) override;
+  void OnSodaProgress(speech::LanguageCode language_code,
+                      int progress) override;
 
   std::unique_ptr<global_media_controls::MediaItemUIView> BuildMediaItemUIView(
       const std::string& id,
diff --git a/chrome/browser/ui/views/global_media_controls/media_dialog_view_interactive_browsertest.cc b/chrome/browser/ui/views/global_media_controls/media_dialog_view_interactive_browsertest.cc
index c995529..62d2701 100644
--- a/chrome/browser/ui/views/global_media_controls/media_dialog_view_interactive_browsertest.cc
+++ b/chrome/browser/ui/views/global_media_controls/media_dialog_view_interactive_browsertest.cc
@@ -538,7 +538,7 @@
   }
 
   void OnSodaProgress(int progress) {
-    speech::SodaInstaller::GetInstance()->NotifySodaDownloadProgressForTesting(
+    speech::SodaInstaller::GetInstance()->NotifySodaProgressForTesting(
         progress);
   }
 
@@ -547,9 +547,8 @@
   }
 
   void OnSodaLanguagePackInstalled() {
-    speech::SodaInstaller::GetInstance()
-        ->NotifyOnSodaLanguagePackInstalledForTesting(
-            speech::LanguageCode::kEnUs);
+    speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting(
+        speech::LanguageCode::kEnUs);
   }
 
  protected:
diff --git a/chrome/browser/ui/views/payments/payment_request_payment_app_browsertest.cc b/chrome/browser/ui/views/payments/payment_request_payment_app_browsertest.cc
index 49aa53ec..ba34eba 100644
--- a/chrome/browser/ui/views/payments/payment_request_payment_app_browsertest.cc
+++ b/chrome/browser/ui/views/payments/payment_request_payment_app_browsertest.cc
@@ -4,6 +4,7 @@
 
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
+#include "build/build_config.h"
 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
@@ -170,7 +171,13 @@
 };
 
 // Test payment request methods are not supported by the payment app.
-IN_PROC_BROWSER_TEST_F(PaymentRequestPaymentAppTest, NotSupportedError) {
+// Flaky on Linux: http://crbug.com/1296289
+#if BUILDFLAG(IS_LINUX)
+#define MAYBE_NotSupportedError DISABLED_NotSupportedError
+#else
+#define MAYBE_NotSupportedError NotSupportedError
+#endif
+IN_PROC_BROWSER_TEST_F(PaymentRequestPaymentAppTest, MAYBE_NotSupportedError) {
   InstallAlicePayForMethod("https://frankpay.com");
 
   {
diff --git a/chrome/browser/ui/web_applications/pwa_mixed_content_browsertest.cc b/chrome/browser/ui/web_applications/pwa_mixed_content_browsertest.cc
index c80cb7bc..f8ef118f7 100644
--- a/chrome/browser/ui/web_applications/pwa_mixed_content_browsertest.cc
+++ b/chrome/browser/ui/web_applications/pwa_mixed_content_browsertest.cc
@@ -32,8 +32,9 @@
 // "REPLACE_WITH_HOST_AND_PORT" string replaced with |host_port_pair|.
 // The page at |original_path| should contain the string
 // "REPLACE_WITH_HOST_AND_PORT".
-std::string GetPathWithHostAndPortReplaced(const std::string& original_path,
-                                           net::HostPortPair host_port_pair) {
+std::string GetPathWithHostAndPortReplaced(
+    const std::string& original_path,
+    const net::HostPortPair& host_port_pair) {
   base::StringPairs replacement_text = {
       {"REPLACE_WITH_HOST_AND_PORT", host_port_pair.ToString()}};
   LOG(ERROR) << "host_port_pair.ToString() " << host_port_pair.ToString();
diff --git a/chrome/browser/ui/web_applications/sub_apps_service_impl.cc b/chrome/browser/ui/web_applications/sub_apps_service_impl.cc
index cdfcb03..794accf 100644
--- a/chrome/browser/ui/web_applications/sub_apps_service_impl.cc
+++ b/chrome/browser/ui/web_applications/sub_apps_service_impl.cc
@@ -45,7 +45,7 @@
 // *always* has the same *origin* as the calling app (normally the renderer
 // should only send the path, but a compromised renderer might send a full URL
 // instead and we guard against that here).
-GURL ResolvePathWithOrigin(const std::string& path, GURL origin) {
+GURL ResolvePathWithOrigin(const std::string& path, const GURL& origin) {
   return origin.Resolve(origin.Resolve(path).PathForRequest());
 }
 
diff --git a/chrome/browser/ui/web_applications/system_web_app_ui_utils.cc b/chrome/browser/ui/web_applications/system_web_app_ui_utils.cc
index 74b2897..cca15a3 100644
--- a/chrome/browser/ui/web_applications/system_web_app_ui_utils.cc
+++ b/chrome/browser/ui/web_applications/system_web_app_ui_utils.cc
@@ -75,7 +75,7 @@
 namespace web_app {
 
 absl::optional<SystemAppType> GetSystemWebAppTypeForAppId(Profile* profile,
-                                                          AppId app_id) {
+                                                          const AppId& app_id) {
   auto* provider = WebAppProvider::GetForSystemWebApps(profile);
   return provider ? provider->system_web_app_manager().GetSystemAppTypeForAppId(
                         app_id)
diff --git a/chrome/browser/ui/web_applications/system_web_app_ui_utils.h b/chrome/browser/ui/web_applications/system_web_app_ui_utils.h
index a0ca5100..faeb754 100644
--- a/chrome/browser/ui/web_applications/system_web_app_ui_utils.h
+++ b/chrome/browser/ui/web_applications/system_web_app_ui_utils.h
@@ -25,7 +25,7 @@
 
 // Returns the system app type for the given App ID.
 absl::optional<SystemAppType> GetSystemWebAppTypeForAppId(Profile* profile,
-                                                          AppId app_id);
+                                                          const AppId& app_id);
 
 // Returns the PWA system App ID for the given system app type.
 absl::optional<AppId> GetAppIdForSystemWebApp(Profile* profile,
diff --git a/chrome/browser/ui/web_applications/web_app_badging_browsertest.cc b/chrome/browser/ui/web_applications/web_app_badging_browsertest.cc
index a3f2537..f1e557d2 100644
--- a/chrome/browser/ui/web_applications/web_app_badging_browsertest.cc
+++ b/chrome/browser/ui/web_applications/web_app_badging_browsertest.cc
@@ -164,7 +164,7 @@
 
  protected:
   // Expects a single badge change only.
-  void ExecuteScriptAndWaitForBadgeChange(std::string script,
+  void ExecuteScriptAndWaitForBadgeChange(const std::string& script,
                                           RenderFrameHost* on) {
     ExecuteScriptAndWaitForMultipleBadgeChanges(
         script, on, /*expected_badge_change_count=*/1);
@@ -173,7 +173,7 @@
   // Handles badge changes that may affect multiple apps. Useful for testing
   // service workers, which can control many apps.
   void ExecuteScriptAndWaitForMultipleBadgeChanges(
-      std::string script,
+      const std::string& script,
       RenderFrameHost* on,
       size_t expected_badge_change_count) {
     expected_badge_change_count_ = expected_badge_change_count;
diff --git a/chrome/browser/ui/web_applications/web_app_link_capturing_browsertest.cc b/chrome/browser/ui/web_applications/web_app_link_capturing_browsertest.cc
index 64642a4..b18cb05 100644
--- a/chrome/browser/ui/web_applications/web_app_link_capturing_browsertest.cc
+++ b/chrome/browser/ui/web_applications/web_app_link_capturing_browsertest.cc
@@ -102,7 +102,7 @@
   }
 
   void InstallTestApp(GURL start_url, bool await_metric) {
-    start_url_ = start_url;
+    start_url_ = std::move(start_url);
     in_scope_1_ = start_url_.Resolve("page1.html");
     in_scope_2_ = start_url_.Resolve("page2.html");
     scope_ = start_url_.GetWithoutFilename();
@@ -136,7 +136,7 @@
     return *provider;
   }
 
-  void AddTab(Browser* browser, GURL url) {
+  void AddTab(Browser* browser, const GURL& url) {
     content::TestNavigationObserver observer(url);
     observer.StartWatchingNewWebContents();
     chrome::AddTabAt(browser, url, /*index=*/-1, /*foreground=*/true);
diff --git a/chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc b/chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc
index 89ed6c60..e23b58f 100644
--- a/chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc
+++ b/chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc
@@ -531,6 +531,15 @@
   }
 }
 
+void BindEcheDisplayStreamHandler(
+    ash::eche_app::EcheAppManager* manager,
+    mojo::PendingReceiver<ash::eche_app::mojom::DisplayStreamHandler>
+        receiver) {
+  if (manager) {
+    manager->BindDisplayStreamHandlerInterface(std::move(receiver));
+  }
+}
+
 template <>
 WebUIController* NewWebUI<ash::eche_app::EcheAppUI>(WebUI* web_ui,
                                                     const GURL& url) {
@@ -541,7 +550,8 @@
       web_ui, base::BindRepeating(&BindEcheSignalingMessageExchanger, manager),
       base::BindRepeating(&BindSystemInfoProvider, manager),
       base::BindRepeating(&BindEcheUidGenerator, manager),
-      base::BindRepeating(&BindEcheNotificationGenerator, manager));
+      base::BindRepeating(&BindEcheNotificationGenerator, manager),
+      base::BindRepeating(&BindEcheDisplayStreamHandler, manager));
 }
 
 void BindScanService(
diff --git a/chrome/browser/ui/webui/settings/captions_handler.cc b/chrome/browser/ui/webui/settings/captions_handler.cc
index d4d40db..ccdbe8e 100644
--- a/chrome/browser/ui/webui/settings/captions_handler.cc
+++ b/chrome/browser/ui/webui/settings/captions_handler.cc
@@ -73,70 +73,78 @@
 #endif
 }
 
-void CaptionsHandler::OnSodaInstalled() {
+void CaptionsHandler::OnSodaInstalled(speech::LanguageCode language_code) {
   if (!base::FeatureList::IsEnabled(media::kLiveCaptionMultiLanguage) &&
       soda_available_) {
+    // If multi-language is disabled and the language code received is not for
+    // Live Caption (perhaps it is downloading because another feature, such as
+    // dictation on ChromeOS, has a different language selected), then return
+    // early. We do not check for a matching language if multi-language is
+    // enabled because we show all of the languages' download status in the UI,
+    // even ones that are not currently selected.
+    if (!prefs::IsLanguageCodeForLiveCaption(language_code, prefs_))
+      return;
     speech::SodaInstaller::GetInstance()->RemoveObserver(this);
   }
 
   FireWebUIListener("soda-download-progress-changed",
                     base::Value(l10n_util::GetStringUTF16(
-                        IDS_SETTINGS_CAPTIONS_LIVE_CAPTION_DOWNLOAD_COMPLETE)));
-}
-
-void CaptionsHandler::OnSodaLanguagePackInstalled(
-    speech::LanguageCode language_code) {
-  if (!base::FeatureList::IsEnabled(media::kLiveCaptionMultiLanguage))
-    return;
-
-  FireWebUIListener("soda-download-progress-changed",
-                    base::Value(l10n_util::GetStringUTF16(
                         IDS_SETTINGS_CAPTIONS_LIVE_CAPTION_DOWNLOAD_COMPLETE)),
                     base::Value(speech::GetLanguageName(language_code)));
 }
 
-void CaptionsHandler::OnSodaError() {
+void CaptionsHandler::OnSodaError(speech::LanguageCode language_code) {
   if (!base::FeatureList::IsEnabled(media::kLiveCaptionMultiLanguage)) {
+    // If multi-language is disabled and the language code received is not for
+    // Live Caption (perhaps it is downloading because another feature, such as
+    // dictation on ChromeOS, has a different language selected), then return
+    // early. We do not check for a matching language if multi-language is
+    // enabled because we show all of the languages' download status in the UI,
+    // even ones that are not currently selected.
+    // Check that language code matches the selected language for Live Caption
+    // or is LanguageCode::kNone (signifying the SODA binary failed).
+    if (!prefs::IsLanguageCodeForLiveCaption(language_code, prefs_) &&
+        language_code != speech::LanguageCode::kNone) {
+      return;
+    }
     prefs_->SetBoolean(prefs::kLiveCaptionEnabled, false);
   }
 
   FireWebUIListener("soda-download-progress-changed",
                     base::Value(l10n_util::GetStringUTF16(
                         IDS_SETTINGS_CAPTIONS_LIVE_CAPTION_DOWNLOAD_ERROR)),
-                    base::Value());
-}
-
-void CaptionsHandler::OnSodaLanguagePackError(
-    speech::LanguageCode language_code) {
-  if (!base::FeatureList::IsEnabled(media::kLiveCaptionMultiLanguage))
-    return;
-
-  prefs_->SetBoolean(prefs::kLiveCaptionEnabled, false);
-  FireWebUIListener("soda-download-progress-changed",
-                    base::Value(l10n_util::GetStringUTF16(
-                        IDS_SETTINGS_CAPTIONS_LIVE_CAPTION_DOWNLOAD_ERROR)),
                     base::Value(speech::GetLanguageName(language_code)));
 }
 
-void CaptionsHandler::OnSodaProgress(int combined_progress) {
-  FireWebUIListener("soda-download-progress-changed",
-                    base::Value(l10n_util::GetStringFUTF16Int(
-                        IDS_SETTINGS_CAPTIONS_LIVE_CAPTION_DOWNLOAD_PROGRESS,
-                        combined_progress)),
-                    base::Value());
-}
-
-void CaptionsHandler::OnSodaLanguagePackProgress(
-    int language_progress,
-    speech::LanguageCode language_code) {
-  if (!base::FeatureList::IsEnabled(media::kLiveCaptionMultiLanguage))
-    return;
-
-  FireWebUIListener("soda-download-progress-changed",
-                    base::Value(l10n_util::GetStringFUTF16Int(
-                        IDS_SETTINGS_CAPTIONS_LIVE_CAPTION_DOWNLOAD_PROGRESS,
-                        language_progress)),
-                    base::Value(speech::GetLanguageName(language_code)));
+void CaptionsHandler::OnSodaProgress(speech::LanguageCode language_code,
+                                     int progress) {
+  if (!base::FeatureList::IsEnabled(media::kLiveCaptionMultiLanguage) &&
+      soda_available_) {
+    // If multi-language is disabled and the language code received is not for
+    // Live Caption (perhaps it is downloading because another feature, such as
+    // dictation on ChromeOS, has a different language selected), then return
+    // early. We do not check for a matching language if multi-language is
+    // enabled because we show all of the languages' download status in the UI,
+    // even ones that are not currently selected.
+    // Check that language code matches the selected language for Live Caption
+    // or is LanguageCode::kNone (signifying the SODA binary progress).
+    if (!prefs::IsLanguageCodeForLiveCaption(language_code, prefs_) &&
+        language_code != speech::LanguageCode::kNone) {
+      return;
+    }
+  }
+  // If the language code is kNone, this means that only the SODA binary has
+  // begun downloading. Therefore we pass the Live Caption language along to the
+  // WebUI, since that is the language which will begin downloading.
+  if (language_code == speech::LanguageCode::kNone) {
+    language_code =
+        speech::GetLanguageCode(prefs::GetLiveCaptionLanguageCode(prefs_));
+  }
+  FireWebUIListener(
+      "soda-download-progress-changed",
+      base::Value(l10n_util::GetStringFUTF16Int(
+          IDS_SETTINGS_CAPTIONS_LIVE_CAPTION_DOWNLOAD_PROGRESS, progress)),
+      base::Value(speech::GetLanguageName(language_code)));
 }
 
 }  // namespace settings
diff --git a/chrome/browser/ui/webui/settings/captions_handler.h b/chrome/browser/ui/webui/settings/captions_handler.h
index e353336d..421951d 100644
--- a/chrome/browser/ui/webui/settings/captions_handler.h
+++ b/chrome/browser/ui/webui/settings/captions_handler.h
@@ -33,13 +33,10 @@
   void HandleOpenSystemCaptionsDialog(const base::Value::List& args);
 
   // SodaInstaller::Observer overrides:
-  void OnSodaInstalled() override;
-  void OnSodaLanguagePackInstalled(speech::LanguageCode language_code) override;
-  void OnSodaError() override;
-  void OnSodaLanguagePackError(speech::LanguageCode language_code) override;
-  void OnSodaProgress(int combined_progress) override;
-  void OnSodaLanguagePackProgress(int language_progress,
-                                  speech::LanguageCode language_code) override;
+  void OnSodaInstalled(speech::LanguageCode language_code) override;
+  void OnSodaError(speech::LanguageCode language_code) override;
+  void OnSodaProgress(speech::LanguageCode language_code,
+                      int progress) override;
 
   raw_ptr<PrefService> prefs_;
   bool soda_available_ = true;
diff --git a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.cc b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.cc
index 8753b90..c2baa56c 100644
--- a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.cc
@@ -162,11 +162,10 @@
   }
 }
 
-void AccessibilityHandler::OnSodaInstallSucceeded() {
-  if (!speech::SodaInstaller::GetInstance()->IsSodaInstalled(
-          GetDictationLocale())) {
+// SodaInstaller::Observer:
+void AccessibilityHandler::OnSodaInstalled(speech::LanguageCode language_code) {
+  if (language_code != GetDictationLocale())
     return;
-  }
 
   // Only show the success message if both the SODA binary and the language pack
   // matching the Dictation locale have been downloaded.
@@ -177,16 +176,15 @@
           GetDictationLocaleDisplayName())));
 }
 
-void AccessibilityHandler::OnSodaInstallProgress(
-    int progress,
-    speech::LanguageCode language_code) {
-  // TODO(https://crbug.com/1266491): Ensure we use combined progress instead
-  // of just the language pack progress.
-  if (language_code != GetDictationLocale())
+void AccessibilityHandler::OnSodaProgress(speech::LanguageCode language_code,
+                                          int progress) {
+  if (language_code != speech::LanguageCode::kNone &&
+      language_code != GetDictationLocale()) {
     return;
+  }
 
-  // Only show the progress message if this applies to the language pack
-  // matching the Dictation locale.
+  // Only show the progress message if either the Dictation locale or the SODA
+  // binary has progress (encoded by LanguageCode::kNone).
   FireWebUIListener(
       "dictation-locale-menu-subtitle-changed",
       base::Value(l10n_util::GetStringFUTF16Int(
@@ -194,43 +192,19 @@
           progress)));
 }
 
-void AccessibilityHandler::OnSodaInstallFailed(
-    speech::LanguageCode language_code) {
-  if (language_code == speech::LanguageCode::kNone ||
-      language_code == GetDictationLocale()) {
-    // Show the failed message if either the Dictation locale failed or the SODA
-    // binary failed (encoded by LanguageCode::kNone).
-    FireWebUIListener(
-        "dictation-locale-menu-subtitle-changed",
-        base::Value(l10n_util::GetStringFUTF16(
-            IDS_SETTINGS_ACCESSIBILITY_DICTATION_SUBTITLE_SODA_DOWNLOAD_ERROR,
-            GetDictationLocaleDisplayName())));
+void AccessibilityHandler::OnSodaError(speech::LanguageCode language_code) {
+  if (language_code != speech::LanguageCode::kNone &&
+      language_code != GetDictationLocale()) {
+    return;
   }
-}
 
-// SodaInstaller::Observer:
-void AccessibilityHandler::OnSodaInstalled() {
-  OnSodaInstallSucceeded();
-}
-
-void AccessibilityHandler::OnSodaLanguagePackInstalled(
-    speech::LanguageCode language_code) {
-  OnSodaInstallSucceeded();
-}
-
-void AccessibilityHandler::OnSodaLanguagePackProgress(
-    int language_progress,
-    speech::LanguageCode language_code) {
-  OnSodaInstallProgress(language_progress, language_code);
-}
-
-void AccessibilityHandler::OnSodaError() {
-  OnSodaInstallFailed(speech::LanguageCode::kNone);
-}
-
-void AccessibilityHandler::OnSodaLanguagePackError(
-    speech::LanguageCode language_code) {
-  OnSodaInstallFailed(language_code);
+  // Show the failed message if either the Dictation locale failed or the SODA
+  // binary failed (encoded by LanguageCode::kNone).
+  FireWebUIListener(
+      "dictation-locale-menu-subtitle-changed",
+      base::Value(l10n_util::GetStringFUTF16(
+          IDS_SETTINGS_ACCESSIBILITY_DICTATION_SUBTITLE_SODA_DOWNLOAD_ERROR,
+          GetDictationLocaleDisplayName())));
 }
 
 void AccessibilityHandler::MaybeAddDictationLocales() {
diff --git a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.h b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.h
index abaeb2b..b64f767b 100644
--- a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.h
+++ b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.h
@@ -48,18 +48,12 @@
   void OpenExtensionOptionsPage(const char extension_id[]);
 
   void MaybeAddSodaInstallerObserver();
-  void OnSodaInstallSucceeded();
-  void OnSodaInstallProgress(int progress, speech::LanguageCode language_code);
-  void OnSodaInstallFailed(speech::LanguageCode language_code);
 
   // SodaInstaller::Observer:
-  void OnSodaInstalled() override;
-  void OnSodaLanguagePackInstalled(speech::LanguageCode language_code) override;
-  void OnSodaProgress(int progress) override {}
-  void OnSodaLanguagePackProgress(int language_progress,
-                                  speech::LanguageCode language_code) override;
-  void OnSodaError() override;
-  void OnSodaLanguagePackError(speech::LanguageCode language_code) override;
+  void OnSodaInstalled(speech::LanguageCode language_code) override;
+  void OnSodaProgress(speech::LanguageCode language_code,
+                      int progress) override;
+  void OnSodaError(speech::LanguageCode language_code) override;
 
   void MaybeAddDictationLocales();
   speech::LanguageCode GetDictationLocale();
diff --git a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler_browsertest.cc b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler_browsertest.cc
index 9377193..7996c4b 100644
--- a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler_browsertest.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler_browsertest.cc
@@ -151,9 +151,9 @@
   // correct language pack before doing anything.
   soda_installer()->NotifySodaInstalledForTesting();
   AssertWebUICalls(num_calls);
-  soda_installer()->NotifyOnSodaLanguagePackInstalledForTesting(en_us());
+  soda_installer()->NotifySodaInstalledForTesting(en_us());
   AssertWebUICalls(num_calls);
-  soda_installer()->NotifyOnSodaLanguagePackInstalledForTesting(fr_fr());
+  soda_installer()->NotifySodaInstalledForTesting(fr_fr());
   AssertWebUICalls(num_calls + 1);
   ASSERT_TRUE(WasWebUIListenerCalledWithStringArgument(
       "dictation-locale-menu-subtitle-changed",
@@ -165,13 +165,12 @@
 // the Dictation locale.
 IN_PROC_BROWSER_TEST_F(AccessibilityHandlerTest, OnSodaProgressNotification) {
   size_t num_calls = GetNumWebUICalls();
-  // Do not give updates for the SODA binary.
-  soda_installer()->NotifySodaDownloadProgressForTesting(50);
+  soda_installer()->NotifySodaProgressForTesting(50, fr_fr());
   AssertWebUICalls(num_calls);
-  soda_installer()->NotifyOnSodaLanguagePackProgressForTesting(50, fr_fr());
-  AssertWebUICalls(num_calls);
-  soda_installer()->NotifyOnSodaLanguagePackProgressForTesting(50, en_us());
+  soda_installer()->NotifySodaProgressForTesting(50, en_us());
   AssertWebUICalls(num_calls + 1);
+  soda_installer()->NotifySodaProgressForTesting(50);
+  AssertWebUICalls(num_calls + 2);
   ASSERT_TRUE(WasWebUIListenerCalledWithStringArgument(
       "dictation-locale-menu-subtitle-changed",
       "Downloading speech recognition files… 50%"));
@@ -197,11 +196,11 @@
   size_t num_calls = GetNumWebUICalls();
   // Do nothing if the failed language pack is different than the Dictation
   // locale.
-  soda_installer()->NotifyOnSodaLanguagePackErrorForTesting(fr_fr());
+  soda_installer()->NotifySodaErrorForTesting(fr_fr());
   AssertWebUICalls(num_calls);
   // Fire the correct listener when the language pack matching the Dictation
   // locale fails.
-  soda_installer()->NotifyOnSodaLanguagePackErrorForTesting(en_us());
+  soda_installer()->NotifySodaErrorForTesting(en_us());
   AssertWebUICalls(num_calls + 1);
   ASSERT_TRUE(WasWebUIListenerCalledWithStringArgument(
       "dictation-locale-menu-subtitle-changed",
@@ -285,6 +284,7 @@
 IN_PROC_BROWSER_TEST_F(AccessibilityHandlerTest,
                        DictationLocalesOfflineAndInstalled) {
   speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
+  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting(en_us());
   MaybeAddDictationLocales();
   base::Value::ConstListView argument;
   ASSERT_TRUE(
diff --git a/chrome/browser/ui/webui/whats_new/whats_new_util.cc b/chrome/browser/ui/webui/whats_new/whats_new_util.cc
index 99ca051..4b01269 100644
--- a/chrome/browser/ui/webui/whats_new/whats_new_util.cc
+++ b/chrome/browser/ui/webui/whats_new/whats_new_util.cc
@@ -6,6 +6,7 @@
 #include "base/bind.h"
 #include "base/callback.h"
 #include "base/check.h"
+#include "base/command_line.h"
 #include "base/feature_list.h"
 #include "base/location.h"
 #include "base/memory/raw_ptr.h"
@@ -23,6 +24,7 @@
 #include "chrome/browser/ui/browser_list_observer.h"
 #include "chrome/browser/ui/browser_tabstrip.h"
 #include "chrome/browser/ui/ui_features.h"
+#include "chrome/common/chrome_switches.h"
 #include "chrome/common/chrome_version.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/common/webui_url_constants.h"
@@ -53,9 +55,19 @@
 }
 
 bool ShouldShowForState(PrefService* local_state) {
-  if (!local_state)
+  if (!local_state || !local_state->FindPreference(prefs::kLastWhatsNewVersion))
     return false;
 
+  // Allow disabling the What's New experience in tests using the standard
+  // kNoFirstRun switch. This behavior can be overridden using the
+  // kForceWhatsNew switch for the What's New experience integration tests.
+  const base::CommandLine* command_line =
+      base::CommandLine::ForCurrentProcess();
+  if (command_line->HasSwitch(switches::kNoFirstRun) &&
+      !command_line->HasSwitch(switches::kForceWhatsNew)) {
+    return false;
+  }
+
   if (!base::FeatureList::IsEnabled(features::kChromeWhatsNewUI))
     return false;
 
diff --git a/chrome/browser/web_applications/alternative_error_page_override_info_browsertest.cc b/chrome/browser/web_applications/alternative_error_page_override_info_browsertest.cc
index 7247909..bce8375 100644
--- a/chrome/browser/web_applications/alternative_error_page_override_info_browsertest.cc
+++ b/chrome/browser/web_applications/alternative_error_page_override_info_browsertest.cc
@@ -43,7 +43,7 @@
   // Helper function to prepare PWA and retrieve information from the
   // alternative error page function.
   content::mojom::AlternativeErrorPageOverrideInfoPtr GetErrorPageInfo(
-      std::string html) {
+      base::StringPiece html) {
     ChromeContentBrowserClient browser_client;
     content::ScopedContentBrowserClientSetting setting(&browser_client);
 
diff --git a/chrome/browser/web_applications/app_service/web_app_publisher_helper.cc b/chrome/browser/web_applications/app_service/web_app_publisher_helper.cc
index 4180d1b56..6823c84 100644
--- a/chrome/browser/web_applications/app_service/web_app_publisher_helper.cc
+++ b/chrome/browser/web_applications/app_service/web_app_publisher_helper.cc
@@ -406,7 +406,7 @@
 void WebAppPublisherHelper::PopulateWebAppPermissions(
     const WebApp* web_app,
     std::vector<apps::mojom::PermissionPtr>* target) {
-  const GURL url = web_app->start_url();
+  const GURL& url = web_app->start_url();
 
   auto* host_content_settings_map =
       HostContentSettingsMapFactory::GetForProfile(profile());
@@ -450,7 +450,7 @@
     const WebApp* web_app) {
   apps::Permissions permissions;
 
-  const GURL url = web_app->start_url();
+  const GURL& url = web_app->start_url();
   auto* host_content_settings_map =
       HostContentSettingsMapFactory::GetForProfile(profile());
   DCHECK(host_content_settings_map);
diff --git a/chrome/browser/web_applications/app_service/web_apps.cc b/chrome/browser/web_applications/app_service/web_apps.cc
index f201719..c0dbec63 100644
--- a/chrome/browser/web_applications/app_service/web_apps.cc
+++ b/chrome/browser/web_applications/app_service/web_apps.cc
@@ -245,7 +245,8 @@
   }
 
   std::vector<apps::mojom::AppPtr> mojom_apps;
-  for (apps::AppPtr& app : apps) {
+  mojom_apps.reserve(apps.size());
+  for (const apps::AppPtr& app : apps) {
     mojom_apps.push_back(apps::ConvertAppToMojomApp(app));
   }
 
@@ -262,6 +263,7 @@
   }
   for (auto& subscriber : subscribers_) {
     std::vector<apps::mojom::AppPtr> cloned_apps;
+    cloned_apps.reserve(mojom_apps.size());
     for (const auto& app : mojom_apps)
       cloned_apps.push_back(app.Clone());
     subscriber->OnApps(std::move(cloned_apps),
diff --git a/chrome/browser/web_applications/daily_metrics_helper.cc b/chrome/browser/web_applications/daily_metrics_helper.cc
index 45addac2..a5fb112 100644
--- a/chrome/browser/web_applications/daily_metrics_helper.cc
+++ b/chrome/browser/web_applications/daily_metrics_helper.cc
@@ -35,7 +35,7 @@
 // This class exists just to be friended by |UkmRecorder|.
 class DesktopWebAppUkmRecorder {
  public:
-  static void Emit(DailyInteraction record) {
+  static void Emit(const DailyInteraction& record) {
     DCHECK(record.start_url.is_valid());
     ukm::SourceId source_id =
         ukm::UkmRecorder::GetSourceIdForDesktopWebAppStartUrl(record.start_url);
@@ -153,7 +153,7 @@
   url::Origin origin = url::Origin::Create(record.start_url);
   // Ensure origin is still in the history before emitting.
   ukm_background_service->GetBackgroundSourceIdIfAllowed(
-      origin, base::BindOnce(&EmitIfSourceIdExists, record));
+      origin, base::BindOnce(&EmitIfSourceIdExists, std::move(record)));
 }
 
 void EmitRecords(Profile* profile) {
@@ -206,7 +206,8 @@
 }  // namespace
 
 DailyInteraction::DailyInteraction() = default;
-DailyInteraction::DailyInteraction(GURL start_url) : start_url(start_url) {}
+DailyInteraction::DailyInteraction(GURL start_url)
+    : start_url(std::move(start_url)) {}
 DailyInteraction::DailyInteraction(const DailyInteraction&) = default;
 DailyInteraction::~DailyInteraction() = default;
 
diff --git a/chrome/browser/web_applications/external_install_options.cc b/chrome/browser/web_applications/external_install_options.cc
index ad4b2ac..7b49eb7 100644
--- a/chrome/browser/web_applications/external_install_options.cc
+++ b/chrome/browser/web_applications/external_install_options.cc
@@ -89,7 +89,7 @@
 base::Value ExternalInstallOptions::AsDebugValue() const {
   base::Value root(base::Value::Type::DICTIONARY);
 
-  auto ConvertStringList = [](const std::vector<std::string> list) {
+  auto ConvertStringList = [](const std::vector<std::string>& list) {
     base::Value list_json(base::Value::Type::LIST);
     for (const std::string& item : list)
       list_json.Append(item);
diff --git a/chrome/browser/web_applications/externally_managed_app_manager.cc b/chrome/browser/web_applications/externally_managed_app_manager.cc
index 229716f..6a26848 100644
--- a/chrome/browser/web_applications/externally_managed_app_manager.cc
+++ b/chrome/browser/web_applications/externally_managed_app_manager.cc
@@ -92,7 +92,8 @@
   DCHECK(!base::Contains(synchronize_requests_, install_source));
 
   std::vector<GURL> installed_urls;
-  for (auto apps_it : registrar_->GetExternallyInstalledApps(install_source)) {
+  for (const auto& apps_it :
+       registrar_->GetExternallyInstalledApps(install_source)) {
     // TODO: Remove this check once we cleanup ExternallyInstalledWebAppPrefs on
     // external app uninstall.
     // https://crbug.com/1300382
@@ -108,6 +109,7 @@
   std::sort(installed_urls.begin(), installed_urls.end());
 
   std::vector<GURL> desired_urls;
+  desired_urls.reserve(desired_apps_install_options.size());
   for (const auto& info : desired_apps_install_options)
     desired_urls.push_back(info.install_url);
 
@@ -146,7 +148,7 @@
 
 void ExternallyManagedAppManager::SetRegistrationCallbackForTesting(
     RegistrationCallback callback) {
-  registration_callback_ = callback;
+  registration_callback_ = std::move(callback);
 }
 
 void ExternallyManagedAppManager::ClearRegistrationCallbackForTesting() {
@@ -178,7 +180,7 @@
   auto source_and_request = synchronize_requests_.find(source);
   DCHECK(source_and_request != synchronize_requests_.end());
   SynchronizeRequest& request = source_and_request->second;
-  request.install_results[app_url] = result;
+  request.install_results[app_url] = std::move(result);
   --request.remaining_install_requests;
   DCHECK_GE(request.remaining_install_requests, 0);
 
diff --git a/chrome/browser/web_applications/externally_managed_app_manager_impl_unittest.cc b/chrome/browser/web_applications/externally_managed_app_manager_impl_unittest.cc
index 1cf617d..0fcce091 100644
--- a/chrome/browser/web_applications/externally_managed_app_manager_impl_unittest.cc
+++ b/chrome/browser/web_applications/externally_managed_app_manager_impl_unittest.cc
@@ -50,7 +50,7 @@
 using UninstallAppsResults = std::vector<std::pair<GURL, bool>>;
 
 ExternalInstallOptions GetInstallOptions(
-    GURL url,
+    const GURL& url,
     absl::optional<bool> override_previous_user_uninstall =
         absl::optional<bool>()) {
   ExternalInstallOptions options(std::move(url), DisplayMode::kBrowser,
@@ -281,7 +281,7 @@
               externally_managed_app_manager_impl->ui_manager(),
               externally_managed_app_manager_impl->finalizer(),
               externally_managed_app_manager_impl->install_manager(),
-              install_options),
+              std::move(install_options)),
           externally_managed_app_manager_impl_(
               externally_managed_app_manager_impl),
           externally_installed_app_prefs_(profile->GetPrefs()),
@@ -366,7 +366,7 @@
     ~TestExternallyManagedAppRegistrationTask() override = default;
 
    private:
-    void OnProgress(GURL install_url) {
+    void OnProgress(const GURL& install_url) {
       if (externally_managed_app_manager_impl_->MaybePreemptRegistration())
         return;
       externally_managed_app_manager_impl_->OnRegistrationFinished(
diff --git a/chrome/browser/web_applications/externally_managed_app_manager_unittest.cc b/chrome/browser/web_applications/externally_managed_app_manager_unittest.cc
index 47e1023..5d48555 100644
--- a/chrome/browser/web_applications/externally_managed_app_manager_unittest.cc
+++ b/chrome/browser/web_applications/externally_managed_app_manager_unittest.cc
@@ -79,10 +79,11 @@
     externally_managed_app_manager_.reset();
   }
 
-  void Sync(std::vector<GURL> urls) {
+  void Sync(const std::vector<GURL>& urls) {
     ResetCounts();
 
     std::vector<ExternalInstallOptions> install_options_list;
+    install_options_list.reserve(urls.size());
     for (const auto& url : urls) {
       install_options_list.emplace_back(
           url, DisplayMode::kStandalone,
@@ -104,13 +105,14 @@
 
   void Expect(int deduped_install_count,
               int deduped_uninstall_count,
-              std::vector<GURL> installed_app_urls) {
+              const std::vector<GURL>& installed_app_urls) {
     EXPECT_EQ(deduped_install_count, deduped_install_count_);
     EXPECT_EQ(deduped_uninstall_count, deduped_uninstall_count_);
     std::map<AppId, GURL> apps = app_registrar().GetExternallyInstalledApps(
         ExternalInstallSource::kInternalDefault);
     std::vector<GURL> urls;
-    for (auto it : apps)
+    urls.reserve(apps.size());
+    for (const auto& it : apps)
       urls.push_back(it.second);
 
     std::sort(urls.begin(), urls.end());
diff --git a/chrome/browser/web_applications/isolated_app_browsertest.cc b/chrome/browser/web_applications/isolated_app_browsertest.cc
index 470fd4a..485e8c25 100644
--- a/chrome/browser/web_applications/isolated_app_browsertest.cc
+++ b/chrome/browser/web_applications/isolated_app_browsertest.cc
@@ -311,7 +311,7 @@
   }
 
   std::string GetHeader(const net::test_server::HttpRequest& request,
-                        std::string header_name) {
+                        const std::string& header_name) {
     auto header = request.headers.find(header_name);
     return header != request.headers.end() ? header->second : "";
   }
diff --git a/chrome/browser/web_applications/web_app.cc b/chrome/browser/web_applications/web_app.cc
index 77229e10..5abc156a 100644
--- a/chrome/browser/web_applications/web_app.cc
+++ b/chrome/browser/web_applications/web_app.cc
@@ -408,6 +408,9 @@
 WebApp::SyncFallbackData::SyncFallbackData(
     const SyncFallbackData& sync_fallback_data) = default;
 
+WebApp::SyncFallbackData::SyncFallbackData(
+    SyncFallbackData&& sync_fallback_data) noexcept = default;
+
 WebApp::SyncFallbackData& WebApp::SyncFallbackData::operator=(
     SyncFallbackData&& sync_fallback_data) = default;
 
diff --git a/chrome/browser/web_applications/web_app.h b/chrome/browser/web_applications/web_app.h
index 3601625..320f9dd 100644
--- a/chrome/browser/web_applications/web_app.h
+++ b/chrome/browser/web_applications/web_app.h
@@ -202,6 +202,7 @@
     ~SyncFallbackData();
     // Copyable and move-assignable to support Copy-on-Write with Commit.
     SyncFallbackData(const SyncFallbackData& sync_fallback_data);
+    SyncFallbackData(SyncFallbackData&& sync_fallback_data) noexcept;
     SyncFallbackData& operator=(SyncFallbackData&& sync_fallback_data);
 
     base::Value AsDebugValue() const;
diff --git a/chrome/browser/web_applications/web_app_icon_generator.cc b/chrome/browser/web_applications/web_app_icon_generator.cc
index e61cdbf..03d34cd 100644
--- a/chrome/browser/web_applications/web_app_icon_generator.cc
+++ b/chrome/browser/web_applications/web_app_icon_generator.cc
@@ -105,7 +105,7 @@
   (*bitmaps)[output_size] = GenerateBitmap(output_size, color, icon_letter);
 }
 
-void GenerateIcons(std::set<SquareSizePx> generate_sizes,
+void GenerateIcons(const std::set<SquareSizePx>& generate_sizes,
                    char16_t icon_letter,
                    SkColor generated_icon_color,
                    SizeToBitmap* bitmap_map) {
diff --git a/chrome/browser/web_applications/web_app_icon_generator_unittest.cc b/chrome/browser/web_applications/web_app_icon_generator_unittest.cc
index bca4745..77ee106 100644
--- a/chrome/browser/web_applications/web_app_icon_generator_unittest.cc
+++ b/chrome/browser/web_applications/web_app_icon_generator_unittest.cc
@@ -87,11 +87,12 @@
   return bitmap_vector.end();
 }
 
-void ValidateIconsGeneratedAndResizedCorrectly(std::vector<SkBitmap> downloaded,
-                                               SizeToBitmap size_map,
-                                               std::set<int> sizes_to_generate,
-                                               int expected_generated,
-                                               int expected_resized) {
+void ValidateIconsGeneratedAndResizedCorrectly(
+    const std::vector<SkBitmap>& downloaded,
+    const SizeToBitmap& size_map,
+    const std::set<int>& sizes_to_generate,
+    int expected_generated,
+    int expected_resized) {
   GURL empty_url("");
   int number_generated = 0;
   int number_resized = 0;
@@ -134,7 +135,9 @@
   EXPECT_EQ(expected_resized, number_resized);
 }
 
-void ValidateBitmapSizeAndColor(SkBitmap bitmap, int size, SkColor color) {
+void ValidateBitmapSizeAndColor(const SkBitmap& bitmap,
+                                int size,
+                                SkColor color) {
   // Obtain pixel lock to access pixels.
   EXPECT_EQ(color, bitmap.getColor(0, 0));
   EXPECT_EQ(size, bitmap.width());
diff --git a/chrome/browser/web_applications/web_app_install_finalizer_unittest.cc b/chrome/browser/web_applications/web_app_install_finalizer_unittest.cc
index 823efe96..5fb827f 100644
--- a/chrome/browser/web_applications/web_app_install_finalizer_unittest.cc
+++ b/chrome/browser/web_applications/web_app_install_finalizer_unittest.cc
@@ -115,8 +115,8 @@
 
   // Synchronous version of FinalizeInstall.
   FinalizeInstallResult AwaitFinalizeInstall(
-      WebAppInstallInfo info,
-      WebAppInstallFinalizer::FinalizeOptions options) {
+      const WebAppInstallInfo& info,
+      const WebAppInstallFinalizer::FinalizeOptions& options) {
     FinalizeInstallResult result{};
     base::RunLoop run_loop;
     finalizer().FinalizeInstall(
diff --git a/chrome/browser/web_applications/web_app_install_manager_unittest.cc b/chrome/browser/web_applications/web_app_install_manager_unittest.cc
index ce6c7a5..4fbb77f 100644
--- a/chrome/browser/web_applications/web_app_install_manager_unittest.cc
+++ b/chrome/browser/web_applications/web_app_install_manager_unittest.cc
@@ -359,7 +359,7 @@
 
   std::map<SquareSizePx, SkBitmap> ReadIcons(const AppId& app_id,
                                              IconPurpose purpose,
-                                             SortedSizesPx sizes_px) {
+                                             const SortedSizesPx& sizes_px) {
     std::map<SquareSizePx, SkBitmap> result;
     base::RunLoop run_loop;
     icon_manager().ReadIcons(
diff --git a/chrome/browser/web_applications/web_app_install_task_unittest.cc b/chrome/browser/web_applications/web_app_install_task_unittest.cc
index e46b38b2..5e2b2b3 100644
--- a/chrome/browser/web_applications/web_app_install_task_unittest.cc
+++ b/chrome/browser/web_applications/web_app_install_task_unittest.cc
@@ -166,8 +166,8 @@
   }
 
   void CreateRendererAppInfo(const GURL& url,
-                             const std::string name,
-                             const std::string description,
+                             const std::string& name,
+                             const std::string& description,
                              const GURL& scope,
                              absl::optional<SkColor> theme_color,
                              DisplayMode user_display_mode) {
@@ -184,8 +184,8 @@
   }
 
   void CreateRendererAppInfo(const GURL& url,
-                             const std::string name,
-                             const std::string description) {
+                             const std::string& name,
+                             const std::string& description) {
     CreateRendererAppInfo(url, name, description, GURL(), absl::nullopt,
                           /*user_display_mode=*/DisplayMode::kStandalone);
   }
@@ -403,8 +403,8 @@
   ~WebAppInstallTaskWithRunOnOsLoginTest() override = default;
 
   void CreateRendererAppInfo(const GURL& url,
-                             const std::string name,
-                             const std::string description,
+                             const std::string& name,
+                             const std::string& description,
                              const GURL& scope,
                              absl::optional<SkColor> theme_color,
                              DisplayMode user_display_mode) {
@@ -1496,12 +1496,12 @@
   // Installs the app and validates |final_web_app_info| matches the args passed
   // in.
   InstallResult InstallWebAppWithShortcutsMenuValidateAndGetResults(
-      GURL start_url,
+      const GURL& start_url,
       SkColor theme_color,
-      std::string shortcut_name,
-      GURL shortcut_url,
+      const std::string& shortcut_name,
+      const GURL& shortcut_url,
       SquareSizePx icon_size,
-      GURL icon_src) {
+      const GURL& icon_src) {
     InstallResult result;
     auto manifest = blink::mojom::Manifest::New();
     manifest->start_url = start_url;
@@ -1573,12 +1573,12 @@
   // Updates the app and validates |final_web_app_info| matches the args passed
   // in.
   InstallResult UpdateWebAppWithShortcutsMenuValidateAndGetResults(
-      GURL url,
+      const GURL& url,
       SkColor theme_color,
-      std::string shortcut_name,
-      GURL shortcut_url,
+      const std::string& shortcut_name,
+      const GURL& shortcut_url,
       SquareSizePx icon_size,
-      GURL icon_src) {
+      const GURL& icon_src) {
     InstallResult result;
     const AppId app_id = GenerateAppId(/*manifest_id=*/absl::nullopt, url);
 
diff --git a/chrome/browser/web_applications/web_app_offline_browsertest.cc b/chrome/browser/web_applications/web_app_offline_browsertest.cc
index 580f3f1..d4576897 100644
--- a/chrome/browser/web_applications/web_app_offline_browsertest.cc
+++ b/chrome/browser/web_applications/web_app_offline_browsertest.cc
@@ -37,7 +37,7 @@
  public:
   // Start a web app without a service worker and disconnect.
   void StartWebAppAndDisconnect(content::WebContents* web_contents,
-                                std::string relative_url) {
+                                base::StringPiece relative_url) {
     GURL target_url(embedded_test_server()->GetURL(relative_url));
     web_app::NavigateToURLAndWait(browser(), target_url);
     web_app::AppId app_id = web_app::test::InstallPwaForCurrentUrl(browser());
@@ -53,7 +53,7 @@
 
   // Start a PWA with a service worker and disconnect.
   void StartPwaAndDisconnect(content::WebContents* web_contents,
-                             std::string relative_url) {
+                             base::StringPiece relative_url) {
     GURL target_url(embedded_test_server()->GetURL(relative_url));
     web_app::ServiceWorkerRegistrationWaiter registration_waiter(
         browser()->profile(), target_url);
diff --git a/chrome/browser/web_applications/web_app_origin_association_task.cc b/chrome/browser/web_applications/web_app_origin_association_task.cc
index e63dcdd..4d900120 100644
--- a/chrome/browser/web_applications/web_app_origin_association_task.cc
+++ b/chrome/browser/web_applications/web_app_origin_association_task.cc
@@ -26,7 +26,7 @@
 // count towards kMaxPathsSize.
 std::vector<std::string> GetValidPaths(std::vector<std::string> paths) {
   base::flat_set<std::string> result;
-  for (const std::string& path : paths) {
+  for (std::string& path : paths) {
     if (result.size() == kMaxPathsSize)
       break;
 
@@ -93,7 +93,7 @@
   }
 
   owner_.GetParser()->ParseWebAppOriginAssociation(
-      std::move(*file_content),
+      *file_content,
       base::BindOnce(&WebAppOriginAssociationManager::Task::OnAssociationParsed,
                      weak_ptr_factory_.GetWeakPtr()));
 }
diff --git a/chrome/browser/web_applications/web_app_proto_utils.cc b/chrome/browser/web_applications/web_app_proto_utils.cc
index fbed3c9..742da70 100644
--- a/chrome/browser/web_applications/web_app_proto_utils.cc
+++ b/chrome/browser/web_applications/web_app_proto_utils.cc
@@ -44,7 +44,7 @@
 
 absl::optional<std::vector<apps::IconInfo>> ParseAppIconInfos(
     const char* container_name_for_logging,
-    RepeatedIconInfosProto manifest_icons_proto) {
+    const RepeatedIconInfosProto& manifest_icons_proto) {
   std::vector<apps::IconInfo> manifest_icons;
   for (const sync_pb::WebAppIconInfo& icon_info_proto : manifest_icons_proto) {
     apps::IconInfo icon_info;
diff --git a/chrome/browser/web_applications/web_app_proto_utils.h b/chrome/browser/web_applications/web_app_proto_utils.h
index d93f7cd..113cd04 100644
--- a/chrome/browser/web_applications/web_app_proto_utils.h
+++ b/chrome/browser/web_applications/web_app_proto_utils.h
@@ -25,7 +25,7 @@
 
 absl::optional<std::vector<apps::IconInfo>> ParseAppIconInfos(
     const char* container_name_for_logging,
-    RepeatedIconInfosProto manifest_icons_proto);
+    const RepeatedIconInfosProto& manifest_icons_proto);
 
 // Use the given |app| to populate a |WebAppSpecifics| sync proto.
 sync_pb::WebAppSpecifics WebAppToSyncProto(const WebApp& app);
diff --git a/chrome/browser/web_applications/web_app_registrar.cc b/chrome/browser/web_applications/web_app_registrar.cc
index edb0059..f77ea9c 100644
--- a/chrome/browser/web_applications/web_app_registrar.cc
+++ b/chrome/browser/web_applications/web_app_registrar.cc
@@ -479,7 +479,7 @@
 
 bool WebAppRegistrar::IsAllowedLaunchProtocol(
     const AppId& app_id,
-    std::string protocol_scheme) const {
+    const std::string& protocol_scheme) const {
   const WebApp* web_app = GetAppById(app_id);
   return web_app &&
          base::Contains(web_app->allowed_launch_protocols(), protocol_scheme);
@@ -487,7 +487,7 @@
 
 bool WebAppRegistrar::IsDisallowedLaunchProtocol(
     const AppId& app_id,
-    std::string protocol_scheme) const {
+    const std::string& protocol_scheme) const {
   const WebApp* web_app = GetAppById(app_id);
   return web_app && base::Contains(web_app->disallowed_launch_protocols(),
                                    protocol_scheme);
diff --git a/chrome/browser/web_applications/web_app_registrar.h b/chrome/browser/web_applications/web_app_registrar.h
index 9701ee2c..02e7c498 100644
--- a/chrome/browser/web_applications/web_app_registrar.h
+++ b/chrome/browser/web_applications/web_app_registrar.h
@@ -132,12 +132,12 @@
   // Returns true if the web app with the |app_id| contains |protocol_scheme|
   // as one of its allowed launch protocols.
   bool IsAllowedLaunchProtocol(const AppId& app_id,
-                               std::string protocol_scheme) const;
+                               const std::string& protocol_scheme) const;
 
   // Returns true if the web app with the |app_id| contains |protocol_scheme|
   // as one of its disallowed launch protocols.
   bool IsDisallowedLaunchProtocol(const AppId& app_id,
-                                  std::string protocol_scheme) const;
+                                  const std::string& protocol_scheme) const;
 
   // Gets all allowed launch protocols from all installed apps.
   base::flat_set<std::string> GetAllAllowedLaunchProtocols() const;
diff --git a/chrome/browser/web_applications/web_app_sync_bridge.cc b/chrome/browser/web_applications/web_app_sync_bridge.cc
index 87b9f256..64e71dfd 100644
--- a/chrome/browser/web_applications/web_app_sync_bridge.cc
+++ b/chrome/browser/web_applications/web_app_sync_bridge.cc
@@ -67,7 +67,7 @@
   app->AddSource(Source::kSync);
 
   // app_id is a hash of start_url. Parse start_url first:
-  GURL start_url(sync_data.start_url());
+  const GURL start_url(sync_data.start_url());
   if (start_url.is_empty() || !start_url.is_valid()) {
     DLOG(ERROR) << "ApplySyncDataToApp: start_url parse error.";
     return;
@@ -91,7 +91,7 @@
   }
 
   if (app->start_url().is_empty()) {
-    app->SetStartUrl(std::move(start_url));
+    app->SetStartUrl(start_url);
   } else if (app->start_url() != start_url) {
     DLOG(ERROR)
         << "ApplySyncDataToApp: existing start_url doesn't match start_url.";
@@ -322,7 +322,7 @@
   if (!registrar_->IsInstalled(app_id))
     return;
   if (web_app)
-    web_app->SetUserPageOrdinal(page_ordinal);
+    web_app->SetUserPageOrdinal(std::move(page_ordinal));
 }
 
 void WebAppSyncBridge::SetUserLaunchOrdinal(
@@ -337,7 +337,7 @@
     return;
   WebApp* web_app = update->UpdateApp(app_id);
   if (web_app)
-    web_app->SetUserLaunchOrdinal(launch_ordinal);
+    web_app->SetUserLaunchOrdinal(std::move(launch_ordinal));
 }
 
 void WebAppSyncBridge::AddAllowedLaunchProtocol(
diff --git a/chrome/browser/web_applications/web_app_sync_bridge_unittest.cc b/chrome/browser/web_applications/web_app_sync_bridge_unittest.cc
index 4d5034f..a489694 100644
--- a/chrome/browser/web_applications/web_app_sync_bridge_unittest.cc
+++ b/chrome/browser/web_applications/web_app_sync_bridge_unittest.cc
@@ -177,7 +177,7 @@
 
 void RunCallbacksOnInstall(
     const std::vector<WebApp*>& apps,
-    FakeWebAppRegistryController::RepeatingInstallCallback callback,
+    const FakeWebAppRegistryController::RepeatingInstallCallback& callback,
     webapps::InstallResultCode code) {
   for (WebApp* app : apps)
     callback.Run(app->app_id(), code);
diff --git a/chrome/browser/web_applications/web_app_translation_manager.cc b/chrome/browser/web_applications/web_app_translation_manager.cc
index 13ac2bc..5ef6dcf 100644
--- a/chrome/browser/web_applications/web_app_translation_manager.cc
+++ b/chrome/browser/web_applications/web_app_translation_manager.cc
@@ -40,7 +40,7 @@
 }
 
 blink::Manifest::TranslationItem ConvertLocaleOverridesToTranslationItem(
-    LocaleOverrides locale_overrides) {
+    const LocaleOverrides& locale_overrides) {
   blink::Manifest::TranslationItem translation_item;
 
   if (locale_overrides.has_name()) {
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 4941a3e..a7b1e0c 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1646157575-2058350e1f5fc2f12cca4564dd13fd2ea417d5a4.profdata
+chrome-mac-arm-main-1646200174-2e50d0cc712ecc3dbd5c3cae5ce3fa4734f35673.profdata
diff --git a/chrome/common/chrome_switches.cc b/chrome/common/chrome_switches.cc
index ea6b394..67f93d2 100644
--- a/chrome/common/chrome_switches.cc
+++ b/chrome/common/chrome_switches.cc
@@ -311,6 +311,11 @@
 // whether or not it's actually the First Run (this overrides kNoFirstRun).
 const char kForceFirstRun[]                 = "force-first-run";
 
+// Displays the What's New experience when the browser is started if it has not
+// yet been shown for the current milestone (this overrides kNoFirstRun, without
+// showing the First Run experience).
+const char kForceWhatsNew[] = "force-whats-new";
+
 // Does not show the crash restore bubble when the browser is started during the
 // system startup phase in ChromeOS, if the ChromeOS full restore feature is
 // enabled, because the ChromeOS full restore notification is shown for the user
@@ -377,10 +382,13 @@
 // then restart chrome without this switch again.
 const char kNoExperiments[]                 = "no-experiments";
 
-// Skip First Run tasks, whether or not it's actually the First Run. Overridden
-// by kForceFirstRun. This does not drop the First Run sentinel and thus doesn't
-// prevent first run from occuring the next time chrome is launched without this
-// flag.
+// Skip First Run tasks, whether or not it's actually the First Run, and the
+// What's New page. Overridden by kForceFirstRun (for FRE) and kForceWhatsNew
+// (for What's New). This does not drop the First Run sentinel and thus doesn't
+// prevent first run from occurring the next time chrome is launched without
+// this flag. It also does not update the last What's New milestone, so does not
+// prevent What's New from occurring the next time chrome is launched without
+// this flag.
 const char kNoFirstRun[]                    = "no-first-run";
 
 // Don't send hyperlink auditing pings
diff --git a/chrome/common/chrome_switches.h b/chrome/common/chrome_switches.h
index 717c7e09..f79266e8 100644
--- a/chrome/common/chrome_switches.h
+++ b/chrome/common/chrome_switches.h
@@ -105,6 +105,7 @@
 extern const char kExtensionsNotWebstore[];
 extern const char kForceAppMode[];
 extern const char kForceFirstRun[];
+extern const char kForceWhatsNew[];
 extern const char kHideCrashRestoreBubble[];
 extern const char kHomePage[];
 extern const char kIncognito[];
diff --git a/chrome/common/extensions/api/web_navigation.json b/chrome/common/extensions/api/web_navigation.json
index 77f907c9..1ba0e91 100644
--- a/chrome/common/extensions/api/web_navigation.json
+++ b/chrome/common/extensions/api/web_navigation.json
@@ -30,15 +30,14 @@
             "name": "details",
             "description": "Information about the frame to retrieve information about.",
             "properties": {
-              "tabId": { "type": "integer", "optional": true, "minimum": 0, "description": "The ID of the tab in which the frame is." },
+              "tabId": { "type": "integer", "minimum": 0, "description": "The ID of the tab in which the frame is." },
               "processId": {
                 "type": "integer",
                 "optional": true,
                 "deprecated": "Frames are now uniquely identified by their tab ID and frame ID; the process ID is no longer needed and therefore ignored.",
                 "description": "The ID of the process that runs the renderer for this tab."
               },
-              "frameId": { "type": "integer", "optional": true, "minimum": 0, "description": "The ID of the frame in the given tab." },
-              "documentId": { "type": "string", "optional": true, "nodoc": true, "description": "The UUID of the document. If the frameId and/or tabId are provided they will be validated to match the document found by provided document ID." }
+              "frameId": { "type": "integer", "minimum": 0, "description": "The ID of the frame in the given tab." }
             }
           }
         ],
diff --git a/chrome/test/data/extensions/api_test/webnavigation/getFrame/test_getFrame.js b/chrome/test/data/extensions/api_test/webnavigation/getFrame/test_getFrame.js
index 61cd3d3..9a3ead6 100644
--- a/chrome/test/data/extensions/api_test/webnavigation/getFrame/test_getFrame.js
+++ b/chrome/test/data/extensions/api_test/webnavigation/getFrame/test_getFrame.js
@@ -7,9 +7,6 @@
 let ready;
 let onScriptLoad = chrome.test.loadScript(scriptUrl);
 
-const kNotSpecifiedErrorMessage =
-  'Either documentId or both tabId and frameId must be specified.';
-
 if (inServiceWorker) {
   ready = onScriptLoad;
 } else {
@@ -61,77 +58,6 @@
       });
     },
 
-    function testGetFrameNoValues() {
-      chrome.webNavigation.getFrame({},
-        function (details) {
-          chrome.test.assertEq(null, details);
-          chrome.test.assertLastError(kNotSpecifiedErrorMessage);
-          chrome.test.succeed();
-      });
-    },
-
-    function testGetFrameNoFrameId() {
-      chrome.webNavigation.getFrame({tabId: tab.id, processId: processId},
-        function (details) {
-          chrome.test.assertEq(null, details);
-          chrome.test.assertLastError(kNotSpecifiedErrorMessage);
-          chrome.test.succeed();
-      });
-    },
-
-    function testGetFrameDocumentId() {
-      chrome.webNavigation.getFrame({tabId: tab.id, documentId: documentId},
-        function (details) {
-          chrome.test.assertEq({
-              errorOccurred: false,
-              url: URL,
-              parentFrameId: -1,
-              documentId: documentId,
-              documentLifecycle: "active",
-              frameType: "outermost_frame",
-            }, details);
-          chrome.test.succeed();
-      });
-    },
-
-    function testGetFrameDocumentIdAndFrameId() {
-      chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0,
-                                     processId: processId,
-                                     documentId: documentId},
-        function (details) {
-          chrome.test.assertEq({
-              errorOccurred: false,
-              url: URL,
-              parentFrameId: -1,
-              documentId: documentId,
-              documentLifecycle: "active",
-              frameType: "outermost_frame",
-            }, details);
-          chrome.test.succeed();
-      });
-    },
-
-    function testGetFrameDocumentIdAndFrameIdDoNotMatch() {
-      chrome.webNavigation.getFrame({tabId: tab.id, frameId: 1,
-                                     processId: processId,
-                                     documentId: documentId},
-        function (details) {
-          chrome.test.assertEq(null, details);
-          chrome.test.succeed();
-      });
-    },
-
-    function testGetFrameInvalidDocumentId() {
-      chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0,
-                                     processId: processId,
-                                     documentId: "42AB"},
-        function (details) {
-          chrome.test.assertLastError("Invalid documentId.");
-          chrome.test.assertEq(null, details);
-          chrome.test.succeed();
-      });
-    },
-
     function testGetAllFrames() {
       chrome.webNavigation.getAllFrames({tabId: tab.id}, function (details) {
           chrome.test.assertEq(
diff --git a/chrome/test/data/extensions/api_test/webnavigation/targetBlank/test_targetBlank.js b/chrome/test/data/extensions/api_test/webnavigation/targetBlank/test_targetBlank.js
index a404a1b1..9533888 100644
--- a/chrome/test/data/extensions/api_test/webnavigation/targetBlank/test_targetBlank.js
+++ b/chrome/test/data/extensions/api_test/webnavigation/targetBlank/test_targetBlank.js
@@ -15,18 +15,10 @@
   var URL_TARGET = "http://127.0.0.1:" + port +
     "/extensions/api_test/webnavigation/targetBlank/b.html";
 
-  var topDocumentId;
-
   chrome.test.runTests([
     // Opens a tab and waits for the user to click on a link with
     // target=_blank in it.
     function targetBlank() {
-      // store the real documentId for the testGetFrame later.
-      chrome.webNavigation.onCommitted.addListener(
-        function(details) {
-          if (!topDocumentId)
-            topDocumentId = details.documentId;
-      });
       expect([
         { label: "a-onBeforeNavigate",
           event: "onBeforeNavigate",
@@ -135,21 +127,5 @@
       // Notify the api test that we're waiting for the user.
       chrome.test.notifyPass();
     },
-
-    // Verify GetFrame via documentId works correctly in incognito mode.
-    function testGetFrame() {
-      chrome.webNavigation.getFrame({documentId: topDocumentId},
-        function (details) {
-          chrome.test.assertEq({
-              errorOccurred: false,
-              url: URL_LOAD,
-              parentFrameId: -1,
-              documentId: topDocumentId,
-              documentLifecycle: 'active',
-              frameType: 'outermost_frame',
-            }, details);
-          chrome.test.succeed();
-      });
-    }
   ]);
 });
diff --git a/chrome/test/data/webui/settings/chromeos/multidevice_permissions_setup_dialog_tests.js b/chrome/test/data/webui/settings/chromeos/multidevice_permissions_setup_dialog_tests.js
index b618e6aa..4fd5c19 100644
--- a/chrome/test/data/webui/settings/chromeos/multidevice_permissions_setup_dialog_tests.js
+++ b/chrome/test/data/webui/settings/chromeos/multidevice_permissions_setup_dialog_tests.js
@@ -9,7 +9,7 @@
 // #import {assertEquals, assertFalse, assertNotEquals, assertTrue} from '../../chai_assert.js';
 // #import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 // #import {TestMultideviceBrowserProxy} from './test_multidevice_browser_proxy.m.js';
-// #import {MultiDeviceBrowserProxyImpl, PermissionsSetupStatus, PhoneHubPermissionsSetupMode} from 'chrome://os-settings/chromeos/os_settings.js';
+// #import {MultiDeviceBrowserProxyImpl, PermissionsSetupStatus, PhoneHubPermissionsSetupMode, SetupFlowStatus} from 'chrome://os-settings/chromeos/os_settings.js';
 // clang-format on
 
 /**
@@ -59,6 +59,13 @@
     return permissionsSetupDialog.shouldShowAppsItem_;
   }
 
+  /**
+   * @param {SetupFlowStatus} status
+   */
+  function isExpectedFlowState(setupState) {
+    return permissionsSetupDialog.flowState_ === setupState;
+  }
+
   setup(() => {
     PolymerTest.clearBody();
     browserProxy = new multidevice.TestMultideviceBrowserProxy();
@@ -84,6 +91,8 @@
     assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
     buttonContainer.querySelector('#getStartedButton').click();
     assertEquals(browserProxy.getCallCount('attemptNotificationSetup'), 1);
+    assertTrue(
+        isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
 
     simulateStatusChanged(PermissionsSetupStatus.CONNECTION_REQUESTED);
     assertFalse(isNotificationItemShowen());
@@ -145,6 +154,8 @@
     assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
     buttonContainer.querySelector('#getStartedButton').click();
     assertEquals(browserProxy.getCallCount('attemptNotificationSetup'), 1);
+    assertTrue(
+        isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
 
     simulateStatusChanged(PermissionsSetupStatus.CONNECTING);
 
@@ -168,6 +179,8 @@
     assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
     buttonContainer.querySelector('#getStartedButton').click();
     assertEquals(browserProxy.getCallCount('attemptNotificationSetup'), 1);
+    assertTrue(
+        isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
 
     simulateStatusChanged(PermissionsSetupStatus.TIMED_OUT_CONNECTING);
 
@@ -209,6 +222,8 @@
     assertFalse(!!buttonContainer.querySelector('#closeButton'));
     buttonContainer.querySelector('#getStartedButton').click();
     assertEquals(browserProxy.getCallCount('attemptNotificationSetup'), 1);
+    assertTrue(
+        isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
 
     simulateStatusChanged(
         PermissionsSetupStatus.NOTIFICATION_ACCESS_PROHIBITED);
@@ -237,6 +252,7 @@
     assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
     buttonContainer.querySelector('#getStartedButton').click();
     assertEquals(browserProxy.getCallCount('attemptAppsSetup'), 1);
+    assertTrue(isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_APPS));
 
     simulateAppsStatusChanged(PermissionsSetupStatus.CONNECTION_REQUESTED);
     assertFalse(isNotificationItemShowen());
@@ -298,6 +314,7 @@
     assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
     buttonContainer.querySelector('#getStartedButton').click();
     assertEquals(browserProxy.getCallCount('attemptAppsSetup'), 1);
+    assertTrue(isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_APPS));
 
     simulateAppsStatusChanged(PermissionsSetupStatus.CONNECTING);
 
@@ -321,6 +338,7 @@
     assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
     buttonContainer.querySelector('#getStartedButton').click();
     assertEquals(browserProxy.getCallCount('attemptAppsSetup'), 1);
+    assertTrue(isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_APPS));
 
     simulateAppsStatusChanged(PermissionsSetupStatus.TIMED_OUT_CONNECTING);
 
@@ -365,6 +383,8 @@
     assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
     buttonContainer.querySelector('#getStartedButton').click();
     assertEquals(browserProxy.getCallCount('attemptNotificationSetup'), 1);
+    assertTrue(
+        isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
 
     simulateStatusChanged(
         PermissionsSetupStatus.SENT_MESSAGE_TO_PHONE_AND_WAITING_FOR_RESPONSE);
@@ -392,6 +412,7 @@
     // becomes PermissionsSetupStatus.COMPLETED_SUCCESSFULLY.
     assertEquals(browserProxy.getCallCount('setFeatureEnabledState'), 1);
     assertEquals(browserProxy.getCallCount('attemptAppsSetup'), 1);
+    assertTrue(isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_APPS));
 
     simulateAppsStatusChanged(PermissionsSetupStatus.COMPLETED_SUCCESSFULLY);
     assertFalse(isNotificationItemShowen());
@@ -412,7 +433,6 @@
     assertFalse(permissionsSetupDialog.$$('#dialog').open);
   });
 
-
   test('Test phone enabled but ChromeOS disabled screen lock', async () => {
     permissionsSetupDialog.phonePermissionSetupMode =
         PhoneHubPermissionsSetupMode.ALL_PERMISSIONS_SETUP_MODE;
@@ -420,7 +440,15 @@
     loadTimeData.overrideValues({isPhoneScreenLockEnabled: true});
     loadTimeData.overrideValues({isChromeosScreenLockEnabled: false});
     buttonContainer.querySelector('#getStartedButton').click();
+
     assertEquals(browserProxy.getCallCount('attemptNotificationSetup'), 0);
+    assertTrue(isExpectedFlowState(SetupFlowStatus.SET_LOCKSCREEN));
+    assertFalse(isSetupInstructionsShownSeparately());
+    assertTrue(!!buttonContainer.querySelector('#learnMore'));
+    assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+    assertTrue(!!buttonContainer.querySelector('#getStartedButton'));
+    assertFalse(!!buttonContainer.querySelector('#doneButton'));
+    assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
   });
 
   test('Test phone and ChromeOS enabled screen lock', async () => {
@@ -452,4 +480,31 @@
     buttonContainer.querySelector('#getStartedButton').click();
     assertEquals(browserProxy.getCallCount('attemptNotificationSetup'), 1);
   });
+
+  test('Test screen lock UI when Eche is disabled', async () => {
+    permissionsSetupDialog.phonePermissionSetupMode =
+        PhoneHubPermissionsSetupMode.NOTIFICATION_SETUP_MODE;
+    loadTimeData.overrideValues({isEcheAppEnabled: false});
+    loadTimeData.overrideValues({isPhoneScreenLockEnabled: true});
+    loadTimeData.overrideValues({isChromeosScreenLockEnabled: false});
+    buttonContainer.querySelector('#getStartedButton').click();
+
+    assertEquals(browserProxy.getCallCount('attemptNotificationSetup'), 1);
+    assertTrue(
+        isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
+  });
+
+  test(
+      'Test screen lock UI when handling NOTIFICATION_SETUP_MODE', async () => {
+        permissionsSetupDialog.phonePermissionSetupMode =
+            PhoneHubPermissionsSetupMode.NOTIFICATION_SETUP_MODE;
+        loadTimeData.overrideValues({isEcheAppEnabled: true});
+        loadTimeData.overrideValues({isPhoneScreenLockEnabled: true});
+        loadTimeData.overrideValues({isChromeosScreenLockEnabled: false});
+        buttonContainer.querySelector('#getStartedButton').click();
+
+        assertEquals(browserProxy.getCallCount('attemptNotificationSetup'), 1);
+        assertTrue(
+            isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
+      });
 });
diff --git a/chromeos/crosapi/mojom/notification.mojom b/chromeos/crosapi/mojom/notification.mojom
index a072f42..80ff18e 100644
--- a/chromeos/crosapi/mojom/notification.mojom
+++ b/chromeos/crosapi/mojom/notification.mojom
@@ -7,6 +7,7 @@
 import "chromeos/crosapi/mojom/bitmap.mojom";
 import "mojo/public/mojom/base/string16.mojom";
 import "mojo/public/mojom/base/time.mojom";
+import "skia/public/mojom/skcolor.mojom";
 import "ui/gfx/image/mojom/image.mojom";
 import "url/mojom/url.mojom";
 
@@ -63,6 +64,9 @@
 // API. See documentation at:
 // https://developer.mozilla.org/en-US/docs/Web/API/notification
 // https://developer.chrome.com/extensions/notifications#type-NotificationOptions
+//
+//  Next MinVersion: 4
+//  Next ID: 27
 [Stable]
 struct Notification {
   // Type of notification to show.
@@ -157,4 +161,8 @@
   [MinVersion=2]
   // Whether the badge needs additional masking.
   bool badge_needs_additional_masking@25;
+
+  [MinVersion=3]
+  // Unified theme color used in new style notification.
+  skia.mojom.SkColor? accent_color@26;
 };
diff --git a/components/autofill/core/browser/payments/virtual_card_enrollment_manager.cc b/components/autofill/core/browser/payments/virtual_card_enrollment_manager.cc
index f22c9016..0ba28db 100644
--- a/components/autofill/core/browser/payments/virtual_card_enrollment_manager.cc
+++ b/components/autofill/core/browser/payments/virtual_card_enrollment_manager.cc
@@ -12,6 +12,7 @@
 #include "components/autofill/core/browser/personal_data_manager.h"
 #include "components/autofill/core/browser/strike_database.h"
 #include "components/autofill/core/browser/strike_database_base.h"
+#include "components/autofill/core/common/autofill_payments_features.h"
 #include "ui/gfx/image/image.h"
 
 namespace autofill {
@@ -288,7 +289,9 @@
   }
 
 #if !BUILDFLAG(IS_ANDROID)
-  if (state_.virtual_card_enrollment_fields.virtual_card_enrollment_source ==
+  if (base::FeatureList::IsEnabled(
+          features::kAutofillEnableToolbarStatusChip) &&
+      state_.virtual_card_enrollment_fields.virtual_card_enrollment_source ==
           VirtualCardEnrollmentSource::kUpstream &&
       !avatar_animation_complete_) {
     return;
diff --git a/components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc b/components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc
index 7922fb19..eb583edc 100644
--- a/components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc
+++ b/components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc
@@ -7,6 +7,7 @@
 #include "base/callback.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/mock_callback.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "components/autofill/core/browser/autofill_test_utils.h"
 #include "components/autofill/core/browser/data_model/credit_card.h"
@@ -20,6 +21,7 @@
 #include "components/autofill/core/browser/test_autofill_client.h"
 #include "components/autofill/core/browser/test_autofill_driver.h"
 #include "components/autofill/core/browser/test_personal_data_manager.h"
+#include "components/autofill/core/common/autofill_payments_features.h"
 #include "services/network/public/cpp/shared_url_loader_factory.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/gfx/image/image_unittest_util.h"
@@ -465,6 +467,8 @@
 }
 
 TEST_F(VirtualCardEnrollmentManagerTest, UpstreamAnimationSync_ResponseFirst) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(features::kAutofillEnableToolbarStatusChip);
   personal_data_manager_->ClearCreditCardArtImages();
   SetUpCard();
   SetValidCardArtImageForCard(*card_);
diff --git a/components/live_caption/live_caption_controller.cc b/components/live_caption/live_caption_controller.cc
index 100ff82e..7f1ffc01 100644
--- a/components/live_caption/live_caption_controller.cc
+++ b/components/live_caption/live_caption_controller.cc
@@ -142,7 +142,10 @@
   DestroyUI();
 }
 
-void LiveCaptionController::OnSodaInstalled() {
+void LiveCaptionController::OnSodaInstalled(
+    speech::LanguageCode language_code) {
+  if (!prefs::IsLanguageCodeForLiveCaption(language_code, profile_prefs_))
+    return;
   // Live Caption should always be enabled when this is called. If Live Caption
   // has been disabled, then this should not be observing the SodaInstaller
   // anymore.
diff --git a/components/live_caption/live_caption_controller.h b/components/live_caption/live_caption_controller.h
index 3d54187f..f568c8a 100644
--- a/components/live_caption/live_caption_controller.h
+++ b/components/live_caption/live_caption_controller.h
@@ -83,9 +83,10 @@
   friend class LiveCaptionSpeechRecognitionHostTest;
 
   // SodaInstaller::Observer:
-  void OnSodaInstalled() override;
-  void OnSodaProgress(int combined_progress) override {}
-  void OnSodaError() override {}
+  void OnSodaInstalled(speech::LanguageCode language_code) override;
+  void OnSodaProgress(speech::LanguageCode language_code,
+                      int progress) override {}
+  void OnSodaError(speech::LanguageCode language_code) override {}
 
   // ui::NativeThemeObserver:
   void OnNativeThemeUpdated(ui::NativeTheme* observed_theme) override {}
diff --git a/components/live_caption/pref_names.cc b/components/live_caption/pref_names.cc
index 79adaac..cf4bb31 100644
--- a/components/live_caption/pref_names.cc
+++ b/components/live_caption/pref_names.cc
@@ -34,6 +34,12 @@
   return speech::kUsEnglishLocale;
 }
 
+bool IsLanguageCodeForLiveCaption(speech::LanguageCode language_code,
+                                  PrefService* profile_prefs) {
+  return language_code ==
+         speech::GetLanguageCode(GetLiveCaptionLanguageCode(profile_prefs));
+}
+
 #endif  // !defined(ANDROID)
 
 // String indicating the size of the captions text as a percentage.
diff --git a/components/live_caption/pref_names.h b/components/live_caption/pref_names.h
index 7c137cd..d7b4270f 100644
--- a/components/live_caption/pref_names.h
+++ b/components/live_caption/pref_names.h
@@ -7,6 +7,12 @@
 
 #include <string>
 
+#include "build/build_config.h"
+
+#if !BUILDFLAG(IS_ANDROID)
+#include "components/soda/constants.h"
+#endif
+
 class PrefService;
 
 namespace prefs {
@@ -18,6 +24,9 @@
 extern const char kLiveCaptionLanguageCode[];
 
 const std::string GetLiveCaptionLanguageCode(PrefService* profile_prefs);
+bool IsLanguageCodeForLiveCaption(speech::LanguageCode language_code,
+                                  PrefService* profile_prefs);
+
 #endif  // !defined(ANDROID)
 
 // These kAccessibilityCaptions* caption style prefs are used on Android
diff --git a/components/query_tiles/internal/tile_config.cc b/components/query_tiles/internal/tile_config.cc
index 1c2d519..cce11da 100644
--- a/components/query_tiles/internal/tile_config.cc
+++ b/components/query_tiles/internal/tile_config.cc
@@ -182,7 +182,10 @@
 
   std::string tag = base::GetFieldTrialParamValueByFeature(
       features::kQueryTiles, kExperimentTagKey);
-  if (tag.empty() && features::IsQueryTilesEnabledForCountry(country_code)) {
+  if (tag.empty() &&
+      !base::FeatureList::IsEnabled(
+          query_tiles::features::kQueryTilesDisableCountryOverride) &&
+      features::IsQueryTilesEnabledForCountry(country_code)) {
     return kDefaultExperimentTagForTrendingEnabledCountries;
   }
   return tag;
diff --git a/components/query_tiles/switches.cc b/components/query_tiles/switches.cc
index fabb95c..ba0f30d 100644
--- a/components/query_tiles/switches.cc
+++ b/components/query_tiles/switches.cc
@@ -25,6 +25,9 @@
 const base::Feature kQueryTilesSegmentation{"QueryTilesSegmentation",
                                             base::FEATURE_ENABLED_BY_DEFAULT};
 
+const base::Feature kQueryTilesDisableCountryOverride{
+    "QueryTilesDisableCountryOverride", base::FEATURE_DISABLED_BY_DEFAULT};
+
 bool IsQueryTilesEnabledForCountry(const std::string& country_code) {
   std::string enabled_countries[] = {"IN", "NG"};
   for (const auto& country : enabled_countries) {
diff --git a/components/query_tiles/switches.h b/components/query_tiles/switches.h
index b6b0f8f..4ee5a9e 100644
--- a/components/query_tiles/switches.h
+++ b/components/query_tiles/switches.h
@@ -37,6 +37,9 @@
 // Whether segmentation rules are applied to query tiles.
 extern const base::Feature kQueryTilesSegmentation;
 
+// Whether to disable the override rules introduced for countries.
+extern const base::Feature kQueryTilesDisableCountryOverride;
+
 // Returns whether query tiles are enabled for the country.
 bool IsQueryTilesEnabledForCountry(const std::string& country_code);
 }  // namespace features
diff --git a/components/segmentation_platform/internal/BUILD.gn b/components/segmentation_platform/internal/BUILD.gn
index 7f6e41630..02ca6180 100644
--- a/components/segmentation_platform/internal/BUILD.gn
+++ b/components/segmentation_platform/internal/BUILD.gn
@@ -57,6 +57,9 @@
     "execution/model_execution_manager_factory.cc",
     "execution/model_execution_manager_factory.h",
     "execution/model_execution_status.h",
+    "execution/query_processor.h",
+    "execution/sql_feature_processor.cc",
+    "execution/sql_feature_processor.h",
     "execution/uma_feature_processor.cc",
     "execution/uma_feature_processor.h",
     "platform_options.cc",
@@ -150,6 +153,7 @@
   # IMPORTANT NOTE: When adding new tests, also remember to update the list of
   # tests in //components/segmentation_platform/components_unittests.filter
   sources = [
+    "data_collection/training_data_collector_unittest.cc",
     "database/database_maintenance_impl_unittest.cc",
     "database/metadata_utils_unittest.cc",
     "database/mock_signal_database.cc",
@@ -172,7 +176,6 @@
     "execution/mock_feature_list_query_processor.cc",
     "execution/mock_feature_list_query_processor.h",
     "execution/model_execution_manager_factory_unittest.cc",
-    "execution/query_processor.h",
     "mock_ukm_data_manager.cc",
     "mock_ukm_data_manager.h",
     "scheduler/model_execution_scheduler_unittest.cc",
diff --git a/components/segmentation_platform/internal/data_collection/training_data_collector.cc b/components/segmentation_platform/internal/data_collection/training_data_collector.cc
index e34fc6a..550a75a 100644
--- a/components/segmentation_platform/internal/data_collection/training_data_collector.cc
+++ b/components/segmentation_platform/internal/data_collection/training_data_collector.cc
@@ -4,39 +4,190 @@
 
 #include "components/segmentation_platform/internal/data_collection/training_data_collector.h"
 
+#include "base/containers/flat_map.h"
+#include "base/containers/flat_set.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/metrics/histogram_base.h"
+#include "base/metrics/metrics_hashes.h"
 #include "base/notreached.h"
+#include "base/time/clock.h"
+#include "components/optimization_guide/proto/models.pb.h"
+#include "components/segmentation_platform/internal/database/metadata_utils.h"
+#include "components/segmentation_platform/internal/database/segment_info_database.h"
 #include "components/segmentation_platform/internal/execution/feature_list_query_processor.h"
+#include "components/segmentation_platform/internal/proto/model_metadata.pb.h"
+#include "components/segmentation_platform/internal/proto/model_prediction.pb.h"
+#include "components/segmentation_platform/internal/segmentation_ukm_helper.h"
+
+using optimization_guide::proto::OptimizationTarget;
+
+const int kInvalidModelVersion = 0;
 
 namespace segmentation_platform {
+namespace {
 
-TrainingDataCollector::TrainingDataCollector(
+// Parse outputs into a map of metric hash of the uma output and its index in
+// the output list.
+std::map<uint64_t, int> ParseUmaOutputs(
+    const proto::SegmentationModelMetadata& metadata) {
+  std::map<uint64_t, int> hash_index_map;
+  if (!metadata.has_training_outputs())
+    return hash_index_map;
+
+  const auto& training_outputs = metadata.training_outputs();
+  for (int i = 0; i < training_outputs.outputs_size(); ++i) {
+    const auto output = training_outputs.outputs(i);
+    if (!output.has_uma_output() || !output.uma_output().has_uma_feature())
+      continue;
+
+    hash_index_map[output.uma_output().uma_feature().name_hash()] = i;
+  }
+  return hash_index_map;
+}
+
+}  // namespace
+
+class TrainingDataCollectorImpl : public TrainingDataCollector {
+ public:
+  TrainingDataCollectorImpl(SegmentInfoDatabase* segment_info_database,
+                            FeatureListQueryProcessor* processor,
+                            HistogramSignalHandler* histogram_signal_handler,
+                            base::Clock* clock)
+      : segment_info_database_(segment_info_database),
+        feature_list_query_processor_(processor),
+        histogram_signal_handler_(histogram_signal_handler),
+        clock_(clock) {}
+
+  ~TrainingDataCollectorImpl() override {
+    histogram_signal_handler_->RemoveObserver(this);
+  }
+
+ private:
+  // TrainingDataCollector implementation.
+  void OnModelMetadataUpdated() override { NOTIMPLEMENTED(); }
+
+  void OnServiceInitialized() override {
+    // TODO(xingliu): Filter out segments that doesn't contain enough data.
+    // Maybe reuse ModelExecutionSchedulerImpl::FilterEligibleSegments.
+    segment_info_database_->GetAllSegmentInfo(
+        base::BindOnce(&TrainingDataCollectorImpl::OnGetSegmentsInfoList,
+                       weak_ptr_factory_.GetWeakPtr()));
+  }
+
+  void OnGetSegmentsInfoList(
+      std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> segments) {
+    histogram_signal_handler_->AddObserver(this);
+
+    DCHECK(segments);
+    for (const auto& segment : *segments) {
+      const proto::SegmentInfo& segment_info = segment.second;
+      // Validate segment info.
+      auto validation_result =
+          metadata_utils::ValidateSegmentInfo(segment_info);
+      // TODO(xingliu): Record histogram for errors.
+      if (validation_result !=
+          metadata_utils::ValidationResult::kValidationSuccess) {
+        VLOG(1) << "Segment info validation failed for optimization target: "
+                << segment.first << ", validation result:"
+                << static_cast<int>(validation_result);
+        continue;
+      }
+
+      // Cache the histograms as outputs of training data, which needs to be
+      // immediately reported when the histogram is recorded.
+      auto hash_index_map = ParseUmaOutputs(segment_info.model_metadata());
+      for (const auto& hash_index : hash_index_map) {
+        immediate_collection_histograms_[hash_index.first].emplace(
+            segment.first);
+      }
+    }
+  }
+
+  // HistogramSignalHandler::Observer implementation.
+  void OnHistogramSignalUpdated(const std::string& histogram_name,
+                                base::HistogramBase::Sample sample) override {
+    auto hash = base::HashMetricName(histogram_name);
+    auto it = immediate_collection_histograms_.find(hash);
+    // Report training data for all models that are interested in
+    // |histogram_name| as output.
+    if (it != immediate_collection_histograms_.end()) {
+      std::vector<OptimizationTarget> optimization_targets(it->second.begin(),
+                                                           it->second.end());
+      segment_info_database_->GetSegmentInfoForSegments(
+          optimization_targets,
+          base::BindOnce(&TrainingDataCollectorImpl::ReportForSegmentsInfoList,
+                         weak_ptr_factory_.GetWeakPtr(), hash, sample));
+    }
+  }
+
+  void ReportForSegmentsInfoList(
+      uint64_t output_metric_hash,
+      base::HistogramBase::Sample output_metric_sample,
+      std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> segments) {
+    DCHECK(segments);
+    for (const auto& segment : *segments) {
+      const proto::SegmentInfo& segment_info = segment.second;
+      // Figure out the output index.
+      auto hash_index_map = ParseUmaOutputs(segment_info.model_metadata());
+      if (hash_index_map.find(output_metric_hash) == hash_index_map.end())
+        continue;
+
+      // Generate training data input.
+      // TODO(xingliu): Validate immediate output is not included in the input
+      // features and update the comment in model_metadata.proto.
+      feature_list_query_processor_->ProcessFeatureList(
+          segment_info.model_metadata(), segment_info.segment_id(),
+          clock_->Now(),
+          base::BindOnce(&TrainingDataCollectorImpl::OnGetInputTensor,
+                         weak_ptr_factory_.GetWeakPtr(),
+                         static_cast<float>(output_metric_sample),
+                         hash_index_map[output_metric_hash],
+                         segment_info.segment_id()));
+    }
+  }
+
+  void OnGetInputTensor(float output_value,
+                        int output_index,
+                        OptimizationTarget segment_id,
+                        bool success,
+                        const std::vector<float>& inputs) {
+    if (!success)
+      return;
+
+    // TODO(xingliu): Plumb model version to here.
+    auto ukm_source_id =
+        SegmentationUkmHelper::GetInstance()->RecordTrainingData(
+            segment_id, /*model_version=*/kInvalidModelVersion, inputs,
+            {output_value}, {output_index});
+    if (ukm_source_id == ukm::kInvalidSourceId) {
+      VLOG(1) << "Failed to collect training data for segment:" << segment_id;
+    }
+  }
+
+  raw_ptr<SegmentInfoDatabase> segment_info_database_;
+  raw_ptr<FeatureListQueryProcessor> feature_list_query_processor_;
+  raw_ptr<HistogramSignalHandler> histogram_signal_handler_;
+  raw_ptr<base::Clock> clock_;
+
+  // Hash of histograms for immediate training data collection. When any
+  // histogram hash contained in the map is recorded, a UKM message is reported
+  // right away.
+  base::flat_map<uint64_t,
+                 base::flat_set<optimization_guide::proto::OptimizationTarget>>
+      immediate_collection_histograms_;
+
+  base::WeakPtrFactory<TrainingDataCollectorImpl> weak_ptr_factory_{this};
+};
+
+// static
+std::unique_ptr<TrainingDataCollector> TrainingDataCollector::Create(
+    SegmentInfoDatabase* segment_info_database,
     FeatureListQueryProcessor* processor,
-    HistogramSignalHandler* histogram_signal_handler)
-    : feature_list_query_processor_(processor),
-      histogram_signal_handler_(histogram_signal_handler) {
-  DCHECK(histogram_signal_handler_);
-  histogram_signal_handler_->AddObserver(this);
-}
-
-TrainingDataCollector::~TrainingDataCollector() {
-  DCHECK(histogram_signal_handler_);
-  histogram_signal_handler_->RemoveObserver(this);
-}
-
-void TrainingDataCollector::OnModelMetadataUpdated() {
-  NOTIMPLEMENTED();
-}
-
-void TrainingDataCollector::OnServiceInitialized() {
-  NOTIMPLEMENTED();
-}
-
-void TrainingDataCollector::OnHistogramSignalUpdated(
-    const std::string& histogram_name,
-    base::HistogramBase::Sample) {
-  // TODO(xingliu): Check whether the histogram needs to trigger a data
-  // collection, and report to UKM.
-  NOTIMPLEMENTED();
+    HistogramSignalHandler* histogram_signal_handler,
+    base::Clock* clock) {
+  return std::make_unique<TrainingDataCollectorImpl>(
+      segment_info_database, processor, histogram_signal_handler, clock);
 }
 
 }  // namespace segmentation_platform
diff --git a/components/segmentation_platform/internal/data_collection/training_data_collector.h b/components/segmentation_platform/internal/data_collection/training_data_collector.h
index 0b3cb14b..bfc2df6a 100644
--- a/components/segmentation_platform/internal/data_collection/training_data_collector.h
+++ b/components/segmentation_platform/internal/data_collection/training_data_collector.h
@@ -5,43 +5,47 @@
 #ifndef COMPONENTS_SEGMENTATION_PLATFORM_INTERNAL_DATA_COLLECTION_TRAINING_DATA_COLLECTOR_H_
 #define COMPONENTS_SEGMENTATION_PLATFORM_INTERNAL_DATA_COLLECTION_TRAINING_DATA_COLLECTOR_H_
 
-#include "base/memory/raw_ptr.h"
-#include "base/metrics/histogram_base.h"
+#include <memory>
+
 #include "components/segmentation_platform/internal/signals/histogram_signal_handler.h"
 
+namespace base {
+class Clock;
+}  // namespace base
+
 namespace segmentation_platform {
 
 class FeatureListQueryProcessor;
 class HistogramSignalHandler;
+class SegmentInfoDatabase;
 
 // Collect training data and report as Ukm message. Live on main thread.
 // TODO(xingliu): Make a new class that owns the training data collector and
 // model execution collector.
 class TrainingDataCollector : public HistogramSignalHandler::Observer {
  public:
-  TrainingDataCollector(FeatureListQueryProcessor* processor,
-                        HistogramSignalHandler* histogram_signal_handler);
-  ~TrainingDataCollector() override;
+  static std::unique_ptr<TrainingDataCollector> Create(
+      SegmentInfoDatabase* segment_info_database,
+      FeatureListQueryProcessor* processor,
+      HistogramSignalHandler* histogram_signal_handler,
+      base::Clock* clock);
+
+  // Called when model metadata is updated. May result in training data
+  // collection behavior change.
+  virtual void OnModelMetadataUpdated() = 0;
+
+  // Called after segmentation platform is initialized. May report training data
+  // to Ukm for |UMAOutput| in |SegmentationModelMetadata|.
+  virtual void OnServiceInitialized() = 0;
+
+  ~TrainingDataCollector() override = default;
 
   // Disallow copy/assign.
   TrainingDataCollector(const TrainingDataCollector&) = delete;
   TrainingDataCollector& operator=(const TrainingDataCollector&) = delete;
 
-  // Called when model metadata is updated. May result in training data
-  // collection behavior change.
-  void OnModelMetadataUpdated();
-
-  // Called after segmentation platform is initialized. May report training data
-  // to Ukm that has a non-zero |duration| field in |UMAOutput|.
-  void OnServiceInitialized();
-
-  // HistogramSignalHandler::Observer overrides.
-  void OnHistogramSignalUpdated(const std::string& histogram_name,
-                                base::HistogramBase::Sample) override;
-
- private:
-  raw_ptr<FeatureListQueryProcessor> feature_list_query_processor_;
-  raw_ptr<HistogramSignalHandler> histogram_signal_handler_;
+ protected:
+  TrainingDataCollector() = default;
 };
 
 }  // namespace segmentation_platform
diff --git a/components/segmentation_platform/internal/data_collection/training_data_collector_unittest.cc b/components/segmentation_platform/internal/data_collection/training_data_collector_unittest.cc
index 0c792a92..e9ddca3 100644
--- a/components/segmentation_platform/internal/data_collection/training_data_collector_unittest.cc
+++ b/components/segmentation_platform/internal/data_collection/training_data_collector_unittest.cc
@@ -4,10 +4,40 @@
 
 #include "components/segmentation_platform/internal/data_collection/training_data_collector.h"
 
+#include <map>
+
+#include "base/metrics/metrics_hashes.h"
+#include "base/test/gmock_callback_support.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/simple_test_clock.h"
+#include "base/test/task_environment.h"
+#include "components/segmentation_platform/internal/database/test_segment_info_database.h"
 #include "components/segmentation_platform/internal/execution/mock_feature_list_query_processor.h"
+#include "components/segmentation_platform/internal/proto/model_metadata.pb.h"
+#include "components/segmentation_platform/internal/proto/model_prediction.pb.h"
+#include "components/segmentation_platform/internal/segmentation_ukm_helper.h"
 #include "components/segmentation_platform/internal/signals/mock_histogram_signal_handler.h"
+#include "components/segmentation_platform/public/config.h"
+#include "components/segmentation_platform/public/features.h"
+#include "components/ukm/test_ukm_recorder.h"
+#include "services/metrics/public/cpp/ukm_builders.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+using ::base::test::RunOnceCallback;
+using ::testing::_;
+using ::testing::NiceMock;
+using Segmentation_ModelExecution =
+    ::ukm::builders::Segmentation_ModelExecution;
+
+constexpr auto kTestOptimizationTarget0 =
+    OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_NEW_TAB;
+constexpr auto kTestOptimizationTarget1 =
+    OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_SHARE;
+constexpr char kHistogramName0[] = "histogram0";
+constexpr char kHistogramName1[] = "histogram1";
+
+constexpr int kSample = 1;
+
 namespace segmentation_platform {
 namespace {
 
@@ -17,24 +47,142 @@
   ~TrainingDataCollectorTest() override = default;
 
   void SetUp() override {
-    collector_ = std::make_unique<TrainingDataCollector>(
-        &feature_list_processor_, &histogram_signal_handler_);
+    test_recorder_.Purge();
+
+    // Allow two models to collect training data.
+    std::map<std::string, std::string> params = {
+        {kSegmentIdsAllowedForReportingKey, "4,5"}};
+    feature_list_.InitAndEnableFeatureWithParameters(
+        features::kSegmentationStructuredMetricsFeature, params);
+
+    // Setup behavior for |feature_list_processor_|.
+    std::vector<float> inputs({1.f});
+    ON_CALL(feature_list_processor_, ProcessFeatureList(_, _, _, _))
+        .WillByDefault(RunOnceCallback<3>(true, inputs));
+
+    test_segment_info_db_ = std::make_unique<test::TestSegmentInfoDatabase>();
+    collector_ = TrainingDataCollector::Create(
+        test_segment_info_db_.get(), &feature_list_processor_,
+        &histogram_signal_handler_, &clock_);
   }
 
  protected:
   TrainingDataCollector* collector() { return collector_.get(); }
+  test::TestSegmentInfoDatabase* test_segment_db() {
+    return test_segment_info_db_.get();
+  }
+  base::test::TaskEnvironment* task_environment() { return &task_environment_; }
+
+  void CreateSegmentInfo() {
+    test_segment_db()->AddUserActionFeature(kTestOptimizationTarget0, "action",
+                                            1, 1, proto::Aggregation::COUNT);
+    // Segment 0 contains 1 immediate collection uma output for for
+    // |kHistogramName0|, 1 continuous collection output for for
+    // |kHistogramName1|.
+    auto* segment_info = CreateSegment(kTestOptimizationTarget0);
+    AddOutput(segment_info, kHistogramName0);
+    proto::TrainingOutput* output1 = AddOutput(segment_info, kHistogramName1);
+    output1->mutable_uma_output()->set_duration(1u);
+  }
+
+  proto::SegmentInfo* CreateSegment(OptimizationTarget optimization_target) {
+    auto* segment_info =
+        test_segment_db()->FindOrCreateSegment(optimization_target);
+    segment_info->mutable_model_metadata()->set_time_unit(proto::TimeUnit::DAY);
+    return segment_info;
+  }
+
+  proto::TrainingOutput* AddOutput(proto::SegmentInfo* segment_info,
+                                   const std::string& histgram_name) {
+    auto* output = segment_info->mutable_model_metadata()
+                       ->mutable_training_outputs()
+                       ->add_outputs();
+    auto* uma_feature = output->mutable_uma_output()->mutable_uma_feature();
+    uma_feature->set_name(histgram_name);
+    uma_feature->set_name_hash(base::HashMetricName(histgram_name));
+    return output;
+  }
+
+  // TODO(xingliu): Share this test code with SegmentationUkmHelperTest, or test
+  // with mock SegmentationUkmHelperTest.
+  void ExpectUkm(std::vector<base::StringPiece> metric_names,
+                 std::vector<int64_t> expected_values) {
+    const auto& entries = test_recorder_.GetEntriesByName(
+        Segmentation_ModelExecution::kEntryName);
+    ASSERT_EQ(1u, entries.size());
+    for (size_t i = 0; i < metric_names.size(); ++i) {
+      test_recorder_.ExpectEntryMetric(entries[0], metric_names[i],
+                                       expected_values[i]);
+    }
+  }
+
+  void ExpectUkmCount(size_t count) {
+    const auto& entries = test_recorder_.GetEntriesByName(
+        Segmentation_ModelExecution::kEntryName);
+    ASSERT_EQ(count, entries.size());
+  }
+
+  void Init() {
+    collector()->OnServiceInitialized();
+    task_environment()->RunUntilIdle();
+  }
+
+  void WaitForHistogramSignalUpdated(const std::string& histogram_name,
+                                     base::HistogramBase::Sample sample) {
+    base::RunLoop run_loop;
+    test_recorder_.SetOnAddEntryCallback(
+        Segmentation_ModelExecution::kEntryName, run_loop.QuitClosure());
+    collector_->OnHistogramSignalUpdated(histogram_name, sample);
+    run_loop.Run();
+  }
 
  private:
-  MockFeatureListQueryProcessor feature_list_processor_;
-  MockHistogramSignalHandler histogram_signal_handler_;
+  base::SimpleTestClock clock_;
+  base::test::TaskEnvironment task_environment_;
+  base::test::ScopedFeatureList feature_list_;
+  ukm::TestAutoSetUkmRecorder test_recorder_;
+  NiceMock<MockFeatureListQueryProcessor> feature_list_processor_;
+  NiceMock<MockHistogramSignalHandler> histogram_signal_handler_;
+  std::unique_ptr<test::TestSegmentInfoDatabase> test_segment_info_db_;
   std::unique_ptr<TrainingDataCollector> collector_;
 };
 
-// Place holder test case that will be replaced to test real implementation
-// logic.
-TEST_F(TrainingDataCollectorTest, Construction) {
-  // TODO(xingliu): Remove this once read test cases are added.
-  EXPECT_NE(nullptr, collector());
+// No segment info in database. Do nothing.
+TEST_F(TrainingDataCollectorTest, NoSegment) {
+  Init();
+  collector()->OnHistogramSignalUpdated(kHistogramName0, kSample);
+  task_environment()->RunUntilIdle();
+  ExpectUkmCount(0u);
+}
+
+// No segment info in database. Do nothing.
+TEST_F(TrainingDataCollectorTest, IrrelevantHistogramNotReported) {
+  CreateSegmentInfo();
+  Init();
+  collector()->OnHistogramSignalUpdated("irrelevant_histogram", kSample);
+  task_environment()->RunUntilIdle();
+  ExpectUkmCount(0u);
+}
+
+TEST_F(TrainingDataCollectorTest, HistogramImmediatelyReported) {
+  CreateSegmentInfo();
+  Init();
+  WaitForHistogramSignalUpdated(kHistogramName0, kSample);
+  ExpectUkm(
+      {Segmentation_ModelExecution::kOptimizationTargetName,
+       Segmentation_ModelExecution::kActualResultName},
+      {kTestOptimizationTarget0, SegmentationUkmHelper::FloatToInt64(kSample)});
+}
+
+TEST_F(TrainingDataCollectorTest, HistogramImmediatelyReported_MultipleModel) {
+  CreateSegmentInfo();
+  // Segment 1 contains 1 immediate collection uma output for for
+  // |kHistogramName0|
+  auto* segment_info = CreateSegment(kTestOptimizationTarget1);
+  AddOutput(segment_info, kHistogramName0);
+  Init();
+  WaitForHistogramSignalUpdated(kHistogramName0, kSample);
+  ExpectUkmCount(2u);
 }
 
 }  // namespace
diff --git a/components/segmentation_platform/internal/execution/feature_list_query_processor.cc b/components/segmentation_platform/internal/execution/feature_list_query_processor.cc
index 515e4082..008b607 100644
--- a/components/segmentation_platform/internal/execution/feature_list_query_processor.cc
+++ b/components/segmentation_platform/internal/execution/feature_list_query_processor.cc
@@ -12,12 +12,18 @@
 #include "components/segmentation_platform/internal/database/metadata_utils.h"
 #include "components/segmentation_platform/internal/execution/custom_input_processor.h"
 #include "components/segmentation_platform/internal/execution/feature_processor_state.h"
+#include "components/segmentation_platform/internal/execution/sql_feature_processor.h"
 #include "components/segmentation_platform/internal/execution/uma_feature_processor.h"
 #include "components/segmentation_platform/internal/proto/model_metadata.pb.h"
 #include "components/segmentation_platform/internal/stats.h"
 
 namespace segmentation_platform {
 
+namespace {
+// Index not actually used for legacy code in FeatureQueryProcessor.
+const int kIndexNotUsed = 0;
+}  // namespace
+
 FeatureListQueryProcessor::FeatureListQueryProcessor(
     SignalDatabase* signal_database,
     std::unique_ptr<FeatureAggregator> feature_aggregator)
@@ -78,7 +84,25 @@
         input_feature.custom_input(), std::move(feature_processor_state),
         base::BindOnce(&FeatureListQueryProcessor::ProcessNextInputFeature,
                        weak_ptr_factory_.GetWeakPtr()));
+  } else if (input_feature.has_sql_feature()) {
+    std::map<SqlFeatureProcessor::FeatureIndex, proto::SqlFeature> queries = {
+        {kIndexNotUsed, input_feature.sql_feature()}};
+    auto sql_feature_processor = std::make_unique<SqlFeatureProcessor>(queries);
+    auto* sql_feature_processor_ptr = sql_feature_processor.get();
+    sql_feature_processor_ptr->Process(
+        std::move(feature_processor_state),
+        base::BindOnce(&FeatureListQueryProcessor::OnSqlQueryProcessed,
+                       weak_ptr_factory_.GetWeakPtr(),
+                       std::move(sql_feature_processor)));
   }
 }
 
+void FeatureListQueryProcessor::OnSqlQueryProcessed(
+    std::unique_ptr<SqlFeatureProcessor> sql_feature_processor,
+    std::unique_ptr<FeatureProcessorState> feature_processor_state,
+    QueryProcessor::IndexedTensors result) {
+  feature_processor_state->AppendInputTensor(result[kIndexNotUsed]);
+  ProcessNextInputFeature(std::move(feature_processor_state));
+}
+
 }  // namespace segmentation_platform
diff --git a/components/segmentation_platform/internal/execution/feature_list_query_processor.h b/components/segmentation_platform/internal/execution/feature_list_query_processor.h
index d1cfc2d..f63542e 100644
--- a/components/segmentation_platform/internal/execution/feature_list_query_processor.h
+++ b/components/segmentation_platform/internal/execution/feature_list_query_processor.h
@@ -6,12 +6,14 @@
 #define COMPONENTS_SEGMENTATION_PLATFORM_INTERNAL_EXECUTION_FEATURE_LIST_QUERY_PROCESSOR_H_
 
 #include <deque>
+#include <memory>
 #include <vector>
 
 #include "base/memory/weak_ptr.h"
 #include "components/optimization_guide/proto/models.pb.h"
 #include "components/segmentation_platform/internal/execution/custom_input_processor.h"
 #include "components/segmentation_platform/internal/execution/model_execution_manager_impl.h"
+#include "components/segmentation_platform/internal/execution/query_processor.h"
 #include "components/segmentation_platform/internal/execution/uma_feature_processor.h"
 #include "components/segmentation_platform/internal/proto/model_metadata.pb.h"
 
@@ -19,6 +21,7 @@
 class FeatureAggregator;
 class FeatureProcessorState;
 class SignalDatabase;
+class SqlFeatureProcessor;
 
 // FeatureListQueryProcessor takes a segmentation model's metadata, processes
 // each feature in the metadata's feature list in order and computes an input
@@ -57,6 +60,15 @@
   void ProcessNextInputFeature(
       std::unique_ptr<FeatureProcessorState> feature_processor_state);
 
+  // Callback called after a sql feature has been processed, indicating that we
+  // can safely discard the sql feature processor that handled the processing.
+  // Continue with the rest of the input features by calling
+  // ProcessNextInputFeature.
+  void OnSqlQueryProcessed(
+      std::unique_ptr<SqlFeatureProcessor> sql_feature_processor,
+      std::unique_ptr<FeatureProcessorState> feature_processor_state,
+      QueryProcessor::IndexedTensors result);
+
   // Feature processor for uma type of input features.
   UmaFeatureProcessor uma_feature_processor_;
 
diff --git a/components/segmentation_platform/internal/execution/query_processor.h b/components/segmentation_platform/internal/execution/query_processor.h
index 316f942..b6b41c8 100644
--- a/components/segmentation_platform/internal/execution/query_processor.h
+++ b/components/segmentation_platform/internal/execution/query_processor.h
@@ -22,11 +22,14 @@
   using FeatureIndex = int;
   using Tensor = std::vector<float>;
   using IndexedTensors = std::map<FeatureIndex, Tensor>;
+  using QueryProcessorCallback =
+      base::OnceCallback<void(std::unique_ptr<FeatureProcessorState>,
+                              IndexedTensors)>;
 
   // Processes the data and return the tensor values in |callback|.
   virtual void Process(
       std::unique_ptr<FeatureProcessorState> feature_processor_state,
-      base::OnceCallback<IndexedTensors> callback) = 0;
+      QueryProcessorCallback callback) = 0;
 
   // Disallow copy/assign.
   QueryProcessor(const QueryProcessor&) = delete;
diff --git a/components/segmentation_platform/internal/execution/sql_feature_processor.cc b/components/segmentation_platform/internal/execution/sql_feature_processor.cc
new file mode 100644
index 0000000..1f9881f
--- /dev/null
+++ b/components/segmentation_platform/internal/execution/sql_feature_processor.cc
@@ -0,0 +1,28 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/segmentation_platform/internal/execution/sql_feature_processor.h"
+
+#include "components/segmentation_platform/internal/execution/feature_processor_state.h"
+
+namespace segmentation_platform {
+
+SqlFeatureProcessor::SqlFeatureProcessor(
+    std::map<FeatureIndex, proto::SqlFeature> queries)
+    : queries_(std::move(queries)) {}
+SqlFeatureProcessor::~SqlFeatureProcessor() = default;
+
+void SqlFeatureProcessor::Process(
+    std::unique_ptr<FeatureProcessorState> feature_processor_state,
+    QueryProcessorCallback callback) {
+  // TODO(haileywang): Implement usage of custom input processor for bind
+  // values.
+  queries_.clear();
+  base::SequencedTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE,
+      base::BindOnce(std::move(callback), std::move(feature_processor_state),
+                     std::move(result_)));
+}
+
+}  // namespace segmentation_platform
diff --git a/components/segmentation_platform/internal/execution/sql_feature_processor.h b/components/segmentation_platform/internal/execution/sql_feature_processor.h
new file mode 100644
index 0000000..e0b8a8c
--- /dev/null
+++ b/components/segmentation_platform/internal/execution/sql_feature_processor.h
@@ -0,0 +1,42 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_SEGMENTATION_PLATFORM_INTERNAL_EXECUTION_SQL_FEATURE_PROCESSOR_H_
+#define COMPONENTS_SEGMENTATION_PLATFORM_INTERNAL_EXECUTION_SQL_FEATURE_PROCESSOR_H_
+
+#include <map>
+#include <memory>
+#include <vector>
+
+#include "base/callback_forward.h"
+#include "components/segmentation_platform/internal/execution/query_processor.h"
+#include "components/segmentation_platform/internal/proto/model_metadata.pb.h"
+
+namespace segmentation_platform {
+class FeatureProcessorState;
+
+// SqlFeatureProcessor takes a list of SqlFeature type of input, fetches samples
+// from the UKMDatabase, and computes an input tensor to use when executing the
+// ML model.
+class SqlFeatureProcessor : public QueryProcessor {
+ public:
+  explicit SqlFeatureProcessor(
+      std::map<FeatureIndex, proto::SqlFeature> queries);
+  ~SqlFeatureProcessor() override;
+
+  // QueryProcessor implementation.
+  void Process(std::unique_ptr<FeatureProcessorState> feature_processor_state,
+               QueryProcessorCallback callback) override;
+
+ private:
+  // List of sql features to process into input tensors.
+  std::map<FeatureIndex, proto::SqlFeature> queries_;
+
+  // List of resulting input tensors.
+  IndexedTensors result_;
+};
+
+}  // namespace segmentation_platform
+
+#endif  // COMPONENTS_SEGMENTATION_PLATFORM_INTERNAL_EXECUTION_SQL_FEATURE_PROCESSOR_H_
diff --git a/components/segmentation_platform/internal/segmentation_platform_service_impl.cc b/components/segmentation_platform/internal/segmentation_platform_service_impl.cc
index b177f83..75d8b318 100644
--- a/components/segmentation_platform/internal/segmentation_platform_service_impl.cc
+++ b/components/segmentation_platform/internal/segmentation_platform_service_impl.cc
@@ -229,8 +229,10 @@
   feature_list_query_processor_ = std::make_unique<FeatureListQueryProcessor>(
       signal_database_.get(), std::make_unique<FeatureAggregatorImpl>());
 
-  training_data_collector_ = std::make_unique<TrainingDataCollector>(
-      feature_list_query_processor_.get(), histogram_signal_handler_.get());
+  training_data_collector_ = TrainingDataCollector::Create(
+      segment_info_database_.get(), feature_list_query_processor_.get(),
+      histogram_signal_handler_.get(), clock_);
+  training_data_collector_->OnServiceInitialized();
 
   model_execution_manager_ = CreateModelExecutionManager(
       model_provider_, task_runner_, all_segment_ids_, clock_,
diff --git a/components/segmentation_platform/internal/signals/histogram_signal_handler.cc b/components/segmentation_platform/internal/signals/histogram_signal_handler.cc
index 6ce7a721..0b3ae3b 100644
--- a/components/segmentation_platform/internal/signals/histogram_signal_handler.cc
+++ b/components/segmentation_platform/internal/signals/histogram_signal_handler.cc
@@ -15,7 +15,9 @@
 HistogramSignalHandler::HistogramSignalHandler(SignalDatabase* signal_database)
     : db_(signal_database), metrics_enabled_(false) {}
 
-HistogramSignalHandler::~HistogramSignalHandler() = default;
+HistogramSignalHandler::~HistogramSignalHandler() {
+  DCHECK(observers_.empty());
+}
 
 void HistogramSignalHandler::SetRelevantHistograms(
     const RelevantHistograms& histograms) {
diff --git a/components/services/app_service/public/cpp/app_update.cc b/components/services/app_service/public/cpp/app_update.cc
index de20326..99c3d3f 100644
--- a/components/services/app_service/public/cpp/app_update.cc
+++ b/components/services/app_service/public/cpp/app_update.cc
@@ -836,19 +836,12 @@
           (mojom_delta_->allow_uninstall != mojom_state_->allow_uninstall));
 }
 
-apps::mojom::OptionalBool AppUpdate::HasBadge() const {
-  if (mojom_delta_ &&
-      (mojom_delta_->has_badge != apps::mojom::OptionalBool::kUnknown)) {
-    return mojom_delta_->has_badge;
+absl::optional<bool> AppUpdate::HasBadge() const {
+  if (ShouldUseNonMojom()) {
+    GET_VALUE_WITH_FALLBACK(has_badge, absl::nullopt)
   }
-  if (mojom_state_) {
-    return mojom_state_->has_badge;
-  }
-  return apps::mojom::OptionalBool::kUnknown;
-}
 
-absl::optional<bool> AppUpdate::GetHasBadge() const {
-  GET_VALUE_WITH_FALLBACK(has_badge, absl::nullopt);
+  CONVERT_MOJOM_OPTIONALBOOL_TO_OPTIONAL_VALUE(has_badge)
 }
 
 bool AppUpdate::HasBadgeChanged() const {
@@ -1046,7 +1039,7 @@
       << std::endl;
   out << "AllowUninstall: " << PRINT_OPTIONAL_VALUE(AllowUninstall)
       << std::endl;
-  out << "HasBadge: " << app.HasBadge() << std::endl;
+  out << "HasBadge: " << PRINT_OPTIONAL_VALUE(HasBadge) << std::endl;
   out << "Paused: " << PRINT_OPTIONAL_VALUE(Paused) << std::endl;
   out << "IntentFilters: " << std::endl;
   for (const auto& filter : app.IntentFilters()) {
diff --git a/components/services/app_service/public/cpp/app_update.h b/components/services/app_service/public/cpp/app_update.h
index 37902fb..9e560df 100644
--- a/components/services/app_service/public/cpp/app_update.h
+++ b/components/services/app_service/public/cpp/app_update.h
@@ -164,8 +164,7 @@
   absl::optional<bool> AllowUninstall() const;
   bool AllowUninstallChanged() const;
 
-  apps::mojom::OptionalBool HasBadge() const;
-  absl::optional<bool> GetHasBadge() const;
+  absl::optional<bool> HasBadge() const;
   bool HasBadgeChanged() const;
 
   absl::optional<bool> Paused() const;
diff --git a/components/services/app_service/public/cpp/app_update_mojom_unittest.cc b/components/services/app_service/public/cpp/app_update_mojom_unittest.cc
index 6081888..9f76814 100644
--- a/components/services/app_service/public/cpp/app_update_mojom_unittest.cc
+++ b/components/services/app_service/public/cpp/app_update_mojom_unittest.cc
@@ -96,7 +96,7 @@
   absl::optional<bool> expect_allow_uninstall_;
   bool expect_allow_uninstall_changed_;
 
-  apps::mojom::OptionalBool expect_has_badge_;
+  absl::optional<bool> expect_has_badge_;
   bool expect_has_badge_changed_;
 
   absl::optional<bool> expect_paused_;
@@ -282,7 +282,7 @@
     expect_show_in_management_ = absl::nullopt;
     expect_handles_intents_ = absl::nullopt;
     expect_allow_uninstall_ = absl::nullopt;
-    expect_has_badge_ = apps::mojom::OptionalBool::kUnknown;
+    expect_has_badge_ = absl::nullopt;
     expect_paused_ = absl::nullopt;
     expect_intent_filters_.clear();
     expect_resize_locked_ = apps::mojom::OptionalBool::kUnknown;
@@ -779,14 +779,14 @@
 
     if (state) {
       state->has_badge = apps::mojom::OptionalBool::kFalse;
-      expect_has_badge_ = apps::mojom::OptionalBool::kFalse;
+      expect_has_badge_ = false;
       expect_has_badge_changed_ = false;
       CheckExpects(u);
     }
 
     if (delta) {
       delta->has_badge = apps::mojom::OptionalBool::kTrue;
-      expect_has_badge_ = apps::mojom::OptionalBool::kTrue;
+      expect_has_badge_ = true;
       expect_has_badge_changed_ = true;
       CheckExpects(u);
     }
diff --git a/components/services/app_service/public/cpp/app_update_unittest.cc b/components/services/app_service/public/cpp/app_update_unittest.cc
index 5cd2c8e..487e3090 100644
--- a/components/services/app_service/public/cpp/app_update_unittest.cc
+++ b/components/services/app_service/public/cpp/app_update_unittest.cc
@@ -239,7 +239,7 @@
     EXPECT_EQ(expect_allow_uninstall_, u.AllowUninstall());
     EXPECT_EQ(expect_allow_uninstall_changed_, u.AllowUninstallChanged());
 
-    EXPECT_EQ(expect_has_badge_, u.GetHasBadge());
+    EXPECT_EQ(expect_has_badge_, u.HasBadge());
     EXPECT_EQ(expect_has_badge_changed_, u.HasBadgeChanged());
 
     EXPECT_EQ(expect_paused_, u.Paused());
diff --git a/components/soda/soda_installer.cc b/components/soda/soda_installer.cc
index 113582e3..179ad05 100644
--- a/components/soda/soda_installer.cc
+++ b/components/soda/soda_installer.cc
@@ -143,12 +143,8 @@
   return (soda_binary_installed_ && IsLanguageInstalled(language_code));
 }
 
-bool SodaInstaller::IsAnyLanguagePackInstalled() const {
-  return !installed_languages_.empty();
-}
-
 bool SodaInstaller::IsLanguageInstalled(LanguageCode language_code) const {
-  return installed_languages_.find(language_code) != installed_languages_.end();
+  return base::Contains(installed_languages_, language_code);
 }
 
 void SodaInstaller::AddObserver(Observer* observer) {
@@ -159,20 +155,43 @@
   observers_.RemoveObserver(observer);
 }
 
-void SodaInstaller::NotifySodaInstalledForTesting() {
-  soda_binary_installed_ = true;
-  is_soda_downloading_ = false;
-  installed_languages_.insert(LanguageCode::kEnUs);
-  language_pack_progress_.clear();
-  NotifyOnSodaInstalled();
+void SodaInstaller::NotifySodaInstalledForTesting(LanguageCode language_code) {
+  // TODO: Call the actual functions in SodaInstallerImpl and
+  // SodaInstallerImpleChromeOS that do this logic
+  // (e.g. SodaInstallerImpl::OnSodaBinaryInstalled) rather than faking it.
+
+  // If language code is none, this signifies that the SODA binary installed.
+  if (language_code == LanguageCode::kNone) {
+    soda_binary_installed_ = true;
+    is_soda_downloading_ = false;
+    for (LanguageCode installed_language : installed_languages_) {
+      NotifyOnSodaInstalled(installed_language);
+    }
+    return;
+  }
+
+  // Otherwise, this means a language pack installed.
+  installed_languages_.insert(language_code);
+  if (base::Contains(language_pack_progress_, language_code))
+    language_pack_progress_.erase(language_code);
+  if (soda_binary_installed_)
+    NotifyOnSodaInstalled(language_code);
 }
 
-void SodaInstaller::NotifySodaErrorForTesting() {
-  soda_binary_installed_ = false;
-  is_soda_downloading_ = false;
-  installed_languages_.clear();
-  language_pack_progress_.clear();
-  NotifyOnSodaError();
+void SodaInstaller::NotifySodaErrorForTesting(LanguageCode language_code) {
+  // TODO: Call the actual functions in SodaInstallerImpl and
+  // SodaInstallerImpleChromeOS that do this logic rather than faking it.
+  if (language_code == LanguageCode::kNone) {
+    // Error with the SODA binary download.
+    soda_binary_installed_ = false;
+    is_soda_downloading_ = false;
+    language_pack_progress_.clear();
+  } else {
+    // Error with the language pack download.
+    if (base::Contains(language_pack_progress_, language_code))
+      language_pack_progress_.erase(language_code);
+  }
+  NotifyOnSodaError(language_code);
 }
 
 void SodaInstaller::UninstallSodaForTesting() {
@@ -183,39 +202,26 @@
   language_pack_progress_.clear();
 }
 
-void SodaInstaller::NotifySodaDownloadProgressForTesting(int progress) {
-  soda_binary_installed_ = false;
-  is_soda_downloading_ = true;
-  installed_languages_.clear();
-  NotifyOnSodaProgress(progress);
+void SodaInstaller::NotifySodaProgressForTesting(int progress,
+                                                 LanguageCode language_code) {
+  // TODO: Call the actual functions in SodaInstallerImpl and
+  // SodaInstallerImpleChromeOS that do this logic rather than faking it.
+  if (language_code == LanguageCode::kNone) {
+    // SODA binary download progress.
+    soda_binary_installed_ = false;
+    is_soda_downloading_ = true;
+  } else {
+    // Language pack download progress.
+    if (base::Contains(language_pack_progress_, language_code))
+      language_pack_progress_.insert({language_code, progress});
+    else
+      language_pack_progress_[language_code] = progress;
+  }
+  NotifyOnSodaProgress(language_code, progress);
 }
 
-void SodaInstaller::NotifyOnSodaLanguagePackInstalledForTesting(
-    LanguageCode language_code) {
-  installed_languages_.insert(language_code);
-  auto it = language_pack_progress_.find(language_code);
-  if (it != language_pack_progress_.end())
-    language_pack_progress_.erase(language_code);
-  NotifyOnSodaLanguagePackInstalled(language_code);
-}
-
-void SodaInstaller::NotifyOnSodaLanguagePackProgressForTesting(
-    int progress,
-    LanguageCode language_code) {
-  auto it = language_pack_progress_.find(language_code);
-  if (it == language_pack_progress_.end())
-    language_pack_progress_.insert({language_code, progress});
-  else
-    language_pack_progress_[language_code] = progress;
-  NotifyOnSodaLanguagePackProgress(progress, language_code);
-}
-
-void SodaInstaller::NotifyOnSodaLanguagePackErrorForTesting(
-    LanguageCode language_code) {
-  auto it = language_pack_progress_.find(language_code);
-  if (it != language_pack_progress_.end())
-    language_pack_progress_.erase(language_code);
-  NotifyOnSodaLanguagePackError(language_code);
+bool SodaInstaller::IsAnyLanguagePackInstalledForTesting() const {
+  return !installed_languages_.empty();
 }
 
 void SodaInstaller::RegisterRegisteredLanguagePackPref(
@@ -227,37 +233,20 @@
                              base::Value(std::move(default_languages)));
 }
 
-void SodaInstaller::NotifyOnSodaInstalled() {
+void SodaInstaller::NotifyOnSodaInstalled(LanguageCode language_code) {
   for (Observer& observer : observers_)
-    observer.OnSodaInstalled();
+    observer.OnSodaInstalled(language_code);
 }
 
-void SodaInstaller::NotifyOnSodaLanguagePackInstalled(
-    LanguageCode language_code) {
+void SodaInstaller::NotifyOnSodaError(LanguageCode language_code) {
   for (Observer& observer : observers_)
-    observer.OnSodaLanguagePackInstalled(language_code);
+    observer.OnSodaError(language_code);
 }
 
-void SodaInstaller::NotifyOnSodaError() {
+void SodaInstaller::NotifyOnSodaProgress(LanguageCode language_code,
+                                         int progress) {
   for (Observer& observer : observers_)
-    observer.OnSodaError();
-}
-
-void SodaInstaller::NotifyOnSodaLanguagePackError(LanguageCode language_code) {
-  for (Observer& observer : observers_)
-    observer.OnSodaLanguagePackError(language_code);
-}
-
-void SodaInstaller::NotifyOnSodaProgress(int combined_progress) {
-  for (Observer& observer : observers_)
-    observer.OnSodaProgress(combined_progress);
-}
-
-void SodaInstaller::NotifyOnSodaLanguagePackProgress(
-    int language_progress,
-    LanguageCode language_code) {
-  for (Observer& observer : observers_)
-    observer.OnSodaLanguagePackProgress(language_progress, language_code);
+    observer.OnSodaProgress(language_code, progress);
 }
 
 void SodaInstaller::RegisterLanguage(const std::string& language,
@@ -274,8 +263,8 @@
 }
 
 bool SodaInstaller::IsSodaDownloading(LanguageCode language_code) const {
-  return is_soda_downloading_ || language_pack_progress_.find(language_code) !=
-                                     language_pack_progress_.end();
+  return is_soda_downloading_ ||
+         base::Contains(language_pack_progress_, language_code);
 }
 
 bool SodaInstaller::IsAnyFeatureUsingSodaEnabled(PrefService* prefs) {
diff --git a/components/soda/soda_installer.h b/components/soda/soda_installer.h
index 89b118cf..d406e25 100644
--- a/components/soda/soda_installer.h
+++ b/components/soda/soda_installer.h
@@ -28,42 +28,19 @@
   // Observer of the SODA (Speech On-Device API) installation.
   class Observer : public base::CheckedObserver {
    public:
-    ////////////////////////////////////////////////////////////////////////////
-    // Main SODA update functions. Use these when informing the user about
-    // the availability of speech on device. This means that the general binary
-    // is ready and at least one language is available. For example, these might
-    // be used to display download progress next to the feature name in
-    // settings.
+    // Called when the SODA binary component and the language pack for this
+    // language code are installed.
+    virtual void OnSodaInstalled(LanguageCode language_code) = 0;
 
-    // Called when the SODA binary component and at least one language pack is
-    // installed.
-    virtual void OnSodaInstalled() = 0;
-
-    // Called if there is an error in the SODA binary or language pack
-    // installation.
-    virtual void OnSodaError() = 0;
+    // Called if there is an error in the SODA installation. If the language
+    // code is LanguageCode::kNone, the error is for the SODA binary; otherwise
+    // it is for the language pack.
+    virtual void OnSodaError(LanguageCode language_code) = 0;
 
     // Called during the SODA installation. Progress is the weighted average of
-    // the download percentage of the SODA binary and at least one language
-    // pack.
-    virtual void OnSodaProgress(int combined_progress) = 0;
-
-    ////////////////////////////////////////////////////////////////////////////
-    // Language-specific SODA update functions. Use these when informing the
-    // user about the availability of a specific language. For example, these
-    // might be used to display download progress of a particular language next
-    // to the language list item.
-
-    // Called when a SODA language pack component is installed.
-    virtual void OnSodaLanguagePackInstalled(LanguageCode language_code) {}
-
-    // Called if there is an error in a SODA language pack installation.
-    virtual void OnSodaLanguagePackError(LanguageCode language_code) {}
-
-    // Called during the SODA installation. Progress is the download percentage
-    // out of 100.
-    virtual void OnSodaLanguagePackProgress(int language_progress,
-                                            LanguageCode language_code) {}
+    // the combined download percentage of the SODA binary and the language pack
+    // for this language code.
+    virtual void OnSodaProgress(LanguageCode language_code, int progress) = 0;
   };
 
   SodaInstaller();
@@ -131,14 +108,17 @@
   void NeverDownloadSodaForTesting() {
     never_download_soda_for_testing_ = true;
   }
-  void NotifySodaInstalledForTesting();
-  void NotifySodaErrorForTesting();
+
+  // The soda binary is encoded as LanguageCode::kNone.
+  void NotifySodaInstalledForTesting(
+      LanguageCode language_code = LanguageCode::kNone);
+  void NotifySodaErrorForTesting(
+      LanguageCode language_code = LanguageCode::kNone);
   void UninstallSodaForTesting();
-  void NotifySodaDownloadProgressForTesting(int percentage);
-  void NotifyOnSodaLanguagePackInstalledForTesting(LanguageCode language_code);
-  void NotifyOnSodaLanguagePackProgressForTesting(int progress,
-                                                  LanguageCode language_code);
-  void NotifyOnSodaLanguagePackErrorForTesting(LanguageCode language_code);
+  void NotifySodaProgressForTesting(
+      int progress,
+      LanguageCode language_code = LanguageCode::kNone);
+  bool IsAnyLanguagePackInstalledForTesting() const;
 
  protected:
   // Registers the preference tracking the installed SODA language packs.
@@ -152,31 +132,19 @@
   // space may not be freed immediately.
   virtual void UninstallSoda(PrefService* global_prefs) = 0;
 
-  // Notifies the observers that the installation of the SODA binary and at
-  // least one language pack has completed.
-  void NotifyOnSodaInstalled();
+  // Notifies the observers that the installation of the SODA binary and the
+  // language pack for this language code has completed.
+  void NotifyOnSodaInstalled(LanguageCode language_code);
 
-  // Notifies the observers that a SODA language pack installation has
-  // completed.
-  void NotifyOnSodaLanguagePackInstalled(LanguageCode language_code);
-
-  // Notifies the observers that there is an error in the SODA binary
-  // installation.
-  void NotifyOnSodaError();
-
-  // Notifies the observers that there is an error in a SODA language pack
-  // installation.
-  void NotifyOnSodaLanguagePackError(LanguageCode language_code);
+  // Notifies the observers that there is an error in the SODA installation.
+  // If the language code is LanguageCode::kNone, the error is for the SODA
+  // binary; otherwise it is for the language pack.
+  void NotifyOnSodaError(LanguageCode language_code);
 
   // Notifies the observers of the combined progress as the SODA binary and
   // language pack are installed. Progress is the download percentage out of
   // 100.
-  void NotifyOnSodaProgress(int combined_progress);
-
-  // Notifies the observers of the progress percentage the SODA language pack is
-  // installed. Progress is the download percentage out of 100.
-  void NotifyOnSodaLanguagePackProgress(int language_progress,
-                                        LanguageCode language_code);
+  void NotifyOnSodaProgress(LanguageCode language_code, int progress);
 
   // Registers a language pack by adding it to the preference tracking the
   // installed SODA language packs.
@@ -190,8 +158,6 @@
   // installed. The language should be localized in BCP-47, e.g. "en-US".
   bool IsLanguageInstalled(LanguageCode language_code) const;
 
-  bool IsAnyLanguagePackInstalled() const;
-
   base::ObserverList<Observer> observers_;
   bool soda_binary_installed_ = false;
   bool soda_installer_initialized_ = false;
diff --git a/components/soda/soda_installer_impl_chromeos.cc b/components/soda/soda_installer_impl_chromeos.cc
index 7582987c..8927ca9 100644
--- a/components/soda/soda_installer_impl_chromeos.cc
+++ b/components/soda/soda_installer_impl_chromeos.cc
@@ -5,6 +5,7 @@
 #include "components/soda/soda_installer_impl_chromeos.h"
 
 #include "base/bind.h"
+#include "base/containers/contains.h"
 #include "base/feature_list.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/numerics/safe_conversions.h"
@@ -126,16 +127,17 @@
   if (install_result.error == dlcservice::kErrorNone) {
     soda_binary_installed_ = true;
     SetSodaBinaryPath(base::FilePath(install_result.root_path));
-    if (IsLanguageInstalled(LanguageCode::kEnUs)) {
-      NotifyOnSodaInstalled();
-    }
+    // TODO(crbug.com/1161569): SODA is only available for English right now.
+    // Update this to notify on all installed languages.
+    if (IsLanguageInstalled(LanguageCode::kEnUs))
+      NotifyOnSodaInstalled(LanguageCode::kEnUs);
 
     base::UmaHistogramTimes(kSodaBinaryInstallationSuccessTimeTaken,
                             base::Time::Now() - start_time);
   } else {
     soda_binary_installed_ = false;
     soda_progress_ = 0.0;
-    NotifyOnSodaError();
+    NotifyOnSodaError(LanguageCode::kNone);
     base::UmaHistogramTimes(kSodaBinaryInstallationFailureTimeTaken,
                             base::Time::Now() - start_time);
   }
@@ -153,7 +155,7 @@
     installed_languages_.insert(language_code);
     SetLanguagePath(base::FilePath(install_result.root_path));
     if (soda_binary_installed_) {
-      NotifyOnSodaInstalled();
+      NotifyOnSodaInstalled(language_code);
     }
     base::UmaHistogramTimes(
         GetInstallationSuccessTimeMetricForLanguagePack(language_code),
@@ -162,7 +164,7 @@
   } else {
     // TODO: Notify the observer of the specific language pack that failed
     // to install. ChromeOS currently only supports the en-US language pack.
-    NotifyOnSodaLanguagePackError(language_code);
+    NotifyOnSodaError(language_code);
 
     base::UmaHistogramTimes(
         GetInstallationFailureTimeMetricForLanguagePack(language_code),
@@ -181,23 +183,21 @@
 
 void SodaInstallerImplChromeOS::OnLanguageProgress(double progress) {
   language_pack_progress_[LanguageCode::kEnUs] = progress;
-
-  // TODO: Notify the observer of the specific language pack that is currently
-  // being installed. ChromeOS currently only supports the en-US language pack.
-  NotifyOnSodaLanguagePackProgress(progress, LanguageCode::kEnUs);
+  OnSodaCombinedProgress();
 }
 
 void SodaInstallerImplChromeOS::OnSodaCombinedProgress() {
   // TODO(crbug.com/1055150): Consider updating this implementation.
   // e.g.: (1) starting progress from 0% if we are downloading language
   // only (2) weighting download progress proportionally to DLC binary size.
-  double language_progress = 0;
-  auto it = language_pack_progress_.find(LanguageCode::kEnUs);
-  if (it != language_pack_progress_.end())
-    language_progress = it->second;
+  double language_progress = 0.0;
+  if (base::Contains(language_pack_progress_, LanguageCode::kEnUs))
+    language_progress = language_pack_progress_[LanguageCode::kEnUs];
 
   const double progress = (soda_progress_ + language_progress) / 2;
-  NotifyOnSodaProgress(base::ClampFloor(100 * progress));
+  // TODO: Notify the observer of the specific language pack that is currently
+  // being installed. ChromeOS currently only supports the en-US language pack.
+  NotifyOnSodaProgress(LanguageCode::kEnUs, base::ClampFloor(100 * progress));
 }
 
 void SodaInstallerImplChromeOS::OnDlcUninstalled(const std::string& dlc_id,
diff --git a/components/soda/soda_installer_impl_chromeos_unittest.cc b/components/soda/soda_installer_impl_chromeos_unittest.cc
index 97cfd78..d63efb72 100644
--- a/components/soda/soda_installer_impl_chromeos_unittest.cc
+++ b/components/soda/soda_installer_impl_chromeos_unittest.cc
@@ -74,7 +74,7 @@
   }
 
   bool IsAnyLanguagePackInstalled() {
-    return soda_installer_impl_->IsAnyLanguagePackInstalled();
+    return soda_installer_impl_->IsAnyLanguagePackInstalledForTesting();
   }
 
   bool IsSodaDownloading() {
@@ -197,7 +197,13 @@
   ASSERT_FALSE(IsSodaDownloading());
   ASSERT_FALSE(IsLanguageInstalled(kEnglishLocale));
   ASSERT_FALSE(IsSodaDownloading());
+
+  // Install just the binary.
   GetInstance()->NotifySodaInstalledForTesting();
+  ASSERT_FALSE(IsSodaDownloading());
+
+  // Now install the language pack.
+  GetInstance()->NotifySodaInstalledForTesting(kEnglishLocale);
   ASSERT_TRUE(IsSodaInstalled());
   ASSERT_FALSE(IsSodaDownloading());
   ASSERT_TRUE(IsLanguageInstalled(kEnglishLocale));
@@ -219,7 +225,7 @@
   ASSERT_FALSE(IsSodaDownloading());
   ASSERT_FALSE(IsLanguageInstalled(kEnglishLocale));
   Init();
-  GetInstance()->NotifySodaDownloadProgressForTesting(50);
+  GetInstance()->NotifySodaProgressForTesting(50);
   ASSERT_FALSE(IsSodaInstalled());
   ASSERT_FALSE(IsAnyLanguagePackInstalled());
   ASSERT_TRUE(IsSodaDownloading());
@@ -232,10 +238,10 @@
   Init();
   RunUntilIdle();
   ASSERT_FALSE(IsLanguageInstalled(fr_fr));
-  GetInstance()->NotifyOnSodaLanguagePackProgressForTesting(50, fr_fr);
+  GetInstance()->NotifySodaProgressForTesting(50, fr_fr);
   ASSERT_TRUE(GetInstance()->IsSodaDownloading(fr_fr));
   ASSERT_FALSE(IsLanguageInstalled(fr_fr));
-  GetInstance()->NotifyOnSodaLanguagePackInstalledForTesting(fr_fr);
+  GetInstance()->NotifySodaInstalledForTesting(fr_fr);
   ASSERT_TRUE(IsLanguageInstalled(fr_fr));
 }
 
@@ -245,10 +251,10 @@
   Init();
   RunUntilIdle();
   ASSERT_FALSE(IsLanguageInstalled(fr_fr));
-  GetInstance()->NotifyOnSodaLanguagePackProgressForTesting(50, fr_fr);
+  GetInstance()->NotifySodaProgressForTesting(50, fr_fr);
   ASSERT_TRUE(GetInstance()->IsSodaDownloading(fr_fr));
   ASSERT_FALSE(IsLanguageInstalled(fr_fr));
-  GetInstance()->NotifyOnSodaLanguagePackErrorForTesting(fr_fr);
+  GetInstance()->NotifySodaErrorForTesting(fr_fr);
   ASSERT_FALSE(IsLanguageInstalled(fr_fr));
   ASSERT_FALSE(GetInstance()->IsSodaDownloading(fr_fr));
 }
diff --git a/components/vector_icons/BUILD.gn b/components/vector_icons/BUILD.gn
index a7dcf9c..c506406 100644
--- a/components/vector_icons/BUILD.gn
+++ b/components/vector_icons/BUILD.gn
@@ -22,6 +22,7 @@
     "call_end.icon",
     "caret_down.icon",
     "caret_up.icon",
+    "celebration.icon",
     "certificate.icon",
     "check_circle.icon",
     "close.icon",
diff --git a/components/vector_icons/celebration.icon b/components/vector_icons/celebration.icon
new file mode 100644
index 0000000..0f9f77f
--- /dev/null
+++ b/components/vector_icons/celebration.icon
@@ -0,0 +1,60 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+CANVAS_DIMENSIONS, 24,
+NEW_PATH,
+MOVE_TO, 2, 22,
+R_LINE_TO, 14, -5,
+LINE_TO, 7, 8,
+LINE_TO, 2, 22,
+CLOSE,
+MOVE_TO, 12.35f, 16.18f,
+LINE_TO, 5.3f, 18.7f,
+R_LINE_TO, 2.52f, -7.05f,
+LINE_TO, 12.35f, 16.18f,
+CLOSE,
+NEW_PATH,
+MOVE_TO, 14.53f, 12.53f,
+R_LINE_TO, 5.59f, -5.59f,
+R_CUBIC_TO, 0.49f, -0.49f, 1.28f, -0.49f, 1.77f, 0,
+R_LINE_TO, 0.59f, 0.59f,
+R_LINE_TO, 1.06f, -1.06f,
+R_LINE_TO, -0.59f, -0.59f,
+R_CUBIC_TO, -1.07f, -1.07f, -2.82f, -1.07f, -3.89f, 0,
+R_LINE_TO, -5.59f, 5.59f,
+LINE_TO, 14.53f, 12.53f,
+CLOSE,
+NEW_PATH,
+MOVE_TO, 10.06f, 6.88f,
+LINE_TO, 9.47f, 7.47f,
+R_LINE_TO, 1.06f, 1.06f,
+R_LINE_TO, 0.59f, -0.59f,
+R_CUBIC_TO, 1.07f, -1.07f, 1.07f, -2.82f, 0, -3.89f,
+R_LINE_TO, -0.59f, -0.59f,
+LINE_TO, 9.47f, 4.53f,
+R_LINE_TO, 0.59f, 0.59f,
+CUBIC_TO, 10.54f, 5.6f, 10.54f, 6.4f, 10.06f, 6.88f,
+CLOSE,
+NEW_PATH,
+MOVE_TO, 17.06f, 11.88f,
+R_LINE_TO, -1.59f, 1.59f,
+R_LINE_TO, 1.06f, 1.06f,
+R_LINE_TO, 1.59f, -1.59f,
+R_CUBIC_TO, 0.49f, -0.49f, 1.28f, -0.49f, 1.77f, 0,
+R_LINE_TO, 1.61f, 1.61f,
+R_LINE_TO, 1.06f, -1.06f,
+R_LINE_TO, -1.61f, -1.61f,
+CUBIC_TO, 19.87f, 10.81f, 18.13f, 10.81f, 17.06f, 11.88f,
+CLOSE,
+NEW_PATH,
+MOVE_TO, 15.06f, 5.88f,
+R_LINE_TO, -3.59f, 3.59f,
+R_LINE_TO, 1.06f, 1.06f,
+R_LINE_TO, 3.59f, -3.59f,
+R_CUBIC_TO, 1.07f, -1.07f, 1.07f, -2.82f, 0, -3.89f,
+R_LINE_TO, -1.59f, -1.59f,
+R_LINE_TO, -1.06f, 1.06f,
+R_LINE_TO, 1.59f, 1.59f,
+CUBIC_TO, 15.54f, 4.6f, 15.54f, 5.4f, 15.06f, 5.88f,
+CLOSE
\ No newline at end of file
diff --git a/content/browser/accessibility/browser_accessibility_android.cc b/content/browser/accessibility/browser_accessibility_android.cc
index c8e6cec..a0f358a 100644
--- a/content/browser/accessibility/browser_accessibility_android.cc
+++ b/content/browser/accessibility/browser_accessibility_android.cc
@@ -627,10 +627,10 @@
   return true;
 }
 
-// Note: this is used to compute an object's name on Android, and is exposed as
-// the name field in Android dump tree tests.
-// TODO(accessibility) Should it be called GetName() so that engineers not
-// familiar with Android can find it more easily?
+// Note: In the Android accessibility API, the word "text" is used where other
+// platforms would use "name". The value returned here will appear in dump tree
+// tests as "name" in the ...-android.txt files, but as "text" in the
+// ...-android-external.txt files. On other platforms this may be ::GetName().
 std::u16string BrowserAccessibilityAndroid::GetTextContentUTF16() const {
   if (ui::IsIframe(GetRole()))
     return std::u16string();
@@ -753,6 +753,11 @@
   return value;
 }
 
+// This method maps to the Android API's "hint" attribute. For nodes that have
+// chosen to expose their value in the name ("text") attribute, the hint must
+// contain the text that would otherwise have been present. The hint includes
+// the placeholder and describedby values for all nodes regardless of where the
+// value is placed. These pieces of content are concatenated for Android.
 std::u16string BrowserAccessibilityAndroid::GetHint() const {
   std::vector<std::u16string> strings;
 
@@ -1917,6 +1922,11 @@
   return false;
 }
 
+// This method determines if a node should expose its value as a name, which is
+// placed in the Android API's "text" attribute. For controls that can take on
+// a value (e.g. a date time, or combobox), we wish to expose the value that
+// the user has chosen. When the value is exposed as the name, then the
+// accessible name is added to the Android API's "hint" attribute instead.
 bool BrowserAccessibilityAndroid::ShouldExposeValueAsName() const {
   switch (GetRole()) {
     case ax::mojom::Role::kDate:
@@ -1935,6 +1945,9 @@
   if (IsTextField())
     return true;
 
+  if (IsCombobox())
+    return true;
+
   if (GetRole() == ax::mojom::Role::kPopUpButton &&
       !GetValueForControl().empty()) {
     return true;
diff --git a/content/browser/accessibility/dump_accessibility_tree_browsertest.cc b/content/browser/accessibility/dump_accessibility_tree_browsertest.cc
index 2f9821b..d0b8331 100644
--- a/content/browser/accessibility/dump_accessibility_tree_browsertest.cc
+++ b/content/browser/accessibility/dump_accessibility_tree_browsertest.cc
@@ -2391,6 +2391,11 @@
   RunHtmlTest(FILE_PATH_LITERAL("ins.html"));
 }
 
+IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
+                       AccessibilityInteractiveControlsWithLabels) {
+  RunHtmlTest(FILE_PATH_LITERAL("interactive-controls-with-labels.html"));
+}
+
 IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilityLabel) {
   RunHtmlTest(FILE_PATH_LITERAL("label.html"));
 }
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 c389c6e..47a224b 100644
--- a/content/browser/attribution_reporting/attribution_data_host_manager_impl.cc
+++ b/content/browser/attribution_reporting/attribution_data_host_manager_impl.cc
@@ -145,6 +145,11 @@
   attribution_manager_->HandleSource(std::move(storable_source));
 }
 
+void AttributionDataHostManagerImpl::TriggerDataAvailable(
+    blink::mojom::AttributionTriggerDataPtr data) {
+  // TODO(johnidel): Add browser process handling for attributionsrc triggers.
+}
+
 void AttributionDataHostManagerImpl::OnDataHostDisconnected() {
   receiver_source_destinations_.erase(receivers_.current_receiver());
 }
diff --git a/content/browser/attribution_reporting/attribution_data_host_manager_impl.h b/content/browser/attribution_reporting/attribution_data_host_manager_impl.h
index aace73b5..3156798 100644
--- a/content/browser/attribution_reporting/attribution_data_host_manager_impl.h
+++ b/content/browser/attribution_reporting/attribution_data_host_manager_impl.h
@@ -63,6 +63,8 @@
   // blink::mojom::AttributionDataHost:
   void SourceDataAvailable(
       blink::mojom::AttributionSourceDataPtr data) override;
+  void TriggerDataAvailable(
+      blink::mojom::AttributionTriggerDataPtr data) override;
 
   void OnDataHostDisconnected();
 
diff --git a/content/browser/attribution_reporting/attribution_src_browsertest.cc b/content/browser/attribution_reporting/attribution_src_browsertest.cc
new file mode 100644
index 0000000..fb1e613
--- /dev/null
+++ b/content/browser/attribution_reporting/attribution_src_browsertest.cc
@@ -0,0 +1,655 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <memory>
+#include <utility>
+
+#include "base/run_loop.h"
+#include "base/strings/strcat.h"
+#include "content/browser/attribution_reporting/attribution_manager_impl.h"
+#include "content/browser/attribution_reporting/attribution_test_utils.h"
+#include "content/public/common/content_switches.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/content_browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
+#include "content/shell/browser/shell.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/controllable_http_response.h"
+#include "net/test/embedded_test_server/default_handlers.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/conversions/attribution_data_host.mojom.h"
+#include "url/gurl.h"
+
+namespace content {
+
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::Field;
+using ::testing::IsEmpty;
+using ::testing::Pair;
+
+std::unique_ptr<MockDataHost> GetRegisteredDataHost(
+    mojo::PendingReceiver<blink::mojom::AttributionDataHost> data_host) {
+  return std::make_unique<MockDataHost>(std::move(data_host));
+}
+
+}  // namespace
+
+class AttributionSrcBrowserTest : public ContentBrowserTest {
+ public:
+  AttributionSrcBrowserTest() {
+    AttributionManagerImpl::RunInMemoryForTesting();
+  }
+
+  void SetUpOnMainThread() override {
+    host_resolver()->AddRule("*", "127.0.0.1");
+    embedded_test_server()->ServeFilesFromSourceDirectory(
+        "content/test/data/attribution_reporting");
+    embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
+    content::SetupCrossSiteRedirector(embedded_test_server());
+    ASSERT_TRUE(embedded_test_server()->Start());
+
+    https_server_ = std::make_unique<net::EmbeddedTestServer>(
+        net::EmbeddedTestServer::TYPE_HTTPS);
+    https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
+    net::test_server::RegisterDefaultHandlers(https_server_.get());
+    https_server_->ServeFilesFromSourceDirectory(
+        "content/test/data/attribution_reporting");
+    https_server_->ServeFilesFromSourceDirectory("content/test/data");
+    SetupCrossSiteRedirector(https_server_.get());
+    ASSERT_TRUE(https_server_->Start());
+  }
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    // Sets up the blink runtime feature for ConversionMeasurement.
+    command_line->AppendSwitch(
+        switches::kEnableExperimentalWebPlatformFeatures);
+
+    // Sets up support for event sources.
+    command_line->AppendSwitch(switches::kEnableBlinkTestFeatures);
+  }
+
+  WebContents* web_contents() { return shell()->web_contents(); }
+
+  net::EmbeddedTestServer* https_server() { return https_server_.get(); }
+
+ private:
+  std::unique_ptr<net::EmbeddedTestServer> https_server_;
+};
+
+IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
+                       AttributionSrcImg_SourceRegistered) {
+  GURL page_url =
+      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  MockAttributionHost host(web_contents());
+  std::unique_ptr<MockDataHost> data_host;
+  base::RunLoop loop;
+  EXPECT_CALL(host, RegisterDataHost)
+      .WillOnce(
+          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+            data_host = GetRegisteredDataHost(std::move(host));
+            loop.Quit();
+          });
+
+  GURL register_url =
+      https_server()->GetURL("c.test", "/register_source_headers.html");
+
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
+  if (!data_host)
+    loop.Run();
+  data_host->WaitForSourceData(/*num_source_data=*/1);
+  const auto& source_data = data_host->source_data();
+
+  EXPECT_EQ(source_data.size(), 1u);
+  EXPECT_EQ(source_data.front()->source_event_id, 5UL);
+  EXPECT_EQ(source_data.front()->destination,
+            url::Origin::Create(GURL("https://advertiser.example")));
+  EXPECT_EQ(source_data.front()->priority, 0);
+  EXPECT_EQ(source_data.front()->expiry, absl::nullopt);
+  EXPECT_FALSE(source_data.front()->debug_key);
+  EXPECT_THAT(source_data.front()->filter_data->filter_values, IsEmpty());
+  EXPECT_THAT(source_data.front()->aggregatable_sources->sources, IsEmpty());
+}
+
+IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
+                       AttributionSrcImg_SourceRegisteredWithOptionalParams) {
+  GURL page_url =
+      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  MockAttributionHost host(web_contents());
+  std::unique_ptr<MockDataHost> data_host;
+  base::RunLoop loop;
+  EXPECT_CALL(host, RegisterDataHost)
+      .WillOnce(
+          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+            data_host = GetRegisteredDataHost(std::move(host));
+            loop.Quit();
+          });
+
+  GURL register_url = https_server()->GetURL(
+      "c.test", "/register_source_headers_all_params.html");
+
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
+  if (!data_host)
+    loop.Run();
+  data_host->WaitForSourceData(/*num_source_data=*/1);
+  const auto& source_data = data_host->source_data();
+
+  EXPECT_EQ(source_data.size(), 1u);
+  EXPECT_EQ(source_data.front()->source_event_id, 5UL);
+  EXPECT_EQ(source_data.front()->destination,
+            url::Origin::Create(GURL("https://advertiser.example")));
+  EXPECT_EQ(source_data.front()->priority, 10);
+  EXPECT_EQ(source_data.front()->expiry, base::Seconds(1000));
+  EXPECT_EQ(source_data.front()->debug_key,
+            blink::mojom::AttributionDebugKey::New(789));
+  EXPECT_THAT(source_data.front()->filter_data->filter_values,
+              UnorderedElementsAre(Pair("a", IsEmpty()),
+                                   Pair("b", ElementsAre("1", "2"))));
+}
+
+IN_PROC_BROWSER_TEST_F(
+    AttributionSrcBrowserTest,
+    AttributionSrcImg_SourceRegisteredWithAttributionAggregatableSources) {
+  GURL page_url =
+      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  MockAttributionHost host(web_contents());
+  std::unique_ptr<MockDataHost> data_host;
+  base::RunLoop loop;
+  EXPECT_CALL(host, RegisterDataHost)
+      .WillOnce(
+          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+            data_host = GetRegisteredDataHost(std::move(host));
+            loop.Quit();
+          });
+
+  GURL register_url = https_server()->GetURL(
+      "c.test", "/register_aggregatable_source_headers.html");
+
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
+  if (!data_host)
+    loop.Run();
+  data_host->WaitForSourceData(/*num_source_data=*/1);
+  const auto& source_data = data_host->source_data();
+
+  EXPECT_EQ(source_data.size(), 1u);
+  EXPECT_EQ(source_data.front()->source_event_id, 5UL);
+  EXPECT_EQ(source_data.front()->destination,
+            url::Origin::Create(GURL("https://advertiser.example")));
+  EXPECT_EQ(source_data.front()->priority, 0);
+  EXPECT_EQ(source_data.front()->expiry, absl::nullopt);
+  EXPECT_FALSE(source_data.front()->debug_key);
+  EXPECT_THAT(
+      source_data.front()->aggregatable_sources->sources,
+      UnorderedElementsAre(
+          Pair("key1",
+               Pointee(AllOf(
+                   Field(&blink::mojom::AttributionAggregatableKey::high_bits,
+                         0),
+                   Field(&blink::mojom::AttributionAggregatableKey::low_bits,
+                         5)))),
+          Pair("key2",
+               Pointee(AllOf(
+                   Field(&blink::mojom::AttributionAggregatableKey::high_bits,
+                         0),
+                   Field(&blink::mojom::AttributionAggregatableKey::low_bits,
+                         345))))));
+}
+
+IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
+                       AttributionSrcImgRedirect_MultipleSourcesRegistered) {
+  GURL page_url =
+      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  MockAttributionHost host(web_contents());
+  std::unique_ptr<MockDataHost> data_host;
+  base::RunLoop loop;
+  EXPECT_CALL(host, RegisterDataHost)
+      .WillOnce(
+          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+            data_host = GetRegisteredDataHost(std::move(host));
+            loop.Quit();
+          });
+
+  GURL register_url = https_server()->GetURL(
+      "c.test", "/register_source_headers_and_redirect.html");
+
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
+  if (!data_host)
+    loop.Run();
+  data_host->WaitForSourceData(/*num_source_data=*/2);
+  const auto& source_data = data_host->source_data();
+
+  EXPECT_EQ(source_data.size(), 2u);
+  EXPECT_EQ(source_data.front()->source_event_id, 1UL);
+  EXPECT_EQ(source_data.front()->destination,
+            url::Origin::Create(GURL("https://advertiser.example")));
+  EXPECT_EQ(source_data.back()->source_event_id, 5UL);
+  EXPECT_EQ(source_data.back()->destination,
+            url::Origin::Create(GURL("https://advertiser.example")));
+}
+
+IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
+                       AttributionSrcImgRedirect_InvalidJsonIgnored) {
+  GURL page_url =
+      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  MockAttributionHost host(web_contents());
+  std::unique_ptr<MockDataHost> data_host;
+  base::RunLoop loop;
+  EXPECT_CALL(host, RegisterDataHost)
+      .WillOnce(
+          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+            data_host = GetRegisteredDataHost(std::move(host));
+            loop.Quit();
+          });
+
+  GURL register_url = https_server()->GetURL(
+      "c.test", "/register_source_headers_and_redirect_invalid.html");
+
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
+  if (!data_host)
+    loop.Run();
+  data_host->WaitForSourceData(/*num_source_data=*/1);
+  const auto& source_data = data_host->source_data();
+
+  // Only the second source is registered.
+  EXPECT_EQ(source_data.size(), 1u);
+  EXPECT_EQ(source_data.back()->source_event_id, 5UL);
+  EXPECT_EQ(source_data.back()->destination,
+            url::Origin::Create(GURL("https://advertiser.example")));
+}
+
+IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
+                       AttributionSrcImgSlowResponse_SourceRegistered) {
+  // Create a separate server as we cannot register a `ControllableHttpResponse`
+  // after the server starts.
+  auto https_server = std::make_unique<net::EmbeddedTestServer>(
+      net::EmbeddedTestServer::TYPE_HTTPS);
+  https_server->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
+  net::test_server::RegisterDefaultHandlers(https_server.get());
+  https_server->ServeFilesFromSourceDirectory(
+      "content/test/data/attribution_reporting");
+  https_server->ServeFilesFromSourceDirectory("content/test/data");
+  SetupCrossSiteRedirector(https_server.get());
+
+  auto register_response =
+      std::make_unique<net::test_server::ControllableHttpResponse>(
+          https_server.get(), "/register_source");
+  ASSERT_TRUE(https_server->Start());
+
+  GURL page_url =
+      https_server->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  MockAttributionHost host(web_contents());
+  std::unique_ptr<MockDataHost> data_host;
+  base::RunLoop loop;
+  EXPECT_CALL(host, RegisterDataHost)
+      .WillOnce(
+          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+            data_host = GetRegisteredDataHost(std::move(host));
+            loop.Quit();
+          });
+
+  GURL register_url = https_server->GetURL("d.test", "/register_source");
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
+
+  // Navigate cross-site before sending a response.
+  GURL page2_url =
+      https_server->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page2_url));
+
+  register_response->WaitForRequest();
+  auto http_response = std::make_unique<net::test_server::BasicHttpResponse>();
+  http_response->set_code(net::HTTP_OK);
+  http_response->AddCustomHeader("Access-Control-Allow-Origin", "*");
+  http_response->AddCustomHeader(
+      "Attribution-Reporting-Register-Source",
+      R"({"source_event_id":"5", "destination":"https://advertiser.example"})");
+  register_response->Send(http_response->ToResponseString());
+  register_response->Done();
+
+  if (!data_host)
+    loop.Run();
+  data_host->WaitForSourceData(/*num_source_data=*/1);
+  const auto& source_data = data_host->source_data();
+
+  // Only the second source is registered.
+  EXPECT_EQ(source_data.size(), 1u);
+  EXPECT_EQ(source_data.back()->source_event_id, 5UL);
+  EXPECT_EQ(source_data.back()->destination,
+            url::Origin::Create(GURL("https://advertiser.example")));
+}
+
+IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
+                       AttributionSrcImg_TriggerRegistered) {
+  GURL page_url =
+      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  MockAttributionHost host(web_contents());
+  std::unique_ptr<MockDataHost> data_host;
+  base::RunLoop loop;
+  EXPECT_CALL(host, RegisterDataHost)
+      .WillOnce(
+          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+            data_host = GetRegisteredDataHost(std::move(host));
+            loop.Quit();
+          });
+
+  GURL register_url =
+      https_server()->GetURL("c.test", "/register_trigger_headers.html");
+
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
+  if (!data_host)
+    loop.Run();
+  data_host->WaitForTriggerData(/*num_trigger_data=*/1);
+  const auto& trigger_data = data_host->trigger_data();
+
+  EXPECT_EQ(trigger_data.size(), 1u);
+  EXPECT_EQ(trigger_data.front()->reporting_origin,
+            url::Origin::Create(register_url));
+  EXPECT_EQ(trigger_data.front()->event_triggers.size(), 1u);
+  EXPECT_EQ(trigger_data.front()->event_triggers.front()->data, 10u);
+}
+
+IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
+                       AttributionSrcImg_TriggerRegisteredAllParams) {
+  GURL page_url =
+      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  MockAttributionHost host(web_contents());
+  std::unique_ptr<MockDataHost> data_host;
+  base::RunLoop loop;
+  EXPECT_CALL(host, RegisterDataHost)
+      .WillOnce(
+          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+            data_host = GetRegisteredDataHost(std::move(host));
+            loop.Quit();
+          });
+
+  GURL register_url = https_server()->GetURL(
+      "c.test", "/register_trigger_headers_all_params.html");
+
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
+  if (!data_host)
+    loop.Run();
+  data_host->WaitForTriggerData(/*num_trigger_data=*/1);
+  const auto& trigger_data = data_host->trigger_data();
+
+  EXPECT_EQ(trigger_data.size(), 1u);
+  EXPECT_EQ(trigger_data.front()->reporting_origin,
+            url::Origin::Create(register_url));
+  EXPECT_EQ(trigger_data.front()->event_triggers.size(), 2u);
+
+  // Verify first trigger.
+  const auto& event_trigger_datas = trigger_data.front()->event_triggers;
+  EXPECT_EQ(event_trigger_datas.front()->data, 1u);
+  EXPECT_EQ(event_trigger_datas.front()->priority, 5);
+  EXPECT_EQ(event_trigger_datas.front()->dedup_key->value, 1024u);
+
+  EXPECT_EQ(event_trigger_datas.back()->data, 2u);
+  EXPECT_EQ(event_trigger_datas.back()->priority, 10);
+  EXPECT_FALSE(event_trigger_datas.back()->dedup_key);
+}
+
+IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
+                       AttributionSrcImg_InvalidTriggerJsonIgnored) {
+  GURL page_url =
+      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  MockAttributionHost host(web_contents());
+  std::unique_ptr<MockDataHost> data_host;
+  base::RunLoop loop;
+  EXPECT_CALL(host, RegisterDataHost)
+      .WillOnce(
+          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+            data_host = GetRegisteredDataHost(std::move(host));
+            loop.Quit();
+          });
+
+  GURL register_url = https_server()->GetURL(
+      "c.test", "/register_trigger_headers_then_redirect_invalid.html");
+
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
+  if (!data_host)
+    loop.Run();
+  data_host->WaitForTriggerData(/*num_trigger_data=*/1);
+  const auto& trigger_data = data_host->trigger_data();
+
+  EXPECT_EQ(trigger_data.size(), 1u);
+  EXPECT_EQ(trigger_data.front()->reporting_origin,
+            url::Origin::Create(register_url));
+  EXPECT_EQ(trigger_data.front()->event_triggers.size(), 1u);
+  EXPECT_EQ(trigger_data.front()->event_triggers.front()->data, 10u);
+}
+
+IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
+                       AttributionSrcImgTriggerThenSource_SourceIgnored) {
+  GURL page_url =
+      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  MockAttributionHost host(web_contents());
+  std::unique_ptr<MockDataHost> data_host;
+  base::RunLoop loop;
+  EXPECT_CALL(host, RegisterDataHost)
+      .WillOnce(
+          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+            data_host = GetRegisteredDataHost(std::move(host));
+            loop.Quit();
+          });
+
+  GURL register_url =
+      https_server()->GetURL("c.test", "/register_trigger_source_trigger.html");
+
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
+  if (!data_host)
+    loop.Run();
+  data_host->WaitForTriggerData(/*num_trigger_data=*/2);
+  const auto& trigger_data = data_host->trigger_data();
+
+  EXPECT_EQ(trigger_data.size(), 2u);
+  EXPECT_EQ(trigger_data.front()->reporting_origin,
+            url::Origin::Create(register_url));
+
+  // Both triggers should be processed.
+  EXPECT_EQ(trigger_data.front()->event_triggers.front()->data, 5u);
+  EXPECT_EQ(trigger_data.back()->event_triggers.front()->data, 10u);
+
+  // Middle redirect source should be ignored.
+  EXPECT_EQ(data_host->source_data().size(), 0u);
+}
+
+class AttributionSrcInvalidFiltersBrowserTest
+    : public AttributionSrcBrowserTest,
+      public ::testing::WithParamInterface<const char*> {};
+
+IN_PROC_BROWSER_TEST_P(AttributionSrcInvalidFiltersBrowserTest,
+                       AttributionSrcImgFiltersInvalid_SourceDropped) {
+  // Create a separate server as we cannot register a `ControllableHttpResponse`
+  // after the server starts.
+  auto https_server = std::make_unique<net::EmbeddedTestServer>(
+      net::EmbeddedTestServer::TYPE_HTTPS);
+  https_server->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
+  net::test_server::RegisterDefaultHandlers(https_server.get());
+  https_server->ServeFilesFromSourceDirectory(
+      "content/test/data/attribution_reporting");
+  https_server->ServeFilesFromSourceDirectory("content/test/data");
+  SetupCrossSiteRedirector(https_server.get());
+
+  auto register_response =
+      std::make_unique<net::test_server::ControllableHttpResponse>(
+          https_server.get(), "/register_source");
+  ASSERT_TRUE(https_server->Start());
+
+  GURL page_url =
+      https_server->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  MockAttributionHost host(web_contents());
+  std::unique_ptr<MockDataHost> data_host;
+  base::RunLoop loop;
+  EXPECT_CALL(host, RegisterDataHost)
+      .WillOnce(
+          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+            data_host = GetRegisteredDataHost(std::move(host));
+            loop.Quit();
+          });
+
+  GURL register_url = https_server->GetURL("d.test", "/register_source");
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
+
+  register_response->WaitForRequest();
+  auto http_response = std::make_unique<net::test_server::BasicHttpResponse>();
+  http_response->set_code(net::HTTP_MOVED_PERMANENTLY);
+  http_response->AddCustomHeader(
+      "Attribution-Reporting-Register-Source",
+      base::StrCat(
+          {R"({"source_event_id":"9", "destination":"https://advertiser.example", "filter_data":)",
+           GetParam(), "}"}));
+  http_response->AddCustomHeader("Location", "/register_source_headers.html");
+  register_response->Send(http_response->ToResponseString());
+  register_response->Done();
+
+  if (!data_host)
+    loop.Run();
+  data_host->WaitForSourceData(/*num_source_data=*/1);
+  const auto& source_data = data_host->source_data();
+
+  // Only the second source is registered.
+  EXPECT_EQ(source_data.size(), 1u);
+  EXPECT_EQ(source_data.back()->source_event_id, 5UL);
+  EXPECT_EQ(source_data.back()->destination,
+            url::Origin::Create(GURL("https://advertiser.example")));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    AttributionSrcInvalidFilters,
+    AttributionSrcInvalidFiltersBrowserTest,
+    ::testing::Values(R"("x")",        // not a dictionary
+                      R"({"a":"y"})",  // dictionary value isn't an array
+                      R"({"b":[8]})"   // array value isn't a string
+                      ));
+
+class AttributionSrcFilterSizeBrowserTest
+    : public AttributionSrcBrowserTest,
+      public ::testing::WithParamInterface<AttributionFilterSizeTestCase> {};
+
+IN_PROC_BROWSER_TEST_P(AttributionSrcFilterSizeBrowserTest,
+                       AttributionSrcImgExcessiveFilterSize_SourceDropped) {
+  const AttributionFilterSizeTestCase& test_case = GetParam();
+
+  // Create a separate server as we cannot register a `ControllableHttpResponse`
+  // after the server starts.
+  auto https_server = std::make_unique<net::EmbeddedTestServer>(
+      net::EmbeddedTestServer::TYPE_HTTPS);
+  https_server->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
+  net::test_server::RegisterDefaultHandlers(https_server.get());
+  https_server->ServeFilesFromSourceDirectory(
+      "content/test/data/attribution_reporting");
+  https_server->ServeFilesFromSourceDirectory("content/test/data");
+  SetupCrossSiteRedirector(https_server.get());
+
+  auto register_response =
+      std::make_unique<net::test_server::ControllableHttpResponse>(
+          https_server.get(), "/register_source");
+  ASSERT_TRUE(https_server->Start());
+
+  GURL page_url =
+      https_server->GetURL("b.test", "/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  MockAttributionHost host(web_contents());
+  std::unique_ptr<MockDataHost> data_host;
+  base::RunLoop loop;
+  EXPECT_CALL(host, RegisterDataHost)
+      .WillOnce(
+          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+            data_host = GetRegisteredDataHost(std::move(host));
+            loop.Quit();
+          });
+
+  GURL register_url = https_server->GetURL("d.test", "/register_source");
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
+
+  register_response->WaitForRequest();
+  auto http_response = std::make_unique<net::test_server::BasicHttpResponse>();
+  http_response->set_code(net::HTTP_MOVED_PERMANENTLY);
+
+  base::Value dict(base::Value::Type::DICTIONARY);
+  dict.SetStringKey("source_event_id", "9");
+  dict.SetStringKey("destination", "https://advertiser.example");
+
+  base::Value filter_data(base::Value::Type::DICTIONARY);
+  for (auto [filter, values] : test_case.AsMap()) {
+    base::Value list(base::Value::Type::LIST);
+    for (auto value : values) {
+      list.Append(std::move(value));
+    }
+    filter_data.SetKey(std::move(filter), std::move(list));
+  }
+  dict.SetKey("filter_data", std::move(filter_data));
+
+  std::string json;
+  EXPECT_TRUE(base::JSONWriter::Write(dict, &json));
+
+  http_response->AddCustomHeader("Attribution-Reporting-Register-Source",
+                                 std::move(json));
+  http_response->AddCustomHeader("Location", "/register_source_headers.html");
+  register_response->Send(http_response->ToResponseString());
+  register_response->Done();
+
+  if (!data_host)
+    loop.Run();
+
+  const size_t expected_sources = test_case.valid ? 2 : 1;
+  data_host->WaitForSourceData(/*num_source_data=*/expected_sources);
+  const auto& source_data = data_host->source_data();
+
+  EXPECT_EQ(source_data.size(), expected_sources);
+  EXPECT_EQ(source_data.back()->source_event_id, 5UL);
+  EXPECT_EQ(source_data.back()->destination,
+            url::Origin::Create(GURL("https://advertiser.example")));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    AttributionSrcFilterSizes,
+    AttributionSrcFilterSizeBrowserTest,
+    ::testing::ValuesIn(kAttributionFilterSizeTestCases),
+    /*name_generator=*/
+    [](const ::testing::TestParamInfo<AttributionFilterSizeTestCase>& info) {
+      return info.param.description;
+    });
+
+// TODO(apaseltiner): Add tests for overlong filters.
+
+}  // namespace content
diff --git a/content/browser/attribution_reporting/attribution_test_utils.cc b/content/browser/attribution_reporting/attribution_test_utils.cc
index b58fc7b..c8a96fb4 100644
--- a/content/browser/attribution_reporting/attribution_test_utils.cc
+++ b/content/browser/attribution_reporting/attribution_test_utils.cc
@@ -67,6 +67,14 @@
   wait_loop_.Run();
 }
 
+void MockDataHost::WaitForTriggerData(size_t num_trigger_data) {
+  min_trigger_data_count_ = num_trigger_data;
+  if (trigger_data_.size() >= min_trigger_data_count_) {
+    return;
+  }
+  wait_loop_.Run();
+}
+
 void MockDataHost::SourceDataAvailable(
     blink::mojom::AttributionSourceDataPtr data) {
   source_data_.push_back(std::move(data));
@@ -76,6 +84,15 @@
   wait_loop_.Quit();
 }
 
+void MockDataHost::TriggerDataAvailable(
+    blink::mojom::AttributionTriggerDataPtr data) {
+  trigger_data_.push_back(std::move(data));
+  if (trigger_data_.size() < min_trigger_data_count_) {
+    return;
+  }
+  wait_loop_.Quit();
+}
+
 MockDataHostManager::MockDataHostManager() = default;
 
 MockDataHostManager::~MockDataHostManager() = default;
diff --git a/content/browser/attribution_reporting/attribution_test_utils.h b/content/browser/attribution_reporting/attribution_test_utils.h
index d952cbb..f484793 100644
--- a/content/browser/attribution_reporting/attribution_test_utils.h
+++ b/content/browser/attribution_reporting/attribution_test_utils.h
@@ -106,21 +106,33 @@
   ~MockDataHost() override;
 
   void WaitForSourceData(size_t num_source_data);
+  void WaitForTriggerData(size_t num_trigger_data);
 
   const std::vector<blink::mojom::AttributionSourceDataPtr>& source_data()
       const {
     return source_data_;
   }
 
+  const std::vector<blink::mojom::AttributionTriggerDataPtr>& trigger_data()
+      const {
+    return trigger_data_;
+  }
+
  private:
   // blink::mojom::AttributionDataHost:
   void SourceDataAvailable(
       blink::mojom::AttributionSourceDataPtr data) override;
+  void TriggerDataAvailable(
+      blink::mojom::AttributionTriggerDataPtr data) override;
 
   size_t min_source_data_count_ = 0;
-  mojo::Receiver<blink::mojom::AttributionDataHost> receiver_{this};
-  base::RunLoop wait_loop_;
   std::vector<blink::mojom::AttributionSourceDataPtr> source_data_;
+
+  size_t min_trigger_data_count_ = 0;
+  std::vector<blink::mojom::AttributionTriggerDataPtr> trigger_data_;
+
+  base::RunLoop wait_loop_;
+  mojo::Receiver<blink::mojom::AttributionDataHost> receiver_{this};
 };
 
 class MockDataHostManager : public AttributionDataHostManager {
diff --git a/content/browser/attribution_reporting/attributions_browsertest.cc b/content/browser/attribution_reporting/attributions_browsertest.cc
index c18886f..13e4fda 100644
--- a/content/browser/attribution_reporting/attributions_browsertest.cc
+++ b/content/browser/attribution_reporting/attributions_browsertest.cc
@@ -714,9 +714,8 @@
       "a.test",
       "/attribution_reporting/register_source_headers_debug_key.html");
 
-  EXPECT_TRUE(
-      ExecJs(web_contents(),
-             JsReplace("createAttributionSourceImg($1);", register_url)));
+  EXPECT_TRUE(ExecJs(web_contents(),
+                     JsReplace("createAttributionSrcImg($1);", register_url)));
 
   GURL conversion_url = https_server()->GetURL(
       "b.test", "/attribution_reporting/page_with_conversion_redirect.html");
diff --git a/content/browser/attribution_reporting/source_declaration_browsertest.cc b/content/browser/attribution_reporting/source_declaration_browsertest.cc
index 7932b9f..88cf306a 100644
--- a/content/browser/attribution_reporting/source_declaration_browsertest.cc
+++ b/content/browser/attribution_reporting/source_declaration_browsertest.cc
@@ -6,7 +6,6 @@
 
 #include "base/json/json_writer.h"
 #include "base/run_loop.h"
-#include "base/strings/strcat.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/values.h"
 #include "build/build_config.h"
@@ -42,11 +41,6 @@
 using ::testing::Pointee;
 using ::testing::UnorderedElementsAre;
 
-std::unique_ptr<MockDataHost> GetRegisteredDataHost(
-    mojo::PendingReceiver<blink::mojom::AttributionDataHost> data_host) {
-  return std::make_unique<MockDataHost>(std::move(data_host));
-}
-
 // WebContentsObserver that waits until a source is available on a
 // navigation handle for a finished navigation.
 class SourceObserver : public TestNavigationObserver {
@@ -167,275 +161,6 @@
 };
 
 IN_PROC_BROWSER_TEST_F(AttributionSourceDeclarationBrowserTest,
-                       AttributionSrcImg_SourceRegistered) {
-  SourceObserver source_observer(web_contents());
-  GURL page_url =
-      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
-  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
-
-  MockAttributionHost host(web_contents());
-  std::unique_ptr<MockDataHost> data_host;
-  base::RunLoop loop;
-  EXPECT_CALL(host, RegisterDataHost)
-      .WillOnce(
-          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
-            data_host = GetRegisteredDataHost(std::move(host));
-            loop.Quit();
-          });
-
-  GURL register_url =
-      https_server()->GetURL("c.test", "/register_source_headers.html");
-
-  EXPECT_TRUE(
-      ExecJs(web_contents(),
-             JsReplace("createAttributionSourceImg($1);", register_url)));
-  if (!data_host)
-    loop.Run();
-  data_host->WaitForSourceData(/*num_source_data=*/1);
-  const auto& source_data = data_host->source_data();
-
-  EXPECT_EQ(source_data.size(), 1u);
-  EXPECT_EQ(source_data.front()->source_event_id, 5UL);
-  EXPECT_EQ(source_data.front()->destination,
-            url::Origin::Create(GURL("https://advertiser.example")));
-  EXPECT_EQ(source_data.front()->priority, 0);
-  EXPECT_EQ(source_data.front()->expiry, absl::nullopt);
-  EXPECT_FALSE(source_data.front()->debug_key);
-  EXPECT_THAT(source_data.front()->filter_data->filter_values, IsEmpty());
-  EXPECT_THAT(source_data.front()->aggregatable_sources->sources, IsEmpty());
-}
-
-IN_PROC_BROWSER_TEST_F(AttributionSourceDeclarationBrowserTest,
-                       AttributionSrcImg_SourceRegisteredWithOptionalParams) {
-  SourceObserver source_observer(web_contents());
-  GURL page_url =
-      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
-  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
-
-  MockAttributionHost host(web_contents());
-  std::unique_ptr<MockDataHost> data_host;
-  base::RunLoop loop;
-  EXPECT_CALL(host, RegisterDataHost)
-      .WillOnce(
-          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
-            data_host = GetRegisteredDataHost(std::move(host));
-            loop.Quit();
-          });
-
-  GURL register_url = https_server()->GetURL(
-      "c.test", "/register_source_headers_all_params.html");
-
-  EXPECT_TRUE(
-      ExecJs(web_contents(),
-             JsReplace("createAttributionSourceImg($1);", register_url)));
-  if (!data_host)
-    loop.Run();
-  data_host->WaitForSourceData(/*num_source_data=*/1);
-  const auto& source_data = data_host->source_data();
-
-  EXPECT_EQ(source_data.size(), 1u);
-  EXPECT_EQ(source_data.front()->source_event_id, 5UL);
-  EXPECT_EQ(source_data.front()->destination,
-            url::Origin::Create(GURL("https://advertiser.example")));
-  EXPECT_EQ(source_data.front()->priority, 10);
-  EXPECT_EQ(source_data.front()->expiry, base::Seconds(1000));
-  EXPECT_EQ(source_data.front()->debug_key,
-            blink::mojom::AttributionDebugKey::New(789));
-  EXPECT_THAT(source_data.front()->filter_data->filter_values,
-              UnorderedElementsAre(Pair("a", IsEmpty()),
-                                   Pair("b", ElementsAre("1", "2"))));
-}
-
-IN_PROC_BROWSER_TEST_F(
-    AttributionSourceDeclarationBrowserTest,
-    AttributionSrcImg_SourceRegisteredWithAttributionAggregatableSources) {
-  SourceObserver source_observer(web_contents());
-  GURL page_url =
-      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
-  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
-
-  MockAttributionHost host(web_contents());
-  std::unique_ptr<MockDataHost> data_host;
-  base::RunLoop loop;
-  EXPECT_CALL(host, RegisterDataHost)
-      .WillOnce(
-          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
-            data_host = GetRegisteredDataHost(std::move(host));
-            loop.Quit();
-          });
-
-  GURL register_url = https_server()->GetURL(
-      "c.test", "/register_aggregatable_source_headers.html");
-
-  EXPECT_TRUE(
-      ExecJs(web_contents(),
-             JsReplace("createAttributionSourceImg($1);", register_url)));
-  if (!data_host)
-    loop.Run();
-  data_host->WaitForSourceData(/*num_source_data=*/1);
-  const auto& source_data = data_host->source_data();
-
-  EXPECT_EQ(source_data.size(), 1u);
-  EXPECT_EQ(source_data.front()->source_event_id, 5UL);
-  EXPECT_EQ(source_data.front()->destination,
-            url::Origin::Create(GURL("https://advertiser.example")));
-  EXPECT_EQ(source_data.front()->priority, 0);
-  EXPECT_EQ(source_data.front()->expiry, absl::nullopt);
-  EXPECT_FALSE(source_data.front()->debug_key);
-  EXPECT_THAT(
-      source_data.front()->aggregatable_sources->sources,
-      UnorderedElementsAre(
-          Pair("key1",
-               Pointee(AllOf(
-                   Field(&blink::mojom::AttributionAggregatableKey::high_bits,
-                         0),
-                   Field(&blink::mojom::AttributionAggregatableKey::low_bits,
-                         5)))),
-          Pair("key2",
-               Pointee(AllOf(
-                   Field(&blink::mojom::AttributionAggregatableKey::high_bits,
-                         0),
-                   Field(&blink::mojom::AttributionAggregatableKey::low_bits,
-                         345))))));
-}
-
-IN_PROC_BROWSER_TEST_F(AttributionSourceDeclarationBrowserTest,
-                       AttributionSrcImgRedirect_MultipleSourcesRegistered) {
-  SourceObserver source_observer(web_contents());
-  GURL page_url =
-      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
-  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
-
-  MockAttributionHost host(web_contents());
-  std::unique_ptr<MockDataHost> data_host;
-  base::RunLoop loop;
-  EXPECT_CALL(host, RegisterDataHost)
-      .WillOnce(
-          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
-            data_host = GetRegisteredDataHost(std::move(host));
-            loop.Quit();
-          });
-
-  GURL register_url = https_server()->GetURL(
-      "c.test", "/register_source_headers_and_redirect.html");
-
-  EXPECT_TRUE(
-      ExecJs(web_contents(),
-             JsReplace("createAttributionSourceImg($1);", register_url)));
-  if (!data_host)
-    loop.Run();
-  data_host->WaitForSourceData(/*num_source_data=*/2);
-  const auto& source_data = data_host->source_data();
-
-  EXPECT_EQ(source_data.size(), 2u);
-  EXPECT_EQ(source_data.front()->source_event_id, 1UL);
-  EXPECT_EQ(source_data.front()->destination,
-            url::Origin::Create(GURL("https://advertiser.example")));
-  EXPECT_EQ(source_data.back()->source_event_id, 5UL);
-  EXPECT_EQ(source_data.back()->destination,
-            url::Origin::Create(GURL("https://advertiser.example")));
-}
-
-IN_PROC_BROWSER_TEST_F(AttributionSourceDeclarationBrowserTest,
-                       AttributionSrcImgRedirect_InvalidJsonIgnored) {
-  SourceObserver source_observer(web_contents());
-  GURL page_url =
-      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
-  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
-
-  MockAttributionHost host(web_contents());
-  std::unique_ptr<MockDataHost> data_host;
-  base::RunLoop loop;
-  EXPECT_CALL(host, RegisterDataHost)
-      .WillOnce(
-          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
-            data_host = GetRegisteredDataHost(std::move(host));
-            loop.Quit();
-          });
-
-  GURL register_url = https_server()->GetURL(
-      "c.test", "/register_source_headers_and_redirect_invalid.html");
-
-  EXPECT_TRUE(
-      ExecJs(web_contents(),
-             JsReplace("createAttributionSourceImg($1);", register_url)));
-  if (!data_host)
-    loop.Run();
-  data_host->WaitForSourceData(/*num_source_data=*/1);
-  const auto& source_data = data_host->source_data();
-
-  // Only the second source is registered.
-  EXPECT_EQ(source_data.size(), 1u);
-  EXPECT_EQ(source_data.back()->source_event_id, 5UL);
-  EXPECT_EQ(source_data.back()->destination,
-            url::Origin::Create(GURL("https://advertiser.example")));
-}
-
-IN_PROC_BROWSER_TEST_F(AttributionSourceDeclarationBrowserTest,
-                       AttributionSrcImgSlowResponse_SourceRegistered) {
-  // Create a separate server as we cannot register a `ControllableHttpResponse`
-  // after the server starts.
-  auto https_server = std::make_unique<net::EmbeddedTestServer>(
-      net::EmbeddedTestServer::TYPE_HTTPS);
-  https_server->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
-  net::test_server::RegisterDefaultHandlers(https_server.get());
-  https_server->ServeFilesFromSourceDirectory(
-      "content/test/data/attribution_reporting");
-  https_server->ServeFilesFromSourceDirectory("content/test/data");
-  SetupCrossSiteRedirector(https_server.get());
-
-  auto register_response =
-      std::make_unique<net::test_server::ControllableHttpResponse>(
-          https_server.get(), "/register_source");
-  ASSERT_TRUE(https_server->Start());
-
-  GURL page_url =
-      https_server->GetURL("b.test", "/page_with_impression_creator.html");
-  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
-
-  MockAttributionHost host(web_contents());
-  std::unique_ptr<MockDataHost> data_host;
-  base::RunLoop loop;
-  EXPECT_CALL(host, RegisterDataHost)
-      .WillOnce(
-          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
-            data_host = GetRegisteredDataHost(std::move(host));
-            loop.Quit();
-          });
-
-  GURL register_url = https_server->GetURL("d.test", "/register_source");
-  EXPECT_TRUE(
-      ExecJs(web_contents(),
-             JsReplace("createAttributionSourceImg($1);", register_url)));
-
-  // Navigate cross-site before sending a response.
-  GURL page2_url =
-      https_server->GetURL("b.test", "/page_with_impression_creator.html");
-  EXPECT_TRUE(NavigateToURL(web_contents(), page2_url));
-
-  register_response->WaitForRequest();
-  auto http_response = std::make_unique<net::test_server::BasicHttpResponse>();
-  http_response->set_code(net::HTTP_OK);
-  http_response->AddCustomHeader("Access-Control-Allow-Origin", "*");
-  http_response->AddCustomHeader(
-      "Attribution-Reporting-Register-Source",
-      R"({"source_event_id":"5", "destination":"https://advertiser.example"})");
-  register_response->Send(http_response->ToResponseString());
-  register_response->Done();
-
-  if (!data_host)
-    loop.Run();
-  data_host->WaitForSourceData(/*num_source_data=*/1);
-  const auto& source_data = data_host->source_data();
-
-  // Only the second source is registered.
-  EXPECT_EQ(source_data.size(), 1u);
-  EXPECT_EQ(source_data.back()->source_event_id, 5UL);
-  EXPECT_EQ(source_data.back()->destination,
-            url::Origin::Create(GURL("https://advertiser.example")));
-}
-
-IN_PROC_BROWSER_TEST_F(AttributionSourceDeclarationBrowserTest,
                        ImpressionTagClicked_ImpressionReceived) {
   SourceObserver source_observer(web_contents());
   GURL page_url =
@@ -1262,171 +987,4 @@
   EXPECT_TRUE(source_observer.WaitForNavigationWithNoImpression());
 }
 
-class AttributionSourceDeclarationInvalidFiltersBrowserTest
-    : public AttributionSourceDeclarationBrowserTest,
-      public ::testing::WithParamInterface<const char*> {};
-
-IN_PROC_BROWSER_TEST_P(AttributionSourceDeclarationInvalidFiltersBrowserTest,
-                       AttributionSrcImgFiltersInvalid_SourceDropped) {
-  // Create a separate server as we cannot register a `ControllableHttpResponse`
-  // after the server starts.
-  auto https_server = std::make_unique<net::EmbeddedTestServer>(
-      net::EmbeddedTestServer::TYPE_HTTPS);
-  https_server->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
-  net::test_server::RegisterDefaultHandlers(https_server.get());
-  https_server->ServeFilesFromSourceDirectory(
-      "content/test/data/attribution_reporting");
-  https_server->ServeFilesFromSourceDirectory("content/test/data");
-  SetupCrossSiteRedirector(https_server.get());
-
-  auto register_response =
-      std::make_unique<net::test_server::ControllableHttpResponse>(
-          https_server.get(), "/register_source");
-  ASSERT_TRUE(https_server->Start());
-
-  GURL page_url =
-      https_server->GetURL("b.test", "/page_with_impression_creator.html");
-  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
-
-  MockAttributionHost host(web_contents());
-  std::unique_ptr<MockDataHost> data_host;
-  base::RunLoop loop;
-  EXPECT_CALL(host, RegisterDataHost)
-      .WillOnce(
-          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
-            data_host = GetRegisteredDataHost(std::move(host));
-            loop.Quit();
-          });
-
-  GURL register_url = https_server->GetURL("d.test", "/register_source");
-  EXPECT_TRUE(
-      ExecJs(web_contents(),
-             JsReplace("createAttributionSourceImg($1);", register_url)));
-
-  register_response->WaitForRequest();
-  auto http_response = std::make_unique<net::test_server::BasicHttpResponse>();
-  http_response->set_code(net::HTTP_MOVED_PERMANENTLY);
-  http_response->AddCustomHeader(
-      "Attribution-Reporting-Register-Source",
-      base::StrCat(
-          {R"({"source_event_id":"9", "destination":"https://advertiser.example", "filter_data":)",
-           GetParam(), "}"}));
-  http_response->AddCustomHeader("Location", "/register_source_headers.html");
-  register_response->Send(http_response->ToResponseString());
-  register_response->Done();
-
-  if (!data_host)
-    loop.Run();
-  data_host->WaitForSourceData(/*num_source_data=*/1);
-  const auto& source_data = data_host->source_data();
-
-  // Only the second source is registered.
-  EXPECT_EQ(source_data.size(), 1u);
-  EXPECT_EQ(source_data.back()->source_event_id, 5UL);
-  EXPECT_EQ(source_data.back()->destination,
-            url::Origin::Create(GURL("https://advertiser.example")));
-}
-
-INSTANTIATE_TEST_SUITE_P(
-    AttributionSourceDeclarationInvalidFilters,
-    AttributionSourceDeclarationInvalidFiltersBrowserTest,
-    ::testing::Values(R"("x")",        // not a dictionary
-                      R"({"a":"y"})",  // dictionary value isn't an array
-                      R"({"b":[8]})"   // array value isn't a string
-                      ));
-
-class AttributionSourceDeclarationFilterSizeBrowserTest
-    : public AttributionSourceDeclarationBrowserTest,
-      public ::testing::WithParamInterface<AttributionFilterSizeTestCase> {};
-
-IN_PROC_BROWSER_TEST_P(AttributionSourceDeclarationFilterSizeBrowserTest,
-                       AttributionSrcImgExcessiveFilterSize_SourceDropped) {
-  const AttributionFilterSizeTestCase& test_case = GetParam();
-
-  // Create a separate server as we cannot register a `ControllableHttpResponse`
-  // after the server starts.
-  auto https_server = std::make_unique<net::EmbeddedTestServer>(
-      net::EmbeddedTestServer::TYPE_HTTPS);
-  https_server->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
-  net::test_server::RegisterDefaultHandlers(https_server.get());
-  https_server->ServeFilesFromSourceDirectory(
-      "content/test/data/attribution_reporting");
-  https_server->ServeFilesFromSourceDirectory("content/test/data");
-  SetupCrossSiteRedirector(https_server.get());
-
-  auto register_response =
-      std::make_unique<net::test_server::ControllableHttpResponse>(
-          https_server.get(), "/register_source");
-  ASSERT_TRUE(https_server->Start());
-
-  GURL page_url =
-      https_server->GetURL("b.test", "/page_with_impression_creator.html");
-  EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
-
-  MockAttributionHost host(web_contents());
-  std::unique_ptr<MockDataHost> data_host;
-  base::RunLoop loop;
-  EXPECT_CALL(host, RegisterDataHost)
-      .WillOnce(
-          [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
-            data_host = GetRegisteredDataHost(std::move(host));
-            loop.Quit();
-          });
-
-  GURL register_url = https_server->GetURL("d.test", "/register_source");
-  EXPECT_TRUE(
-      ExecJs(web_contents(),
-             JsReplace("createAttributionSourceImg($1);", register_url)));
-
-  register_response->WaitForRequest();
-  auto http_response = std::make_unique<net::test_server::BasicHttpResponse>();
-  http_response->set_code(net::HTTP_MOVED_PERMANENTLY);
-
-  base::Value dict(base::Value::Type::DICTIONARY);
-  dict.SetStringKey("source_event_id", "9");
-  dict.SetStringKey("destination", "https://advertiser.example");
-
-  base::Value filter_data(base::Value::Type::DICTIONARY);
-  for (auto [filter, values] : test_case.AsMap()) {
-    base::Value list(base::Value::Type::LIST);
-    for (auto value : values) {
-      list.Append(std::move(value));
-    }
-    filter_data.SetKey(std::move(filter), std::move(list));
-  }
-  dict.SetKey("filter_data", std::move(filter_data));
-
-  std::string json;
-  EXPECT_TRUE(base::JSONWriter::Write(dict, &json));
-
-  http_response->AddCustomHeader("Attribution-Reporting-Register-Source",
-                                 std::move(json));
-  http_response->AddCustomHeader("Location", "/register_source_headers.html");
-  register_response->Send(http_response->ToResponseString());
-  register_response->Done();
-
-  if (!data_host)
-    loop.Run();
-
-  const size_t expected_sources = test_case.valid ? 2 : 1;
-  data_host->WaitForSourceData(/*num_source_data=*/expected_sources);
-  const auto& source_data = data_host->source_data();
-
-  EXPECT_EQ(source_data.size(), expected_sources);
-  EXPECT_EQ(source_data.back()->source_event_id, 5UL);
-  EXPECT_EQ(source_data.back()->destination,
-            url::Origin::Create(GURL("https://advertiser.example")));
-}
-
-INSTANTIATE_TEST_SUITE_P(
-    AttributionSourceDeclarationFilterSizes,
-    AttributionSourceDeclarationFilterSizeBrowserTest,
-    ::testing::ValuesIn(kAttributionFilterSizeTestCases),
-    /*name_generator=*/
-    [](const ::testing::TestParamInfo<AttributionFilterSizeTestCase>& info) {
-      return info.param.description;
-    });
-
-// TODO(apaseltiner): Add tests for overlong filters.
-
 }  // namespace content
diff --git a/content/browser/renderer_host/back_forward_cache_impl.cc b/content/browser/renderer_host/back_forward_cache_impl.cc
index a482999..73e7904 100644
--- a/content/browser/renderer_host/back_forward_cache_impl.cc
+++ b/content/browser/renderer_host/back_forward_cache_impl.cc
@@ -692,9 +692,9 @@
   // flattened list, and return the tree if needed.
   std::unique_ptr<BackForwardCacheCanStoreTreeResult> result_tree;
   if (rfh->IsInPrimaryMainFrame() || main_frame_in_bfcache) {
-    result_tree = PopulateReasonsForDocumentAndDescendants(
-        rfh, rfh->GetLastCommittedOrigin(), flattened_result,
-        include_non_sticky, create_tree);
+    NotRestoredReasonBuilder builder(rfh, include_non_sticky, create_tree);
+    result_tree = builder.GetTreeResult();
+    flattened_result.AddReasonsFrom(builder.GetFlattenedResult());
   } else {
     result_tree = BackForwardCacheCanStoreTreeResult::CreateEmptyTree(rfh);
   }
@@ -961,37 +961,55 @@
   }
 }
 
-std::unique_ptr<BackForwardCacheCanStoreTreeResult>
-BackForwardCacheImpl::PopulateReasonsForDocumentAndDescendants(
-    RenderFrameHostImpl* rfh,
-    const url::Origin& main_origin,
-    BackForwardCacheCanStoreDocumentResult& flattened_result,
+BackForwardCacheImpl::NotRestoredReasonBuilder::NotRestoredReasonBuilder(
+    RenderFrameHostImpl* root_rfh,
     bool include_non_sticky,
-    bool create_tree) {
-  BackForwardCacheCanStoreDocumentResult result_for_this_document;
-  PopulateReasonsForDocument(result_for_this_document, rfh, include_non_sticky);
-  flattened_result.AddReasonsFrom(result_for_this_document);
+    bool create_tree)
+    : root_rfh_(root_rfh),
+      bfcache_(root_rfh_->frame_tree_node()
+                   ->navigator()
+                   .controller()
+                   .GetBackForwardCache()),
+      include_non_sticky_(include_non_sticky),
+      create_tree_(create_tree) {
+  // |root_rfh_| should be either primary main frame or back/forward cached
+  // page's main frame.
+  DCHECK(root_rfh_->IsInPrimaryMainFrame() ||
+         (root_rfh_->IsInBackForwardCache() && root_rfh_->is_main_frame()));
+  // Populate the reasons and build the tree if needed.
+  tree_result_ = PopulateReasonsAndReturnSubtreeIfNeededFor(root_rfh_);
+}
+
+BackForwardCacheImpl::NotRestoredReasonBuilder::~NotRestoredReasonBuilder() =
+    default;
+
+std::unique_ptr<BackForwardCacheCanStoreTreeResult> BackForwardCacheImpl::
+    NotRestoredReasonBuilder::PopulateReasonsAndReturnSubtreeIfNeededFor(
+        RenderFrameHostImpl* rfh) {
+  BackForwardCacheCanStoreDocumentResult result_for_rfh;
+  // Populate |result_for_rfh| by checking the bfcache eligibility of |rfh|.
+  bfcache_.PopulateReasonsForDocument(result_for_rfh, rfh, include_non_sticky_);
+  flattened_result_.AddReasonsFrom(result_for_rfh);
 
   // Finds the reasons recursively and create the reason subtree for the
   // children if needed.
   BackForwardCacheCanStoreTreeResult::ChildrenVector children_result;
   for (size_t i = 0; i < rfh->child_count(); i++) {
     std::unique_ptr<BackForwardCacheCanStoreTreeResult> child =
-        PopulateReasonsForDocumentAndDescendants(
-            rfh->child_at(i)->current_frame_host(), main_origin,
-            flattened_result, include_non_sticky, create_tree);
-    if (create_tree) {
+        PopulateReasonsAndReturnSubtreeIfNeededFor(
+            rfh->child_at(i)->current_frame_host());
+    if (create_tree_) {
       children_result.emplace_back(std::move(child));
     }
   }
 
-  if (!create_tree)
+  if (!create_tree_)
     return nullptr;
 
   std::unique_ptr<BackForwardCacheCanStoreTreeResult> tree(
-      new BackForwardCacheCanStoreTreeResult(rfh, main_origin,
-                                             result_for_this_document,
-                                             std::move(children_result)));
+      new BackForwardCacheCanStoreTreeResult(
+          rfh, root_rfh_->GetLastCommittedOrigin(), result_for_rfh,
+          std::move(children_result)));
   return tree;
 }
 
diff --git a/content/browser/renderer_host/back_forward_cache_impl.h b/content/browser/renderer_host/back_forward_cache_impl.h
index 75541b14..d4d2209 100644
--- a/content/browser/renderer_host/back_forward_cache_impl.h
+++ b/content/browser/renderer_host/back_forward_cache_impl.h
@@ -360,18 +360,6 @@
       bool include_non_sticky,
       bool create_tree);
 
-  // Populates the reasons why this |rfh| and its subframes cannot enter the
-  // back/forward cache.
-  // |main_origin| is the origin of the outermost document. Refer to
-  // |PopulateReasonsForPage| for other params.
-  std::unique_ptr<BackForwardCacheCanStoreTreeResult>
-  PopulateReasonsForDocumentAndDescendants(
-      RenderFrameHostImpl* rfh,
-      const url::Origin& main_origin,
-      BackForwardCacheCanStoreDocumentResult& flattened_result,
-      bool include_non_sticky,
-      bool create_tree);
-
   // Populates the sticky reasons for `rfh` without recursing into subframes.
   // Sticky features can't be unregistered and remain active for the rest of the
   // lifetime of the page.
@@ -463,6 +451,56 @@
 
   const UnloadSupportStrategy unload_strategy_;
 
+  // Helper class to iterate through the frame tree in the page and populate the
+  // NotRestoredReasons.
+  class NotRestoredReasonBuilder {
+   public:
+    // |rfh_root| represents the root document of the page. |include_non_sticky|
+    // controls whether or not we should record non-sticky reasons in the tree,
+    // and |create_tree| controls whether or not we should build
+    // |BackForwardCacheCanStoreTreeResult|. If |create_tree| is false, we only
+    // record them in a flattened list.
+    NotRestoredReasonBuilder(RenderFrameHostImpl* rfh_root,
+                             bool include_non_sticky,
+                             bool create_tree);
+
+    ~NotRestoredReasonBuilder();
+
+    // Access the populated result.
+    BackForwardCacheCanStoreDocumentResult GetFlattenedResult() {
+      // TODO(yuzus): Check that |flattened_result_| and the tree result match.
+      return flattened_result_;
+    }
+
+    std::unique_ptr<BackForwardCacheCanStoreTreeResult> GetTreeResult() {
+      return std::move(tree_result_);
+    }
+
+   private:
+    // Populate NotRestoredReasons for the subtree whose root is |rfh| by
+    // iterating the frame tree and populating NotRestoredReasons in
+    // |flattened_result_|. This will return nullptr if |create_tree| is false,
+    // and returns a NotRestoredReason tree otherwise.
+    std::unique_ptr<BackForwardCacheCanStoreTreeResult>
+    PopulateReasonsAndReturnSubtreeIfNeededFor(RenderFrameHostImpl* rfh);
+
+    // Root document of the tree.
+    RenderFrameHostImpl* const root_rfh_;
+    // BackForwardCacheImpl instance to access eligibility check functions.
+    BackForwardCacheImpl& bfcache_;
+    // Flattened list of NotRestoredReasons for the tree. This is empty at the
+    // start and has to be merged using |GetFlattenedResult()|.
+    BackForwardCacheCanStoreDocumentResult flattened_result_;
+    // Tree result of NotRestoredReasons. This is populated in the constructor.
+    std::unique_ptr<BackForwardCacheCanStoreTreeResult> tree_result_;
+    // If true, check both non-sticky reasons and sticky reasons. If false,
+    // check only sticky reasons.
+    const bool include_non_sticky_;
+    // If true, construct a tree of NotRestoredReasons representing the frame
+    // tree structure. If false, only populate |flattened_result_|.
+    const bool create_tree_;
+  };
+
   base::WeakPtrFactory<BackForwardCacheImpl> weak_factory_;
 };
 
diff --git a/content/public/android/javatests/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityTreeTest.java b/content/public/android/javatests/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityTreeTest.java
index 2d6943b..0ce9841a 100644
--- a/content/public/android/javatests/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityTreeTest.java
+++ b/content/public/android/javatests/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityTreeTest.java
@@ -1855,6 +1855,12 @@
 
     @Test
     @SmallTest
+    public void test_interactiveControlsWithLabels() {
+        performHtmlTest("interactive-controls-with-labels.html");
+    }
+
+    @Test
+    @SmallTest
     public void test_isInteresting() {
         performHtmlTest("isInteresting.html");
     }
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index ae062bd..cb8f39c 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -1086,6 +1086,7 @@
     "../browser/accessibility/snapshot_ax_tree_browsertest.cc",
     "../browser/accessibility/touch_accessibility_aura_browsertest.cc",
     "../browser/attribution_reporting/attribution_internals_browsertest.cc",
+    "../browser/attribution_reporting/attribution_src_browsertest.cc",
     "../browser/attribution_reporting/attributions_browsertest.cc",
     "../browser/attribution_reporting/attributions_origin_trial_browsertest.cc",
     "../browser/attribution_reporting/source_declaration_browsertest.cc",
diff --git a/content/test/data/accessibility/accname/desc-combobox-focusable-expected-android-external.txt b/content/test/data/accessibility/accname/desc-combobox-focusable-expected-android-external.txt
index 5636d0f..04a2c360 100644
--- a/content/test/data/accessibility/accname/desc-combobox-focusable-expected-android-external.txt
+++ b/content/test/data/accessibility/accname/desc-combobox-focusable-expected-android-external.txt
@@ -1,2 +1,2 @@
 WebView focusable focused scrollable actions:[CLEAR_FOCUS, AX_FOCUS] bundle:[chromeRole="rootWebArea"]
-++Spinner text:"Choose your language." viewIdResName:"test" canOpenPopUp clickable focusable actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="comboBoxMenuButton"]
\ No newline at end of file
+++Spinner text:"English" hint:"Choose your language." viewIdResName:"test" canOpenPopUp clickable focusable actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="comboBoxMenuButton", hint="Choose your language."]
\ No newline at end of file
diff --git a/content/test/data/accessibility/accname/desc-combobox-focusable-expected-android.txt b/content/test/data/accessibility/accname/desc-combobox-focusable-expected-android.txt
index 9ab123e..cd370de 100644
--- a/content/test/data/accessibility/accname/desc-combobox-focusable-expected-android.txt
+++ b/content/test/data/accessibility/accname/desc-combobox-focusable-expected-android.txt
@@ -1 +1 @@
-android.widget.Spinner
+android.widget.Spinner hint='Choose your language.'
\ No newline at end of file
diff --git a/content/test/data/accessibility/accname/name-combobox-focusable-expected-android-external.txt b/content/test/data/accessibility/accname/name-combobox-focusable-expected-android-external.txt
index 5636d0f..04a2c360 100644
--- a/content/test/data/accessibility/accname/name-combobox-focusable-expected-android-external.txt
+++ b/content/test/data/accessibility/accname/name-combobox-focusable-expected-android-external.txt
@@ -1,2 +1,2 @@
 WebView focusable focused scrollable actions:[CLEAR_FOCUS, AX_FOCUS] bundle:[chromeRole="rootWebArea"]
-++Spinner text:"Choose your language." viewIdResName:"test" canOpenPopUp clickable focusable actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="comboBoxMenuButton"]
\ No newline at end of file
+++Spinner text:"English" hint:"Choose your language." viewIdResName:"test" canOpenPopUp clickable focusable actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="comboBoxMenuButton", hint="Choose your language."]
\ No newline at end of file
diff --git a/content/test/data/accessibility/accname/name-combobox-focusable-expected-android.txt b/content/test/data/accessibility/accname/name-combobox-focusable-expected-android.txt
index c4d799a..954164e 100644
--- a/content/test/data/accessibility/accname/name-combobox-focusable-expected-android.txt
+++ b/content/test/data/accessibility/accname/name-combobox-focusable-expected-android.txt
@@ -1 +1 @@
-android.widget.Spinner name='Choose your language.'
+android.widget.Spinner name='English' hint='Choose your language.'
\ No newline at end of file
diff --git a/content/test/data/accessibility/aria/aria-combobox-implicit-haspopup-expected-android-external.txt b/content/test/data/accessibility/aria/aria-combobox-implicit-haspopup-expected-android-external.txt
index db3df51..c3ab803 100644
--- a/content/test/data/accessibility/aria/aria-combobox-implicit-haspopup-expected-android-external.txt
+++ b/content/test/data/accessibility/aria/aria-combobox-implicit-haspopup-expected-android-external.txt
@@ -1,5 +1,5 @@
 WebView focusable focused scrollable actions:[CLEAR_FOCUS, AX_FOCUS] bundle:[chromeRole="rootWebArea"]
-++View text:"ComboBoxGrouping" canOpenPopUp actions:[AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="comboBoxGrouping"]
+++View hint:"ComboBoxGrouping" canOpenPopUp actions:[AX_FOCUS] bundle:[chromeRole="comboBoxGrouping", hint="ComboBoxGrouping"]
 ++++EditText clickable editable focusable inputType:1 textSelectionStart:0 textSelectionEnd:0 actions:[FOCUS, CLICK, AX_FOCUS, PASTE, SET_TEXT, IME_ENTER] bundle:[chromeRole="textField", clickableScore="300"]
 ++EditText hint:"TextFieldWithComboBox" canOpenPopUp clickable editable focusable inputType:1 textSelectionStart:0 textSelectionEnd:0 actions:[FOCUS, CLICK, AX_FOCUS, PASTE, SET_TEXT, IME_ENTER] bundle:[chromeRole="textFieldWithComboBox", clickableScore="300", hint="TextFieldWithComboBox"]
-++Spinner text:"ComboBoxMenuButton"" canOpenPopUp clickable focusable actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="comboBoxMenuButton"]
\ No newline at end of file
+++Spinner text:"Select" hint:"ComboBoxMenuButton"" canOpenPopUp clickable focusable actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="comboBoxMenuButton", hint="ComboBoxMenuButton""]
\ No newline at end of file
diff --git a/content/test/data/accessibility/aria/aria-combobox-uneditable-expected-android-external.txt b/content/test/data/accessibility/aria/aria-combobox-uneditable-expected-android-external.txt
index 778c3c96..88265f8 100644
--- a/content/test/data/accessibility/aria/aria-combobox-uneditable-expected-android-external.txt
+++ b/content/test/data/accessibility/aria/aria-combobox-uneditable-expected-android-external.txt
@@ -1,6 +1,6 @@
 WebView focusable focused scrollable actions:[CLEAR_FOCUS, AX_FOCUS] bundle:[chromeRole="rootWebArea"]
 ++TextView text:"Choose a fruit, with text content" viewIdResName:"combo1-label" actions:[AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="genericContainer"]
-++Spinner text:"Choose a fruit, with text content" canOpenPopUp clickable focusable actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS, EXPAND] bundle:[chromeRole="comboBoxMenuButton"]
+++Spinner text:"Apple" hint:"Choose a fruit, with text content" canOpenPopUp clickable focusable actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS, EXPAND] bundle:[chromeRole="comboBoxMenuButton", hint="Choose a fruit, with text content"]
 ++ListView viewIdResName:"listbox1" stateDescription:"3 items" clickable CollectionInfo:[rows=3, cols=0] actions:[CLICK, AX_FOCUS] bundle:[chromeRole="listBox", clickableScore="300", roleDescription="list box"]
 ++++View text:"Apple" viewIdResName:"combo1-0" stateDescription:"in list, item 1 of 3" clickable focusable selected CollectionItemInfo:[rowIndex=0, rowSpan=0, colIndex=0, colSpan=0] actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="listBoxOption", clickableScore="200"]
 ++++View text:"Banana" viewIdResName:"combo1-1" stateDescription:"in list, item 2 of 3" clickable focusable CollectionItemInfo:[rowIndex=1, rowSpan=0, colIndex=0, colSpan=0] actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="listBoxOption", clickableScore="200"]
diff --git a/content/test/data/accessibility/aria/aria-combobox-uneditable-expected-android.txt b/content/test/data/accessibility/aria/aria-combobox-uneditable-expected-android.txt
index 77d084d..2fd4151 100644
--- a/content/test/data/accessibility/aria/aria-combobox-uneditable-expected-android.txt
+++ b/content/test/data/accessibility/aria/aria-combobox-uneditable-expected-android.txt
@@ -1,6 +1,6 @@
 android.webkit.WebView focusable focused scrollable
 ++android.widget.TextView name='Choose a fruit, with text content'
-++android.widget.Spinner clickable collapsed focusable name='Choose a fruit, with text content'
+++android.widget.Spinner clickable collapsed focusable name='Apple' hint='Choose a fruit, with text content'
 ++android.widget.ListView role_description='list box' clickable collection state_description='3 items' item_count=3 row_count=3
 ++++android.view.View clickable collection_item focusable selected name='Apple' state_description='in list, item 1 of 3'
 ++++android.view.View clickable collection_item focusable name='Banana' state_description='in list, item 2 of 3' item_index=1 row_index=1
diff --git a/content/test/data/accessibility/html/interactive-controls-with-labels-expected-android-external.txt b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-android-external.txt
new file mode 100644
index 0000000..e3b7dc7
--- /dev/null
+++ b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-android-external.txt
@@ -0,0 +1,28 @@
+WebView focusable focused scrollable actions:[CLEAR_FOCUS, AX_FOCUS] bundle:[chromeRole="rootWebArea"]
+++View text:"Test label" viewIdResName:"label1" actions:[AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="labelText"]
+++TextView text:"aria label" actions:[AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="genericContainer"]
+++TextView text:"Test label" actions:[AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="genericContainer"]
+++EditText text:"Textbox" hint:"aria label" clickable editable textSelectionStart:0 textSelectionEnd:0 actions:[CLICK, AX_FOCUS, NEXT, PREVIOUS, COPY, PASTE, CUT, SET_SELECTION, SET_TEXT, IME_ENTER] bundle:[chromeRole="textField", hint="aria label"]
+++EditText text:"Textbox with aria-labelledby" hint:"Test label" clickable editable textSelectionStart:0 textSelectionEnd:0 actions:[CLICK, AX_FOCUS, NEXT, PREVIOUS, COPY, PASTE, CUT, SET_SELECTION, SET_TEXT, IME_ENTER] bundle:[chromeRole="textField", hint="Test label"]
+++Button text:"aria label" clickable actions:[CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="button", roleDescription="button"]
+++Button text:"Test label" clickable actions:[CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="button", roleDescription="button"]
+++EditText hint:"aria label" canOpenPopUp clickable editable focusable inputType:1 textSelectionStart:0 textSelectionEnd:0 actions:[FOCUS, CLICK, AX_FOCUS, PASTE, SET_TEXT, IME_ENTER] bundle:[chromeRole="textFieldWithComboBox", clickableScore="300", hint="aria label"]
+++ListView stateDescription:"2 items" clickable CollectionInfo:[rows=2, cols=0] actions:[CLICK, AX_FOCUS] bundle:[chromeRole="listBox", roleDescription="list box"]
+++++View text:"Option 1" viewIdResName:"option1" stateDescription:"in list, item 1 of 2" focusable CollectionItemInfo:[rowIndex=0, rowSpan=0, colIndex=0, colSpan=0] actions:[FOCUS, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="listBoxOption"]
+++++View text:"Option 2" viewIdResName:"option2" stateDescription:"in list, item 2 of 2" focusable CollectionItemInfo:[rowIndex=1, rowSpan=0, colIndex=0, colSpan=0] actions:[FOCUS, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="listBoxOption"]
+++EditText hint:"Test label" canOpenPopUp clickable editable focusable inputType:1 textSelectionStart:0 textSelectionEnd:0 actions:[FOCUS, CLICK, AX_FOCUS, PASTE, SET_TEXT, IME_ENTER] bundle:[chromeRole="textFieldWithComboBox", clickableScore="300", hint="Test label"]
+++ListView stateDescription:"2 items" clickable CollectionInfo:[rows=2, cols=0] actions:[CLICK, AX_FOCUS] bundle:[chromeRole="listBox", roleDescription="list box"]
+++++View text:"Option 3" viewIdResName:"option3" stateDescription:"in list, item 1 of 2" focusable CollectionItemInfo:[rowIndex=0, rowSpan=0, colIndex=0, colSpan=0] actions:[FOCUS, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="listBoxOption"]
+++++View text:"Option 4" viewIdResName:"option4" stateDescription:"in list, item 2 of 2" focusable CollectionItemInfo:[rowIndex=1, rowSpan=0, colIndex=0, colSpan=0] actions:[FOCUS, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="listBoxOption"]
+++Spinner text:"Combobox" hint:"aria label" canOpenPopUp clickable focusable actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="comboBoxMenuButton", hint="aria label"]
+++ListView viewIdResName:"listbox1" stateDescription:"2 items" clickable CollectionInfo:[rows=2, cols=0] actions:[CLICK, AX_FOCUS] bundle:[chromeRole="listBox", roleDescription="list box"]
+++++View text:"Option 5" viewIdResName:"option5" stateDescription:"in list, item 1 of 2" focusable CollectionItemInfo:[rowIndex=0, rowSpan=0, colIndex=0, colSpan=0] actions:[FOCUS, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="listBoxOption"]
+++++View text:"Option 6" viewIdResName:"option6" stateDescription:"in list, item 2 of 2" focusable CollectionItemInfo:[rowIndex=1, rowSpan=0, colIndex=0, colSpan=0] actions:[FOCUS, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="listBoxOption"]
+++Spinner text:"Combobox with aria-labelledby" hint:"Test label" canOpenPopUp clickable focusable actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="comboBoxMenuButton", hint="Test label"]
+++ListView viewIdResName:"listbox2" stateDescription:"2 items" clickable CollectionInfo:[rows=2, cols=0] actions:[CLICK, AX_FOCUS] bundle:[chromeRole="listBox", roleDescription="list box"]
+++++View text:"Option 7" viewIdResName:"option7" stateDescription:"in list, item 1 of 2" focusable CollectionItemInfo:[rowIndex=0, rowSpan=0, colIndex=0, colSpan=0] actions:[FOCUS, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="listBoxOption"]
+++++View text:"Option 8" viewIdResName:"option8" stateDescription:"in list, item 2 of 2" focusable CollectionItemInfo:[rowIndex=1, rowSpan=0, colIndex=0, colSpan=0] actions:[FOCUS, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="listBoxOption"]
+++Spinner text:"12:05" hint:"aria label" clickable focusable inputType:36 actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="inputTime", clickableScore="300", hint="aria label", roleDescription="time picker"]
+++Spinner text:"12:05" hint:"Test label" clickable focusable inputType:36 actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="inputTime", clickableScore="300", hint="Test label", roleDescription="time picker"]
+++Spinner text:"#E4E4E4" clickable focusable actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="colorWell", clickableScore="300", roleDescription="color picker"]
+++Spinner text:"#E4E4E4" clickable focusable actions:[FOCUS, CLICK, AX_FOCUS, NEXT, PREVIOUS] bundle:[chromeRole="colorWell", clickableScore="300", roleDescription="color picker"]
\ No newline at end of file
diff --git a/content/test/data/accessibility/html/interactive-controls-with-labels-expected-android.txt b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-android.txt
new file mode 100644
index 0000000..0817c6c
--- /dev/null
+++ b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-android.txt
@@ -0,0 +1,28 @@
+android.webkit.WebView focusable focused scrollable
+++android.view.View name='Test label'
+++android.widget.TextView name='aria label'
+++android.widget.TextView name='Test label'
+++android.widget.EditText clickable editable_text has_non_empty_value name='Textbox' hint='aria label' text_change_added_count=7
+++android.widget.EditText clickable editable_text has_non_empty_value name='Textbox with aria-labelledby' hint='Test label' text_change_added_count=28
+++android.widget.Button role_description='button' clickable name='aria label'
+++android.widget.Button role_description='button' clickable name='Test label'
+++android.widget.EditText clickable editable_text focusable hint='aria label' input_type=1
+++android.widget.ListView role_description='list box' clickable collection state_description='2 items' item_count=2 row_count=2
+++++android.view.View collection_item focusable name='Option 1' state_description='in list, item 1 of 2'
+++++android.view.View collection_item focusable name='Option 2' state_description='in list, item 2 of 2' item_index=1 row_index=1
+++android.widget.EditText clickable editable_text focusable hint='Test label' input_type=1
+++android.widget.ListView role_description='list box' clickable collection state_description='2 items' item_count=2 row_count=2
+++++android.view.View collection_item focusable name='Option 3' state_description='in list, item 1 of 2'
+++++android.view.View collection_item focusable name='Option 4' state_description='in list, item 2 of 2' item_index=1 row_index=1
+++android.widget.Spinner clickable focusable name='Combobox' hint='aria label'
+++android.widget.ListView role_description='list box' clickable collection state_description='2 items' item_count=2 row_count=2
+++++android.view.View collection_item focusable name='Option 5' state_description='in list, item 1 of 2'
+++++android.view.View collection_item focusable name='Option 6' state_description='in list, item 2 of 2' item_index=1 row_index=1
+++android.widget.Spinner clickable focusable name='Combobox with aria-labelledby' hint='Test label'
+++android.widget.ListView role_description='list box' clickable collection state_description='2 items' item_count=2 row_count=2
+++++android.view.View collection_item focusable name='Option 7' state_description='in list, item 1 of 2'
+++++android.view.View collection_item focusable name='Option 8' state_description='in list, item 2 of 2' item_index=1 row_index=1
+++android.widget.Spinner role_description='time picker' clickable focusable name='12:05' hint='aria label' input_type=36
+++android.widget.Spinner role_description='time picker' clickable focusable name='12:05' hint='Test label' input_type=36
+++android.widget.Spinner role_description='color picker' clickable focusable name='#E4E4E4'
+++android.widget.Spinner role_description='color picker' clickable focusable name='#E4E4E4'
\ No newline at end of file
diff --git a/content/test/data/accessibility/html/interactive-controls-with-labels-expected-auralinux.txt b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-auralinux.txt
new file mode 100644
index 0000000..2601726d
--- /dev/null
+++ b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-auralinux.txt
@@ -0,0 +1,51 @@
+[document web]
+++[label] label-for
+++++[static] name='Test label'
+++[section] name='aria label'
+++++[static] name='Generic container'
+++[section] name='Test label' labelled-by
+++++[static] name='Generic container with aria-labelledby'
+++[entry] name='aria label' selectable-text
+++++[static] name='Textbox'
+++[entry] name='Test label' selectable-text labelled-by
+++++[static] name='Textbox with aria-labelledby'
+++[push button] name='aria label'
+++[push button] name='Test label' labelled-by
+++[combo box] name='aria label' selectable-text
+++[list box]
+++++[list item] name='Option 1' selectable
+++++[list item] name='Option 2' selectable
+++[combo box] name='Test label' selectable-text labelled-by
+++[list box]
+++++[list item] name='Option 3' selectable
+++++[list item] name='Option 4' selectable
+++[combo box] name='aria label' controller-for
+++++[static] name='Combobox'
+++[list box]
+++++[list item] name='Option 5' selectable
+++++[list item] name='Option 6' selectable
+++[combo box] name='Test label' controller-for labelled-by
+++++[static] name='Combobox with aria-labelledby'
+++[list box] controlled-by
+++++[list item] name='Option 7' selectable
+++++[list item] name='Option 8' selectable
+++[dateeditor] name='aria label'
+++++[section]
+++++++[section]
+++++++++[spin button] name='Hours aria label' current=12.000000 minimum=1.000000 maximum=12.000000
+++++++++[static] name=':'
+++++++++[spin button] name='Minutes aria label' current=5.000000 minimum=0.000000 maximum=59.000000
+++++++++[static] name=' '
+++++++++[spin button] name='AM/PM aria label' current=2.000000 minimum=1.000000 maximum=2.000000
+++++[push button] name='Show time picker'
+++[dateeditor] name='Test label' labelled-by
+++++[section]
+++++++[section]
+++++++++[spin button] name='Hours Test label' labelled-by current=12.000000 minimum=1.000000 maximum=12.000000
+++++++++[static] name=':'
+++++++++[spin button] name='Minutes Test label' labelled-by current=5.000000 minimum=0.000000 maximum=59.000000
+++++++++[static] name=' '
+++++++++[spin button] name='AM/PM Test label' labelled-by current=2.000000 minimum=1.000000 maximum=2.000000
+++++[push button] name='Show time picker'
+++[push button] name='aria label'
+++[push button] name='Test label' labelled-by
\ No newline at end of file
diff --git a/content/test/data/accessibility/html/interactive-controls-with-labels-expected-blink.txt b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-blink.txt
new file mode 100644
index 0000000..7ffdcd2b
--- /dev/null
+++ b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-blink.txt
@@ -0,0 +1,126 @@
+rootWebArea
+++genericContainer ignored
+++++genericContainer ignored
+++++++labelText
+++++++++staticText name='Test label'
+++++++++++inlineTextBox name='Test label'
+++++++genericContainer name='aria label'
+++++++++staticText name='Generic container'
+++++++++++inlineTextBox name='Generic container'
+++++++genericContainer name='Test label'
+++++++++staticText name='Generic container with aria-labelledby'
+++++++++++inlineTextBox name='Generic container with aria-labelledby'
+++++++textField name='aria label' value='Textbox'
+++++++++staticText name='Textbox'
+++++++++++inlineTextBox name='Textbox'
+++++++textField name='Test label' value='Textbox with aria-labelledby'
+++++++++staticText name='Textbox with aria-labelledby'
+++++++++++inlineTextBox name='Textbox with aria-labelledby'
+++++++button name='aria label'
+++++++++staticText name='Button'
+++++++++++inlineTextBox name='Button'
+++++++button name='Test label'
+++++++++staticText name='Button with aria-labelledby'
+++++++++++inlineTextBox name='Button with aria-labelledby'
+++++++textFieldWithComboBox name='aria label' activedescendantId=listBoxOption
+++++++++genericContainer
+++++++listBox
+++++++++listBoxOption name='Option 1' selected=false
+++++++++++none ignored
+++++++++++++staticText name='%E2%80%A2 '
+++++++++++++++inlineTextBox name='%E2%80%A2 '
+++++++++++staticText name='Option 1'
+++++++++++++inlineTextBox name='Option 1'
+++++++++listBoxOption name='Option 2' selected=false
+++++++++++none ignored
+++++++++++++staticText name='%E2%80%A2 '
+++++++++++++++inlineTextBox name='%E2%80%A2 '
+++++++++++staticText name='Option 2'
+++++++++++++inlineTextBox name='Option 2'
+++++++textFieldWithComboBox name='Test label' activedescendantId=listBoxOption
+++++++++genericContainer
+++++++listBox
+++++++++listBoxOption name='Option 3' selected=false
+++++++++++none ignored
+++++++++++++staticText name='%E2%80%A2 '
+++++++++++++++inlineTextBox name='%E2%80%A2 '
+++++++++++staticText name='Option 3'
+++++++++++++inlineTextBox name='Option 3'
+++++++++listBoxOption name='Option 4' selected=false
+++++++++++none ignored
+++++++++++++staticText name='%E2%80%A2 '
+++++++++++++++inlineTextBox name='%E2%80%A2 '
+++++++++++staticText name='Option 4'
+++++++++++++inlineTextBox name='Option 4'
+++++++comboBoxMenuButton name='aria label' value='Combobox' controlsIds=listBox
+++++++++staticText name='Combobox'
+++++++++++inlineTextBox name='Combobox'
+++++++listBox
+++++++++listBoxOption name='Option 5' selected=false
+++++++++++none ignored
+++++++++++++staticText name='%E2%80%A2 '
+++++++++++++++inlineTextBox name='%E2%80%A2 '
+++++++++++staticText name='Option 5'
+++++++++++++inlineTextBox name='Option 5'
+++++++++listBoxOption name='Option 6' selected=false
+++++++++++none ignored
+++++++++++++staticText name='%E2%80%A2 '
+++++++++++++++inlineTextBox name='%E2%80%A2 '
+++++++++++staticText name='Option 6'
+++++++++++++inlineTextBox name='Option 6'
+++++++comboBoxMenuButton name='Test label' value='Combobox with aria-labelledby' controlsIds=listBox
+++++++++staticText name='Combobox with aria-labelledby'
+++++++++++inlineTextBox name='Combobox with aria-labelledby'
+++++++listBox
+++++++++listBoxOption name='Option 7' selected=false
+++++++++++none ignored
+++++++++++++staticText name='%E2%80%A2 '
+++++++++++++++inlineTextBox name='%E2%80%A2 '
+++++++++++staticText name='Option 7'
+++++++++++++inlineTextBox name='Option 7'
+++++++++listBoxOption name='Option 8' selected=false
+++++++++++none ignored
+++++++++++++staticText name='%E2%80%A2 '
+++++++++++++++inlineTextBox name='%E2%80%A2 '
+++++++++++staticText name='Option 8'
+++++++++++++inlineTextBox name='Option 8'
+++++++inputTime name='aria label' value='12:05'
+++++++++genericContainer
+++++++++++genericContainer
+++++++++++++spinButton name='Hours aria label' placeholder='--' value='12' valueForRange=12.00 minValueForRange=1.00 maxValueForRange=12.00
+++++++++++++++staticText name='12'
+++++++++++++++++inlineTextBox name='12'
+++++++++++++staticText name=':'
+++++++++++++++inlineTextBox name=':'
+++++++++++++spinButton name='Minutes aria label' placeholder='--' value='05' valueForRange=5.00 minValueForRange=0.00 maxValueForRange=59.00
+++++++++++++++staticText name='05'
+++++++++++++++++inlineTextBox name='05'
+++++++++++++staticText name=' '
+++++++++++++++inlineTextBox name=' '
+++++++++++++spinButton name='AM/PM aria label' placeholder='--' value='PM' valueForRange=2.00 minValueForRange=1.00 maxValueForRange=2.00
+++++++++++++++staticText name='PM'
+++++++++++++++++inlineTextBox name='PM'
+++++++++popUpButton name='Show time picker'
+++++++inputTime name='Test label' value='12:05'
+++++++++genericContainer
+++++++++++genericContainer
+++++++++++++spinButton name='Hours Test label' placeholder='--' value='12' valueForRange=12.00 minValueForRange=1.00 maxValueForRange=12.00
+++++++++++++++staticText name='12'
+++++++++++++++++inlineTextBox name='12'
+++++++++++++staticText name=':'
+++++++++++++++inlineTextBox name=':'
+++++++++++++spinButton name='Minutes Test label' placeholder='--' value='05' valueForRange=5.00 minValueForRange=0.00 maxValueForRange=59.00
+++++++++++++++staticText name='05'
+++++++++++++++++inlineTextBox name='05'
+++++++++++++staticText name=' '
+++++++++++++++inlineTextBox name=' '
+++++++++++++spinButton name='AM/PM Test label' placeholder='--' value='PM' valueForRange=2.00 minValueForRange=1.00 maxValueForRange=2.00
+++++++++++++++staticText name='PM'
+++++++++++++++++inlineTextBox name='PM'
+++++++++popUpButton name='Show time picker'
+++++++colorWell name='aria label' value='#e4e4e4'
+++++++++genericContainer ignored
+++++++++++genericContainer ignored
+++++++colorWell name='Test label' value='#e4e4e4'
+++++++++genericContainer ignored
+++++++++++genericContainer ignored
\ No newline at end of file
diff --git a/content/test/data/accessibility/html/interactive-controls-with-labels-expected-mac.txt b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-mac.txt
new file mode 100644
index 0000000..36d070e
--- /dev/null
+++ b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-mac.txt
@@ -0,0 +1,51 @@
+AXWebArea
+++AXGroup
+++++AXStaticText AXValue='Test label'
+++AXGroup AXDescription='aria label'
+++++AXStaticText AXValue='Generic container'
+++AXGroup AXDescription='Test label'
+++++AXStaticText AXValue='Generic container with aria-labelledby'
+++AXTextField AXDescription='aria label' AXValue='Textbox'
+++++AXStaticText AXValue='Textbox'
+++AXTextField AXTitle='Test label' AXValue='Textbox with aria-labelledby'
+++++AXStaticText AXValue='Textbox with aria-labelledby'
+++AXButton AXDescription='aria label'
+++AXButton AXTitle='Test label'
+++AXComboBox AXDescription='aria label'
+++AXList
+++++AXStaticText AXValue='Option 1'
+++++AXStaticText AXValue='Option 2'
+++AXComboBox AXTitle='Test label'
+++AXList
+++++AXStaticText AXValue='Option 3'
+++++AXStaticText AXValue='Option 4'
+++AXComboBox AXDescription='aria label' AXValue='Combobox'
+++++AXStaticText AXValue='Combobox'
+++AXList
+++++AXStaticText AXValue='Option 5'
+++++AXStaticText AXValue='Option 6'
+++AXComboBox AXTitle='Test label' AXValue='Combobox with aria-labelledby'
+++++AXStaticText AXValue='Combobox with aria-labelledby'
+++AXList
+++++AXStaticText AXValue='Option 7'
+++++AXStaticText AXValue='Option 8'
+++AXTimeField AXDescription='aria label' AXValue='12:05'
+++++AXGroup
+++++++AXGroup
+++++++++AXIncrementor AXDescription='Hours aria label' AXValue=12
+++++++++AXStaticText AXValue=':'
+++++++++AXIncrementor AXDescription='Minutes aria label' AXValue=5
+++++++++AXStaticText AXValue=' '
+++++++++AXIncrementor AXDescription='AM/PM aria label' AXValue=2
+++++AXPopUpButton AXDescription='Show time picker'
+++AXTimeField AXTitle='Test label' AXValue='12:05'
+++++AXGroup
+++++++AXGroup
+++++++++AXIncrementor AXTitle='Hours Test label' AXValue=12
+++++++++AXStaticText AXValue=':'
+++++++++AXIncrementor AXTitle='Minutes Test label' AXValue=5
+++++++++AXStaticText AXValue=' '
+++++++++AXIncrementor AXTitle='AM/PM Test label' AXValue=2
+++++AXPopUpButton AXDescription='Show time picker'
+++AXColorWell AXDescription='aria label' AXValue='rgb 0.89412 0.89412 0.89412 1'
+++AXColorWell AXTitle='Test label' AXValue='rgb 0.89412 0.89412 0.89412 1'
\ No newline at end of file
diff --git a/content/test/data/accessibility/html/interactive-controls-with-labels-expected-uia-win.txt b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-uia-win.txt
new file mode 100644
index 0000000..e32d805
--- /dev/null
+++ b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-uia-win.txt
@@ -0,0 +1,51 @@
+Document
+++Text
+++++Text Name='Test label'
+++Group Name='aria label'
+++++Text Name='Generic container'
+++Group Name='Test label'
+++++Text Name='Generic container with aria-labelledby'
+++Edit Name='aria label' Value.Value='Textbox'
+++++Text Name='Textbox'
+++Edit Name='Test label' Value.Value='Textbox with aria-labelledby'
+++++Text Name='Textbox with aria-labelledby'
+++Button Name='aria label'
+++Button Name='Test label'
+++ComboBox Name='aria label' ExpandCollapse.ExpandCollapseState='LeafNode'
+++List Selection.CanSelectMultiple=false Selection.IsSelectionRequired=false
+++++ListItem Name='Option 1' SelectionItem.IsSelected=false
+++++ListItem Name='Option 2' SelectionItem.IsSelected=false
+++ComboBox Name='Test label' ExpandCollapse.ExpandCollapseState='LeafNode'
+++List Selection.CanSelectMultiple=false Selection.IsSelectionRequired=false
+++++ListItem Name='Option 3' SelectionItem.IsSelected=false
+++++ListItem Name='Option 4' SelectionItem.IsSelected=false
+++ComboBox Name='aria label' ExpandCollapse.ExpandCollapseState='LeafNode' Selection.CanSelectMultiple=false Selection.IsSelectionRequired=false Value.Value='Combobox'
+++++Text Name='Combobox'
+++List Selection.CanSelectMultiple=false Selection.IsSelectionRequired=false
+++++ListItem Name='Option 5' SelectionItem.IsSelected=false
+++++ListItem Name='Option 6' SelectionItem.IsSelected=false
+++ComboBox Name='Test label' ExpandCollapse.ExpandCollapseState='LeafNode' Selection.CanSelectMultiple=false Selection.IsSelectionRequired=false Value.Value='Combobox with aria-labelledby'
+++++Text Name='Combobox with aria-labelledby'
+++List Selection.CanSelectMultiple=false Selection.IsSelectionRequired=false
+++++ListItem Name='Option 7' SelectionItem.IsSelected=false
+++++ListItem Name='Option 8' SelectionItem.IsSelected=false
+++Group Name='aria label' Value.Value='12:05'
+++++Group IsControlElement=false
+++++++Group IsControlElement=false
+++++++++Spinner Name='Hours aria label' RangeValue.IsReadOnly=false RangeValue.LargeChange=0.00 RangeValue.SmallChange=0.00 RangeValue.Maximum=12.00 RangeValue.Minimum=1.00 RangeValue.Value=12.00 Value.Value='12'
+++++++++Text Name=':'
+++++++++Spinner Name='Minutes aria label' RangeValue.IsReadOnly=false RangeValue.LargeChange=0.00 RangeValue.SmallChange=0.00 RangeValue.Maximum=59.00 RangeValue.Minimum=0.00 RangeValue.Value=5.00 Value.Value='05'
+++++++++Text Name=' '
+++++++++Spinner Name='AM/PM aria label' RangeValue.IsReadOnly=false RangeValue.LargeChange=0.00 RangeValue.SmallChange=0.00 RangeValue.Maximum=2.00 RangeValue.Minimum=1.00 RangeValue.Value=2.00 Value.Value='PM'
+++++Button Name='Show time picker' ExpandCollapse.ExpandCollapseState='Collapsed'
+++Group Name='Test label' Value.Value='12:05'
+++++Group IsControlElement=false
+++++++Group IsControlElement=false
+++++++++Spinner Name='Hours Test label' RangeValue.IsReadOnly=false RangeValue.LargeChange=0.00 RangeValue.SmallChange=0.00 RangeValue.Maximum=12.00 RangeValue.Minimum=1.00 RangeValue.Value=12.00 Value.Value='12'
+++++++++Text Name=':'
+++++++++Spinner Name='Minutes Test label' RangeValue.IsReadOnly=false RangeValue.LargeChange=0.00 RangeValue.SmallChange=0.00 RangeValue.Maximum=59.00 RangeValue.Minimum=0.00 RangeValue.Value=5.00 Value.Value='05'
+++++++++Text Name=' '
+++++++++Spinner Name='AM/PM Test label' RangeValue.IsReadOnly=false RangeValue.LargeChange=0.00 RangeValue.SmallChange=0.00 RangeValue.Maximum=2.00 RangeValue.Minimum=1.00 RangeValue.Value=2.00 Value.Value='PM'
+++++Button Name='Show time picker' ExpandCollapse.ExpandCollapseState='Collapsed'
+++Button Name='aria label' Value.Value='89% red 89% green 89% blue'
+++Button Name='Test label' Value.Value='89% red 89% green 89% blue'
\ No newline at end of file
diff --git a/content/test/data/accessibility/html/interactive-controls-with-labels-expected-win.txt b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-win.txt
new file mode 100644
index 0000000..30e6708
--- /dev/null
+++ b/content/test/data/accessibility/html/interactive-controls-with-labels-expected-win.txt
@@ -0,0 +1,51 @@
+ROLE_SYSTEM_DOCUMENT READONLY FOCUSABLE
+++IA2_ROLE_LABEL
+++++ROLE_SYSTEM_STATICTEXT name='Test label'
+++IA2_ROLE_SECTION name='aria label'
+++++ROLE_SYSTEM_STATICTEXT name='Generic container'
+++IA2_ROLE_SECTION name='Test label'
+++++ROLE_SYSTEM_STATICTEXT name='Generic container with aria-labelledby'
+++ROLE_SYSTEM_TEXT name='aria label' value='Textbox'
+++++ROLE_SYSTEM_STATICTEXT name='Textbox'
+++ROLE_SYSTEM_TEXT name='Test label' value='Textbox with aria-labelledby'
+++++ROLE_SYSTEM_STATICTEXT name='Textbox with aria-labelledby'
+++ROLE_SYSTEM_PUSHBUTTON name='aria label'
+++ROLE_SYSTEM_PUSHBUTTON name='Test label'
+++ROLE_SYSTEM_COMBOBOX name='aria label' FOCUSABLE HASPOPUP
+++ROLE_SYSTEM_LIST
+++++ROLE_SYSTEM_LISTITEM name='Option 1' FOCUSABLE
+++++ROLE_SYSTEM_LISTITEM name='Option 2' FOCUSABLE
+++ROLE_SYSTEM_COMBOBOX name='Test label' FOCUSABLE HASPOPUP
+++ROLE_SYSTEM_LIST
+++++ROLE_SYSTEM_LISTITEM name='Option 3' FOCUSABLE
+++++ROLE_SYSTEM_LISTITEM name='Option 4' FOCUSABLE
+++ROLE_SYSTEM_COMBOBOX name='aria label' value='Combobox' FOCUSABLE HASPOPUP
+++++ROLE_SYSTEM_STATICTEXT name='Combobox'
+++ROLE_SYSTEM_LIST
+++++ROLE_SYSTEM_LISTITEM name='Option 5' FOCUSABLE
+++++ROLE_SYSTEM_LISTITEM name='Option 6' FOCUSABLE
+++ROLE_SYSTEM_COMBOBOX name='Test label' value='Combobox with aria-labelledby' FOCUSABLE HASPOPUP
+++++ROLE_SYSTEM_STATICTEXT name='Combobox with aria-labelledby'
+++ROLE_SYSTEM_LIST
+++++ROLE_SYSTEM_LISTITEM name='Option 7' FOCUSABLE
+++++ROLE_SYSTEM_LISTITEM name='Option 8' FOCUSABLE
+++ROLE_SYSTEM_GROUPING name='aria label' value='12:05' FOCUSABLE
+++++IA2_ROLE_SECTION
+++++++IA2_ROLE_SECTION
+++++++++ROLE_SYSTEM_SPINBUTTON name='Hours aria label' value='12' FOCUSABLE
+++++++++ROLE_SYSTEM_STATICTEXT name=':'
+++++++++ROLE_SYSTEM_SPINBUTTON name='Minutes aria label' value='05' FOCUSABLE
+++++++++ROLE_SYSTEM_STATICTEXT name=' '
+++++++++ROLE_SYSTEM_SPINBUTTON name='AM/PM aria label' value='PM' FOCUSABLE
+++++ROLE_SYSTEM_BUTTONMENU name='Show time picker' FOCUSABLE HASPOPUP
+++ROLE_SYSTEM_GROUPING name='Test label' value='12:05' FOCUSABLE
+++++IA2_ROLE_SECTION
+++++++IA2_ROLE_SECTION
+++++++++ROLE_SYSTEM_SPINBUTTON name='Hours Test label' value='12' FOCUSABLE
+++++++++ROLE_SYSTEM_STATICTEXT name=':'
+++++++++ROLE_SYSTEM_SPINBUTTON name='Minutes Test label' value='05' FOCUSABLE
+++++++++ROLE_SYSTEM_STATICTEXT name=' '
+++++++++ROLE_SYSTEM_SPINBUTTON name='AM/PM Test label' value='PM' FOCUSABLE
+++++ROLE_SYSTEM_BUTTONMENU name='Show time picker' FOCUSABLE HASPOPUP
+++IA2_ROLE_COLOR_CHOOSER name='aria label' value='89% red 89% green 89% blue' FOCUSABLE
+++IA2_ROLE_COLOR_CHOOSER name='Test label' value='89% red 89% green 89% blue' FOCUSABLE
\ No newline at end of file
diff --git a/content/test/data/accessibility/html/interactive-controls-with-labels.html b/content/test/data/accessibility/html/interactive-controls-with-labels.html
new file mode 100644
index 0000000..b758c3b
--- /dev/null
+++ b/content/test/data/accessibility/html/interactive-controls-with-labels.html
@@ -0,0 +1,42 @@
+<html>
+<body>
+  <label id="label1">Test label</label>
+  <div aria-label="aria label">Generic container</div>
+  <div aria-labelledby="label1">Generic container with aria-labelledby</div>
+
+  <div role="textbox" aria-label="aria label">Textbox</div>
+  <div role="textbox" aria-labelledby="label1">Textbox with aria-labelledby</div>
+
+  <div role="button" aria-label="aria label">Button</div>
+  <div role="button" aria-labelledby="label1">Button with aria-labelledby</div>
+
+  <input type="text" role="combobox" aria-activedescendant="option1" aria-label="aria label">
+  <ul role="listbox">
+    <li id="option1" role="option">Option 1</li>
+    <li id="option2" role="option">Option 2</li>
+  </ul>
+  <input type="text" role="combobox" aria-activedescendant="option3" aria-labelledby="label1">
+  <ul role="listbox">
+    <li id="option3" role="option">Option 3</li>
+    <li id="option4" role="option">Option 4</li>
+  </ul>
+
+  <div role="combobox" tabindex="0" aria-controls="listbox2" aria-label="aria label">Combobox</div>
+  <ul id="listbox1" role="listbox">
+    <li id="option5" role="option">Option 5</li>
+    <li id="option6" role="option">Option 6</li>
+  </ul>
+
+  <div role="combobox" tabindex="0" aria-controls="listbox2" aria-labelledby="label1">Combobox with aria-labelledby</div>
+  <ul id="listbox2" role="listbox">
+    <li id="option7" role="option">Option 7</li>
+    <li id="option8" role="option">Option 8</li>
+  </ul>
+
+  <input type="time" value="12:05" aria-label="aria label">
+  <input type="time" value="12:05" aria-labelledby="label1">
+
+  <input type="color" value="#e4e4e4" aria-label="aria label">
+  <input type="color" value="#e4e4e4" aria-labelledby="label1">
+</body>
+</html>
diff --git a/content/test/data/attribution_reporting/register_impression.js b/content/test/data/attribution_reporting/register_impression.js
index d9526084..8f7bcbb0 100644
--- a/content/test/data/attribution_reporting/register_impression.js
+++ b/content/test/data/attribution_reporting/register_impression.js
@@ -71,7 +71,7 @@
   return anchor;
 }
 
-function createAttributionSourceImg(src) {
+function createAttributionSrcImg(src) {
   const img = document.createElement('img');
   img.setAttribute('target', "top");
   img.width = 100;
diff --git a/content/test/data/attribution_reporting/register_source_trigger_redirect_chain.html b/content/test/data/attribution_reporting/register_source_trigger_redirect_chain.html
new file mode 100644
index 0000000..4225c14
--- /dev/null
+++ b/content/test/data/attribution_reporting/register_source_trigger_redirect_chain.html
@@ -0,0 +1 @@
+Redirect chain which registers source -> trigger
diff --git a/content/test/data/attribution_reporting/register_source_trigger_redirect_chain.html.mock-http-headers b/content/test/data/attribution_reporting/register_source_trigger_redirect_chain.html.mock-http-headers
new file mode 100644
index 0000000..318247b3
--- /dev/null
+++ b/content/test/data/attribution_reporting/register_source_trigger_redirect_chain.html.mock-http-headers
@@ -0,0 +1,3 @@
+HTTP/1.1 301 Yo
+Attribution-Reporting-Register-Source:{"source_event_id":"5","destination":"https://advertiser.example"}
+Location: /register_trigger_headers.html
\ No newline at end of file
diff --git a/content/test/data/attribution_reporting/register_trigger_headers.html b/content/test/data/attribution_reporting/register_trigger_headers.html
new file mode 100644
index 0000000..d40c54c6
--- /dev/null
+++ b/content/test/data/attribution_reporting/register_trigger_headers.html
@@ -0,0 +1 @@
+Registers a trigger with headers
diff --git a/content/test/data/attribution_reporting/register_trigger_headers.html.mock-http-headers b/content/test/data/attribution_reporting/register_trigger_headers.html.mock-http-headers
new file mode 100644
index 0000000..a1ddf2c4
--- /dev/null
+++ b/content/test/data/attribution_reporting/register_trigger_headers.html.mock-http-headers
@@ -0,0 +1,2 @@
+HTTP/1.1 200 OK
+Attribution-Reporting-Register-Event-Trigger:[{"trigger_data": "10"}]
\ No newline at end of file
diff --git a/content/test/data/attribution_reporting/register_trigger_headers_all_params.html b/content/test/data/attribution_reporting/register_trigger_headers_all_params.html
new file mode 100644
index 0000000..9555c15
--- /dev/null
+++ b/content/test/data/attribution_reporting/register_trigger_headers_all_params.html
@@ -0,0 +1 @@
+Registers a trigger with headers using all parameters and multiple event trigger datas
diff --git a/content/test/data/attribution_reporting/register_trigger_headers_all_params.html.mock-http-headers b/content/test/data/attribution_reporting/register_trigger_headers_all_params.html.mock-http-headers
new file mode 100644
index 0000000..d4aea3e1
--- /dev/null
+++ b/content/test/data/attribution_reporting/register_trigger_headers_all_params.html.mock-http-headers
@@ -0,0 +1,2 @@
+HTTP/1.1 200 OK
+Attribution-Reporting-Register-Event-Trigger:[{"trigger_data": "1","priority":"5","deduplication_key":"1024"},{"trigger_data":"2","priority":"10"}]
\ No newline at end of file
diff --git a/content/test/data/attribution_reporting/register_trigger_headers_then_redirect_invalid.html b/content/test/data/attribution_reporting/register_trigger_headers_then_redirect_invalid.html
new file mode 100644
index 0000000..60e8a1d1
--- /dev/null
+++ b/content/test/data/attribution_reporting/register_trigger_headers_then_redirect_invalid.html
@@ -0,0 +1 @@
+Registers a trigger with a bad header value
diff --git a/content/test/data/attribution_reporting/register_trigger_headers_then_redirect_invalid.html.mock-http-headers b/content/test/data/attribution_reporting/register_trigger_headers_then_redirect_invalid.html.mock-http-headers
new file mode 100644
index 0000000..09ca1ff
--- /dev/null
+++ b/content/test/data/attribution_reporting/register_trigger_headers_then_redirect_invalid.html.mock-http-headers
@@ -0,0 +1,3 @@
+HTTP/1.1 301 Yo
+Attribution-Reporting-Register-Event-Trigger:{[]}
+Location: /register_trigger_headers.html
\ No newline at end of file
diff --git a/content/test/data/attribution_reporting/register_trigger_source_trigger.html b/content/test/data/attribution_reporting/register_trigger_source_trigger.html
new file mode 100644
index 0000000..419e3319
--- /dev/null
+++ b/content/test/data/attribution_reporting/register_trigger_source_trigger.html
@@ -0,0 +1 @@
+Redirect chain which registers trigger -> source -> trigger
diff --git a/content/test/data/attribution_reporting/register_trigger_source_trigger.html.mock-http-headers b/content/test/data/attribution_reporting/register_trigger_source_trigger.html.mock-http-headers
new file mode 100644
index 0000000..65f4c0ea
--- /dev/null
+++ b/content/test/data/attribution_reporting/register_trigger_source_trigger.html.mock-http-headers
@@ -0,0 +1,3 @@
+HTTP/1.1 301 Yo
+Attribution-Reporting-Register-Event-Trigger:[{"trigger_data": "5"}]
+Location: /register_source_trigger_redirect_chain.html
\ No newline at end of file
diff --git a/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
index b5c5a5d..683d4c3 100644
--- a/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
@@ -408,14 +408,6 @@
 crbug.com/angleproject/6430 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] deqp/functional/gles3/texturefiltering/cube_sizes_04.html [ Failure ]
 crbug.com/angleproject/6430 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] deqp/functional/gles3/texturespecification/basic_copyteximage2d.html [ Failure ]
 crbug.com/angleproject/6430 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] deqp/functional/gles3/texturespecification/basic_copytexsubimage2d.html [ Failure ]
-# Post Python 3 conversion: crbug.com/1266604
-crbug.com/1271941 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] conformance/textures/canvas_sub_rectangle/tex-2d-rgba-rgba-unsigned_short_5_5_5_1.html [ RetryOnFailure ]
-crbug.com/1271941 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] deqp/functional/gles3/clipping.html [ Failure ]
-crbug.com/1271941 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] deqp/functional/gles3/fborender/shared_colorbuffer_00.html [ Failure ]
-crbug.com/1271941 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] deqp/functional/gles3/fborender/shared_colorbuffer_01.html [ Failure ]
-crbug.com/1271941 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] deqp/functional/gles3/fborender/shared_colorbuffer_02.html [ Failure ]
-crbug.com/1271941 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] deqp/functional/gles3/fborender/shared_colorbuffer_clear.html [ Failure ]
-crbug.com/1271941 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] deqp/functional/gles3/fragdepth.html [ Failure ]
 crbug.com/1298619 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] deqp/functional/gles3/occlusionquery_strict.html [ Failure ]
 
 ######################################################################
@@ -790,6 +782,10 @@
 crbug.com/1175229 [ android android-pixel-4 angle-opengles passthrough ] conformance2/textures/webgl_canvas/tex-2d-rgb565-rgb-unsigned_short_5_6_5.html [ Failure ]
 crbug.com/1175232 [ android android-pixel-4 angle-opengles passthrough ] conformance2/reading/read-pixels-from-fbo-test.html [ Failure ]
 
+crbug.com/1302143 [ android android-pixel-4 no-passthrough ] deqp/functional/gles3/transformfeedback/array_interleaved_lines.html [ RetryOnFailure ]
+crbug.com/1302143 [ android android-pixel-4 no-passthrough ] deqp/functional/gles3/transformfeedback/array_interleaved_points.html [ RetryOnFailure ]
+crbug.com/1302143 [ android android-pixel-4 no-passthrough ] deqp/functional/gles3/transformfeedback/basic_types_interleaved_triangles.html [ RetryOnFailure ]
+
 crbug.com/1191030 [ android android-pixel-4 ] conformance/textures/misc/video-rotation.html [ Failure ]
 
 crbug.com/1239079 [ android no-passthrough ] conformance2/transform_feedback/too-small-buffers.html [ Failure ]
diff --git a/extensions/browser/extension_api_frame_id_map.cc b/extensions/browser/extension_api_frame_id_map.cc
index 49f8b0b0..b28fa94 100644
--- a/extensions/browser/extension_api_frame_id_map.cc
+++ b/extensions/browser/extension_api_frame_id_map.cc
@@ -138,31 +138,6 @@
   return rfh;
 }
 
-content::RenderFrameHost*
-ExtensionApiFrameIdMap::GetRenderFrameHostByDocumentId(
-    const DocumentId& document_id) {
-  auto iter = document_id_map_.find(document_id);
-  if (iter == document_id_map_.end())
-    return nullptr;
-  return &iter->second->render_frame_host();
-}
-
-ExtensionApiFrameIdMap::DocumentId ExtensionApiFrameIdMap::DocumentIdFromString(
-    const std::string& document_id) {
-  if (document_id.length() != 32)
-    return DocumentId();
-
-  base::StringPiece string_piece(document_id);
-  uint64_t high = 0;
-  uint64_t low = 0;
-  if (!base::HexStringToUInt64(string_piece.substr(0, 16), &high) ||
-      !base::HexStringToUInt64(string_piece.substr(16, 16), &low)) {
-    return DocumentId();
-  }
-
-  return base::UnguessableToken::Deserialize(high, low);
-}
-
 ExtensionApiFrameIdMap::FrameData ExtensionApiFrameIdMap::KeyToValue(
     content::GlobalRenderFrameHostId key,
     bool require_live_frame) const {
@@ -312,14 +287,10 @@
 ExtensionApiFrameIdMap::ExtensionDocumentUserData::ExtensionDocumentUserData(
     content::RenderFrameHost* render_frame_host)
     : content::DocumentUserData<ExtensionDocumentUserData>(render_frame_host),
-      document_id_(DocumentId::Create()) {
-  Get()->document_id_map_[document_id_] = this;
-}
+      document_id_(DocumentId::Create()) {}
 
 ExtensionApiFrameIdMap::ExtensionDocumentUserData::
-    ~ExtensionDocumentUserData() {
-  Get()->document_id_map_.erase(document_id_);
-}
+    ~ExtensionDocumentUserData() = default;
 
 DOCUMENT_USER_DATA_KEY_IMPL(ExtensionApiFrameIdMap::ExtensionDocumentUserData);
 
diff --git a/extensions/browser/extension_api_frame_id_map.h b/extensions/browser/extension_api_frame_id_map.h
index 5c976bee..842eab5a 100644
--- a/extensions/browser/extension_api_frame_id_map.h
+++ b/extensions/browser/extension_api_frame_id_map.h
@@ -145,14 +145,6 @@
       content::WebContents* web_contents,
       int frame_id);
 
-  // Find the current RenderFrameHost for a given extension documentID.
-  // Returns nullptr if not found.
-  content::RenderFrameHost* GetRenderFrameHostByDocumentId(
-      const DocumentId& document_id);
-
-  // Parses a serialized document id string to a DocumentId.
-  static DocumentId DocumentIdFromString(const std::string& document_id);
-
   // Retrieves the FrameData for a given RenderFrameHost id.
   [[nodiscard]] FrameData GetFrameData(content::GlobalRenderFrameHostId rfh_id);
 
@@ -196,10 +188,6 @@
   // continue after a frame is unloaded can access the FrameData.
   using FrameDataMap = std::map<content::GlobalRenderFrameHostId, FrameData>;
   FrameDataMap deleted_frame_data_map_;
-
-  // Holds mapping of DocumentIds to ExtensionDocumentUserData objects.
-  using DocumentIdMap = std::map<DocumentId, ExtensionDocumentUserData*>;
-  DocumentIdMap document_id_map_;
 };
 
 }  // namespace extensions
diff --git a/infra/config/generated/builders/ci/android-backuprefptr-arm-fyi-rel/properties.json b/infra/config/generated/builders/ci/android-backuprefptr-arm-fyi-rel/properties.json
index fc3bce3..b232002 100644
--- a/infra/config/generated/builders/ci/android-backuprefptr-arm-fyi-rel/properties.json
+++ b/infra/config/generated/builders/ci/android-backuprefptr-arm-fyi-rel/properties.json
@@ -1,9 +1,8 @@
 {
-  "$build/goma": {
-    "enable_ats": true,
-    "rpc_extra_params": "?prod",
-    "server_host": "goma.chromium.org",
-    "use_luci_auth": true
+  "$build/reclient": {
+    "instance": "rbe-chromium-trusted",
+    "jobs": 250,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git a/infra/config/generated/builders/ci/android-backuprefptr-arm64-fyi-rel/properties.json b/infra/config/generated/builders/ci/android-backuprefptr-arm64-fyi-rel/properties.json
index fc3bce3..b232002 100644
--- a/infra/config/generated/builders/ci/android-backuprefptr-arm64-fyi-rel/properties.json
+++ b/infra/config/generated/builders/ci/android-backuprefptr-arm64-fyi-rel/properties.json
@@ -1,9 +1,8 @@
 {
-  "$build/goma": {
-    "enable_ats": true,
-    "rpc_extra_params": "?prod",
-    "server_host": "goma.chromium.org",
-    "use_luci_auth": true
+  "$build/reclient": {
+    "instance": "rbe-chromium-trusted",
+    "jobs": 250,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git a/infra/config/generated/luci/luci-milo.cfg b/infra/config/generated/luci/luci-milo.cfg
index b6d28eb0..c82f3ee 100644
--- a/infra/config/generated/luci/luci-milo.cfg
+++ b/infra/config/generated/luci/luci-milo.cfg
@@ -3653,31 +3653,6 @@
     short_name: "x86"
   }
   builders {
-    name: "buildbucket/luci.chromium.ci/android-pie-arm64-wpt-rel-non-cq"
-    category: "builder_tester|arm64"
-    short_name: "P-WPT"
-  }
-  builders {
-    name: "buildbucket/luci.chromium.ci/android-chrome-pie-x86-wpt-fyi-rel"
-    category: "builder_tester|web-platform"
-    short_name: "P"
-  }
-  builders {
-    name: "buildbucket/luci.chromium.ci/android-weblayer-pie-x86-wpt-fyi-rel"
-    category: "builder_tester|weblayer"
-    short_name: "P"
-  }
-  builders {
-    name: "buildbucket/luci.chromium.ci/android-weblayer-pie-x86-wpt-smoketest"
-    category: "builder_tester|weblayer"
-    short_name: "P"
-  }
-  builders {
-    name: "buildbucket/luci.chromium.ci/android-webview-pie-x86-wpt-fyi-rel"
-    category: "builder_tester|webview"
-    short_name: "P"
-  }
-  builders {
     name: "buildbucket/luci.chromium.ci/android-cronet-x86-dbg-kitkat-tests"
     category: "cronet|test"
     short_name: "k"
@@ -3732,6 +3707,31 @@
     category: "tester|webview"
     short_name: "12"
   }
+  builders {
+    name: "buildbucket/luci.chromium.ci/android-chrome-pie-x86-wpt-fyi-rel"
+    category: "wpt|chrome"
+    short_name: "p-x86"
+  }
+  builders {
+    name: "buildbucket/luci.chromium.ci/android-weblayer-pie-x86-wpt-fyi-rel"
+    category: "wpt|weblayer"
+    short_name: "p-x86"
+  }
+  builders {
+    name: "buildbucket/luci.chromium.ci/android-weblayer-pie-x86-wpt-smoketest"
+    category: "wpt|weblayer"
+    short_name: "p-x86"
+  }
+  builders {
+    name: "buildbucket/luci.chromium.ci/android-pie-arm64-wpt-rel-non-cq"
+    category: "wpt|webview"
+    short_name: "p-arm64"
+  }
+  builders {
+    name: "buildbucket/luci.chromium.ci/android-webview-pie-x86-wpt-fyi-rel"
+    category: "wpt|webview"
+    short_name: "p-x86"
+  }
   header {
     oncalls {
       name: "Chromium"
diff --git a/infra/config/subprojects/chromium/ci/chromium.android.fyi.star b/infra/config/subprojects/chromium/ci/chromium.android.fyi.star
index 1a6ad001..b37152b0 100644
--- a/infra/config/subprojects/chromium/ci/chromium.android.fyi.star
+++ b/infra/config/subprojects/chromium/ci/chromium.android.fyi.star
@@ -42,8 +42,8 @@
 ci.builder(
     name = "android-pie-arm64-wpt-rel-non-cq",
     console_view_entry = consoles.console_view_entry(
-        category = "builder_tester|arm64",
-        short_name = "P-WPT",
+        category = "wpt|webview",
+        short_name = "p-arm64",
     ),
     goma_backend = None,
     reclient_jobs = rbe_jobs.DEFAULT,
@@ -53,8 +53,8 @@
 ci.builder(
     name = "android-chrome-pie-x86-wpt-fyi-rel",
     console_view_entry = consoles.console_view_entry(
-        category = "builder_tester|web-platform",
-        short_name = "P",
+        category = "wpt|chrome",
+        short_name = "p-x86",
     ),
     goma_backend = None,
     reclient_jobs = rbe_jobs.DEFAULT,
@@ -74,8 +74,8 @@
 ci.builder(
     name = "android-weblayer-pie-x86-wpt-fyi-rel",
     console_view_entry = consoles.console_view_entry(
-        category = "builder_tester|weblayer",
-        short_name = "P",
+        category = "wpt|weblayer",
+        short_name = "p-x86",
     ),
     goma_backend = None,
     reclient_jobs = rbe_jobs.DEFAULT,
@@ -85,8 +85,8 @@
 ci.builder(
     name = "android-weblayer-pie-x86-wpt-smoketest",
     console_view_entry = consoles.console_view_entry(
-        category = "builder_tester|weblayer",
-        short_name = "P",
+        category = "wpt|weblayer",
+        short_name = "p-x86",
     ),
     goma_backend = None,
     reclient_jobs = rbe_jobs.DEFAULT,
@@ -96,8 +96,8 @@
 ci.builder(
     name = "android-webview-pie-x86-wpt-fyi-rel",
     console_view_entry = consoles.console_view_entry(
-        category = "builder_tester|webview",
-        short_name = "P",
+        category = "wpt|webview",
+        short_name = "p-x86",
     ),
     goma_backend = None,
     reclient_jobs = rbe_jobs.DEFAULT,
diff --git a/infra/config/subprojects/chromium/ci/chromium.fyi.star b/infra/config/subprojects/chromium/ci/chromium.fyi.star
index 9a03670..51ce50c 100644
--- a/infra/config/subprojects/chromium/ci/chromium.fyi.star
+++ b/infra/config/subprojects/chromium/ci/chromium.fyi.star
@@ -127,6 +127,9 @@
     ),
     notifies = ["chrome-memory-safety"],
     os = os.LINUX_BIONIC_SWITCH_TO_DEFAULT,
+    goma_backend = None,
+    reclient_jobs = rbe_jobs.DEFAULT,
+    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -138,6 +141,9 @@
     ),
     notifies = ["chrome-memory-safety"],
     os = os.LINUX_BIONIC_SWITCH_TO_DEFAULT,
+    goma_backend = None,
+    reclient_jobs = rbe_jobs.DEFAULT,
+    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
diff --git a/ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_egtest.mm b/ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_egtest.mm
index 2137647..e5b25e25 100644
--- a/ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_egtest.mm
+++ b/ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_egtest.mm
@@ -38,8 +38,11 @@
 using chrome_test_util::AddToBookmarksButton;
 using chrome_test_util::AddToReadingListButton;
 using chrome_test_util::CloseTabMenuButton;
+using chrome_test_util::TabGridCellAtIndex;
+using chrome_test_util::TabGridNormalModePageControl;
 using chrome_test_util::TabGridSearchBar;
 using chrome_test_util::TabGridSearchCancelButton;
+using chrome_test_util::TabGridSearchModeToolbar;
 using chrome_test_util::TabGridSearchTabsButton;
 using chrome_test_util::TabGridSelectTabsMenuButton;
 
@@ -59,9 +62,19 @@
 const CFTimeInterval kSnackbarAppearanceTimeout = 5;
 const CFTimeInterval kSnackbarDisappearanceTimeout = 11;
 
+id<GREYMatcher> TabGridCell() {
+  return grey_allOf(grey_kindOfClassName(@"GridCell"),
+                    grey_sufficientlyVisible(), nil);
+}
+
 id<GREYMatcher> TabWithTitle(NSString* title) {
-  return grey_allOf(grey_accessibilityLabel(title), grey_sufficientlyVisible(),
-                    nil);
+  return grey_allOf(TabGridCell(), grey_accessibilityLabel(title),
+                    grey_sufficientlyVisible(), nil);
+}
+
+id<GREYMatcher> TabWithTitleAndIndex(char* title, unsigned int index) {
+  return grey_allOf(TabWithTitle([NSString stringWithUTF8String:title]),
+                    TabGridCellAtIndex(index), nil);
 }
 
 // Identifer for cell at given |index| in the tab grid.
@@ -104,6 +117,12 @@
       base::test::ios::kWaitForUIElementTimeout, condition);
   GREYAssertTrue(fullscreenAchieved, @"BrowserViewHiderView still shown");
 }
+
+// Returns a matcher for the scrim view on the tab search.
+id<GREYMatcher> SearchScrim() {
+  return grey_accessibilityID(kTabGridScrimIdentifier);
+}
+
 }  // namespace
 
 @interface TabGridTestCase : WebHttpServerChromeTestCase {
@@ -118,11 +137,18 @@
 
 - (AppLaunchConfiguration)appConfigurationForTestCase {
   AppLaunchConfiguration config;
-
-  if ([self isRunningTest:@selector(testEnterExitSearch)]) {
-    config.features_enabled.push_back(kTabsSearch);
+  std::vector<SEL> searchTests = {
+      @selector(testEnterExitSearch),
+      @selector(testTabGridResetAfterExitingSearch),
+      @selector(testScrimVisibleInSearchModeWhenSearchBarIsEmpty),
+      @selector(testTapOnSearchScrimExitsSearchMode),
+      @selector(testSearchRegularOpenTabs)};
+  for (SEL test : searchTests) {
+    if ([self isRunningTest:test]) {
+      config.features_enabled.push_back(kTabsSearch);
+      break;
+    }
   }
-
   return config;
 }
 
@@ -1208,15 +1234,130 @@
   [ChromeEarlGrey openNewTab];
   [ChromeEarlGrey showTabSwitcher];
 
+  // Enter search mode.
   [[EarlGrey selectElementWithMatcher:TabGridSearchTabsButton()]
       performAction:grey_tap()];
-  [[EarlGrey selectElementWithMatcher:chrome_test_util::TabGridSearchBar()]
-      performAction:grey_typeText(@"text")];
+
+  // Verify that search mode is active.
+  [[EarlGrey
+      selectElementWithMatcher:chrome_test_util::TabGridSearchModeToolbar()]
+      assertWithMatcher:grey_notNil()];
+
+  // Exit search mode.
   [[EarlGrey selectElementWithMatcher:TabGridSearchCancelButton()]
       performAction:grey_tap()];
 
-  GREYAssertEqual([ChromeEarlGrey mainTabCount], 2,
-                  @"All tabs did not return after exiting search.");
+  // Verify that normal mode is active.
+  [[EarlGrey
+      selectElementWithMatcher:chrome_test_util::TabGridNormalModePageControl()]
+      assertWithMatcher:grey_notNil()];
+}
+
+// Tests that exiting search mode reset the tabs count to the original number.
+- (void)testTabGridResetAfterExitingSearch {
+  [ChromeEarlGrey openNewTab];
+  [ChromeEarlGrey showTabSwitcher];
+
+  // Enter search mode & search with a query that produce no results.
+  [[EarlGrey selectElementWithMatcher:TabGridSearchTabsButton()]
+      performAction:grey_tap()];
+  [[EarlGrey selectElementWithMatcher:chrome_test_util::TabGridSearchBar()]
+      performAction:grey_typeText(@"hello")];
+
+  // Verify that search reduced the number of visible tabs.
+  [self verifyVisibleTabsCount:0];
+
+  // Exit search mode & verify that tabs grid was reset.
+  [[EarlGrey selectElementWithMatcher:TabGridSearchCancelButton()]
+      performAction:grey_tap()];
+  [self verifyVisibleTabsCount:2];
+}
+
+// Tests that the scrim view is always shown when the search bar is empty in the
+// search mode.
+- (void)testScrimVisibleInSearchModeWhenSearchBarIsEmpty {
+  [ChromeEarlGrey openNewTab];
+  [ChromeEarlGrey showTabSwitcher];
+
+  // Enter search mode.
+  [[EarlGrey selectElementWithMatcher:TabGridSearchTabsButton()]
+      performAction:grey_tap()];
+
+  // Upon entry, the search bar is empty. Verify that scrim is visible.
+  [[EarlGrey selectElementWithMatcher:SearchScrim()]
+      assertWithMatcher:grey_notNil()];
+
+  // Searching with any query should render scrim invisible.
+  [[EarlGrey selectElementWithMatcher:TabGridSearchBar()]
+      performAction:grey_typeText(@"text")];
+  [[EarlGrey selectElementWithMatcher:SearchScrim()]
+      assertWithMatcher:grey_nil()];
+
+  // Clearing search bar text should render scrim visible again.
+  [[EarlGrey selectElementWithMatcher:TabGridSearchBar()]
+      performAction:grey_clearText()];
+  [[EarlGrey selectElementWithMatcher:SearchScrim()]
+      assertWithMatcher:grey_notNil()];
+
+  // Cancel search mode.
+  [[EarlGrey selectElementWithMatcher:TabGridSearchCancelButton()]
+      performAction:grey_tap()];
+
+  // Verify that scrim is not visible anymore.
+  [[EarlGrey selectElementWithMatcher:SearchScrim()]
+      assertWithMatcher:grey_nil()];
+}
+
+// Tests that tapping on the scrim view while in search mode dismisses the scrim
+// and exits search mode.
+- (void)testTapOnSearchScrimExitsSearchMode {
+  [ChromeEarlGrey openNewTab];
+  [ChromeEarlGrey showTabSwitcher];
+
+  // Enter search mode.
+  [[EarlGrey selectElementWithMatcher:TabGridSearchTabsButton()]
+      performAction:grey_tap()];
+
+  // Tap on scrim.
+  [[EarlGrey selectElementWithMatcher:SearchScrim()] performAction:grey_tap()];
+
+  // Verify that search mode is exit, scrim not visible, and transition to
+  // normal mode was successful.
+  [[EarlGrey selectElementWithMatcher:SearchScrim()]
+      assertWithMatcher:grey_nil()];
+  [[EarlGrey selectElementWithMatcher:TabGridNormalModePageControl()]
+      assertWithMatcher:grey_notNil()];
+  [self verifyVisibleTabsCount:2];
+}
+
+// Tests that searching in open tabs in the regular mode will filter the tabs
+// correctly.
+- (void)testSearchRegularOpenTabs {
+  [self loadTestURLsInNewTabs];
+  [ChromeEarlGrey showTabSwitcher];
+
+  [self verifyVisibleTabsCount:4];
+
+  // Enter search mode.
+  [[EarlGrey selectElementWithMatcher:TabGridSearchTabsButton()]
+      performAction:grey_tap()];
+
+  // Searching with the word "Page" should match only 3 results.
+  [[EarlGrey selectElementWithMatcher:TabGridSearchBar()]
+      performAction:grey_typeText(@"Page")];
+  [self verifyVisibleTabsCount:3];
+
+  // Verify that search results are correct and in the expected order.
+  [[EarlGrey selectElementWithMatcher:TabWithTitleAndIndex(kTitle1, 0)]
+      assertWithMatcher:grey_notNil()];
+  [[EarlGrey selectElementWithMatcher:TabWithTitleAndIndex(kTitle2, 1)]
+      assertWithMatcher:grey_notNil()];
+  [[EarlGrey selectElementWithMatcher:TabWithTitleAndIndex(kTitle4, 2)]
+      assertWithMatcher:grey_notNil()];
+
+  // Cancel search mode.
+  [[EarlGrey selectElementWithMatcher:TabGridSearchCancelButton()]
+      performAction:grey_tap()];
 }
 
 #pragma mark - Helper Methods
@@ -1232,6 +1373,23 @@
   [ChromeEarlGrey waitForWebStateContainingText:kResponse3];
 }
 
+- (void)loadTestURLsInNewTabs {
+  [ChromeEarlGrey loadURL:_URL1];
+  [ChromeEarlGrey waitForWebStateContainingText:kResponse1];
+
+  [ChromeEarlGrey openNewTab];
+  [ChromeEarlGrey loadURL:_URL2];
+  [ChromeEarlGrey waitForWebStateContainingText:kResponse2];
+
+  [ChromeEarlGrey openNewTab];
+  [ChromeEarlGrey loadURL:_URL3];
+  [ChromeEarlGrey waitForWebStateContainingText:kResponse3];
+
+  [ChromeEarlGrey openNewTab];
+  [ChromeEarlGrey loadURL:_URL4];
+  [ChromeEarlGrey waitForWebStateContainingText:kResponse4];
+}
+
 // Loads a URL in a new tab and deletes it to populate Recent Tabs. Then,
 // navigates to the Recent tabs via tab grid.
 - (void)prepareRecentTabWithURL:(const GURL&)URL
@@ -1337,4 +1495,21 @@
              @"Snackbar did not disappear.");
 }
 
+// Verifies that the tab grid has exactly |expectedCount| tabs.
+- (void)verifyVisibleTabsCount:(NSUInteger)expectedCount {
+  // Verify that the cell # |expectedCount| exist.
+  if (expectedCount == 0) {
+    [[EarlGrey selectElementWithMatcher:TabGridCell()]
+        assertWithMatcher:grey_nil()];
+  } else {
+    [[[EarlGrey selectElementWithMatcher:TabGridCell()]
+        atIndex:expectedCount - 1] assertWithMatcher:grey_notNil()];
+  }
+  // Then verify that there is no more cells after that.
+  [[EarlGrey
+      selectElementWithMatcher:grey_allOf(TabGridCell(),
+                                          TabGridCellAtIndex(expectedCount),
+                                          nil)] assertWithMatcher:grey_nil()];
+}
+
 @end
diff --git a/ios/chrome/test/earl_grey/chrome_matchers.h b/ios/chrome/test/earl_grey/chrome_matchers.h
index 738ab80..bc2e0810 100644
--- a/ios/chrome/test/earl_grey/chrome_matchers.h
+++ b/ios/chrome/test/earl_grey/chrome_matchers.h
@@ -487,6 +487,11 @@
 // the tab grid.
 id<GREYMatcher> TabGridOtherDevicesPanelButton();
 
+// Returns a matcher that matches tab grid normal mode page control - The
+// PageControl panel always exist only on the tab grid normal mode, So this can
+// be used to validate that the tab grid normal mode is active.
+id<GREYMatcher> TabGridNormalModePageControl();
+
 // Returns a matcher for the tab grid background.
 id<GREYMatcher> TabGridBackground();
 
@@ -642,6 +647,9 @@
 // Returns a matcher for the tab grid search cancel button.
 id<GREYMatcher> TabGridSearchCancelButton();
 
+// Returns a matcher for the tab grid search mode toolbar.
+id<GREYMatcher> TabGridSearchModeToolbar();
+
 }  // namespace chrome_test_util
 
 #endif  // IOS_CHROME_TEST_EARL_GREY_CHROME_MATCHERS_H_
diff --git a/ios/chrome/test/earl_grey/chrome_matchers.mm b/ios/chrome/test/earl_grey/chrome_matchers.mm
index 89f1ce4..aa7d80ca 100644
--- a/ios/chrome/test/earl_grey/chrome_matchers.mm
+++ b/ios/chrome/test/earl_grey/chrome_matchers.mm
@@ -602,6 +602,10 @@
   return [ChromeMatchersAppInterface tabGridOtherDevicesPanelButton];
 }
 
+id<GREYMatcher> TabGridNormalModePageControl() {
+  return [ChromeMatchersAppInterface tabGridNormalModePageControl];
+}
+
 id<GREYMatcher> TabGridBackground() {
   return [ChromeMatchersAppInterface tabGridBackground];
 }
@@ -801,4 +805,8 @@
   return [ChromeMatchersAppInterface tabGridSearchCancelButton];
 }
 
+id<GREYMatcher> TabGridSearchModeToolbar() {
+  return [ChromeMatchersAppInterface tabGridSearchModeToolbar];
+}
+
 }  // namespace chrome_test_util
diff --git a/ios/chrome/test/earl_grey/chrome_matchers_app_interface.h b/ios/chrome/test/earl_grey/chrome_matchers_app_interface.h
index 3a5fa9ae..008b19d 100644
--- a/ios/chrome/test/earl_grey/chrome_matchers_app_interface.h
+++ b/ios/chrome/test/earl_grey/chrome_matchers_app_interface.h
@@ -474,6 +474,11 @@
 // the tab grid.
 + (id<GREYMatcher>)tabGridOtherDevicesPanelButton;
 
+// Returns a matcher that matches tab grid normal mode page control - The
+// PageControl panel always exist only on the tab grid normal mode, So this can
+// be used to validate that the tab grid normal mode is active.
++ (id<GREYMatcher>)tabGridNormalModePageControl;
+
 // Returns the GREYMatcher for the background of the tab grid.
 + (id<GREYMatcher>)tabGridBackground;
 
@@ -627,6 +632,9 @@
 // Returns a matcher for the tab grid search cancel button.
 + (id<GREYMatcher>)tabGridSearchCancelButton;
 
+// Returns a matcher for the tab grid search mode toolbar.
++ (id<GREYMatcher>)tabGridSearchModeToolbar;
+
 @end
 
 #endif  // IOS_CHROME_TEST_EARL_GREY_CHROME_MATCHERS_APP_INTERFACE_H_
diff --git a/ios/chrome/test/earl_grey/chrome_matchers_app_interface.mm b/ios/chrome/test/earl_grey/chrome_matchers_app_interface.mm
index 78417e2..cb9aa6f 100644
--- a/ios/chrome/test/earl_grey/chrome_matchers_app_interface.mm
+++ b/ios/chrome/test/earl_grey/chrome_matchers_app_interface.mm
@@ -930,6 +930,18 @@
   return grey_accessibilityID(kTabGridRemoteTabsPageButtonIdentifier);
 }
 
++ (id<GREYMatcher>)tabGridNormalModePageControl {
+  return grey_allOf(
+      grey_kindOfClassName(@"UIControl"),
+      grey_descendant(
+          [ChromeMatchersAppInterface tabGridIncognitoTabsPanelButton]),
+      grey_descendant([ChromeMatchersAppInterface tabGridOpenTabsPanelButton]),
+      grey_descendant(
+          [ChromeMatchersAppInterface tabGridOtherDevicesPanelButton]),
+      grey_ancestor(grey_kindOfClassName(@"UIToolbar")),
+      grey_sufficientlyVisible(), nil);
+}
+
 + (id<GREYMatcher>)tabGridBackground {
   return grey_accessibilityID(kGridBackgroundIdentifier);
 }
@@ -1214,4 +1226,12 @@
                     grey_sufficientlyVisible(), nil);
 }
 
++ (id<GREYMatcher>)tabGridSearchModeToolbar {
+  return grey_allOf(
+      grey_kindOfClassName(@"UIToolbar"),
+      grey_descendant([ChromeMatchersAppInterface tabGridSearchBar]),
+      grey_descendant([ChromeMatchersAppInterface tabGridSearchCancelButton]),
+      grey_sufficientlyVisible(), nil);
+}
+
 @end
diff --git a/media/gpu/windows/media_foundation_video_encode_accelerator_win.cc b/media/gpu/windows/media_foundation_video_encode_accelerator_win.cc
index 7cd58a2..61febee3 100644
--- a/media/gpu/windows/media_foundation_video_encode_accelerator_win.cc
+++ b/media/gpu/windows/media_foundation_video_encode_accelerator_win.cc
@@ -40,7 +40,7 @@
 namespace media {
 
 namespace {
-
+const uint32_t kDefaultGOPLength = 3000;
 const uint32_t kDefaultTargetBitrate = 5000000u;
 const size_t kMaxFrameRateNumerator = 30;
 const size_t kMaxFrameRateDenominator = 1;
@@ -402,7 +402,7 @@
     frame_rate_ = kMaxFrameRateNumerator / kMaxFrameRateDenominator;
   bitrate_ = config.bitrate;
   bitstream_buffer_size_ = config.input_visible_size.GetArea();
-  gop_length_ = config.gop_length;
+  gop_length_ = config.gop_length.value_or(kDefaultGOPLength);
   low_latency_mode_ = config.require_low_delay;
 
   if (config.HasTemporalLayer())
@@ -838,11 +838,9 @@
     }
   }
 
-  if (gop_length_.has_value()) {
-    var.ulVal = gop_length_.value();
-    hr = codec_api_->SetValue(&CODECAPI_AVEncMPVGOPSize, &var);
-    RETURN_ON_HR_FAILURE(hr, "Couldn't set low keyframe interval", false);
-  }
+  var.ulVal = gop_length_;
+  hr = codec_api_->SetValue(&CODECAPI_AVEncMPVGOPSize, &var);
+  RETURN_ON_HR_FAILURE(hr, "Couldn't set keyframe interval", false);
 
   if (S_OK == codec_api_->IsModifiable(&CODECAPI_AVLowLatencyMode)) {
     var.vt = VT_BOOL;
@@ -1288,6 +1286,11 @@
 
   {
     MediaBufferScopedPointer scoped_buffer(output_buffer.Get());
+    if (!buffer_ref->mapping.IsValid() || !scoped_buffer.get()) {
+      DLOG(ERROR) << "Failed to copy bitstream media buffer.";
+      return;
+    }
+
     memcpy(buffer_ref->mapping.memory(), scoped_buffer.get(), size);
   }
 
diff --git a/media/gpu/windows/media_foundation_video_encode_accelerator_win.h b/media/gpu/windows/media_foundation_video_encode_accelerator_win.h
index 6d43cab7..2136435 100644
--- a/media/gpu/windows/media_foundation_video_encode_accelerator_win.h
+++ b/media/gpu/windows/media_foundation_video_encode_accelerator_win.h
@@ -186,7 +186,7 @@
 
   // Group of picture length for encoded output stream, indicates the
   // distance between two key frames.
-  absl::optional<uint32_t> gop_length_;
+  uint32_t gop_length_;
 
   Microsoft::WRL::ComPtr<IMFActivate> activate_;
   Microsoft::WRL::ComPtr<IMFTransform> encoder_;
diff --git a/mojo/public/cpp/bindings/BUILD.gn b/mojo/public/cpp/bindings/BUILD.gn
index ecc27957..92e64d4 100644
--- a/mojo/public/cpp/bindings/BUILD.gn
+++ b/mojo/public/cpp/bindings/BUILD.gn
@@ -98,6 +98,7 @@
     "map_traits_stl.h",
     "message.h",
     "message_header_validator.h",
+    "message_metadata_helpers.h",
     "scoped_interface_endpoint_handle.h",
     "scoped_message_error_crash_key.cc",
     "scoped_message_error_crash_key.h",
diff --git a/mojo/public/cpp/bindings/interface_endpoint_client.h b/mojo/public/cpp/bindings/interface_endpoint_client.h
index 84b7019..b1c097e 100644
--- a/mojo/public/cpp/bindings/interface_endpoint_client.h
+++ b/mojo/public/cpp/bindings/interface_endpoint_client.h
@@ -33,9 +33,9 @@
 #include "mojo/public/cpp/bindings/lib/control_message_proxy.h"
 #include "mojo/public/cpp/bindings/message.h"
 #include "mojo/public/cpp/bindings/message_dispatcher.h"
+#include "mojo/public/cpp/bindings/message_metadata_helpers.h"
 #include "mojo/public/cpp/bindings/scoped_interface_endpoint_handle.h"
 #include "mojo/public/cpp/bindings/thread_safe_proxy.h"
-#include "mojo/public/cpp/bindings/tracing_helpers.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace mojo {
diff --git a/mojo/public/cpp/bindings/message_metadata_helpers.h b/mojo/public/cpp/bindings/message_metadata_helpers.h
new file mode 100644
index 0000000..4a403e3
--- /dev/null
+++ b/mojo/public/cpp/bindings/message_metadata_helpers.h
@@ -0,0 +1,23 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MOJO_PUBLIC_CPP_BINDINGS_MESSAGE_METADATA_HELPERS_H_
+#define MOJO_PUBLIC_CPP_BINDINGS_MESSAGE_METADATA_HELPERS_H_
+
+#include <cstdint>
+
+namespace mojo {
+
+class Message;
+
+// Alias for a function taking mojo::Message and returning an IPC hash (stable
+// across Chrome versions).
+using MessageToStableIPCHashCallback = uint32_t (*)(Message&);
+
+// Alias for a function taking mojo::Message and returning method name.
+using MessageToMethodNameCallback = const char* (*)(Message&);
+
+}  // namespace mojo
+
+#endif  // MOJO_PUBLIC_CPP_BINDINGS_MESSAGE_METADATA_HELPERS_H_
diff --git a/mojo/public/cpp/bindings/tracing_helpers.h b/mojo/public/cpp/bindings/tracing_helpers.h
index ec8fedc..23644d5 100644
--- a/mojo/public/cpp/bindings/tracing_helpers.h
+++ b/mojo/public/cpp/bindings/tracing_helpers.h
@@ -24,11 +24,4 @@
 #define TRACE_CATEGORY_OR_DISABLED_BY_DEFAULT_MOJOM(category) category
 #endif
 
-namespace mojo {
-
-using MessageToStableIPCHashCallback = uint32_t (*)(Message&);
-using MessageToMethodNameCallback = const char* (*)(Message&);
-
-}  // namespace mojo
-
 #endif  // MOJO_PUBLIC_CPP_BINDINGS_TRACING_HELPERS_H_
diff --git a/mojo/public/tools/bindings/generators/cpp_templates/module.cc.tmpl b/mojo/public/tools/bindings/generators/cpp_templates/module.cc.tmpl
index ee5fa34..87077f1 100644
--- a/mojo/public/tools/bindings/generators/cpp_templates/module.cc.tmpl
+++ b/mojo/public/tools/bindings/generators/cpp_templates/module.cc.tmpl
@@ -26,7 +26,6 @@
 #include "base/hash/md5_constexpr.h"
 #include "base/run_loop.h"
 #include "base/strings/string_number_conversions.h"
-#include "base/task/common/task_annotator.h"
 #include "base/trace_event/trace_event.h"
 #include "base/trace_event/typed_macros.h"
 #include "mojo/public/cpp/bindings/lib/generated_code_util.h"
diff --git a/net/cert/x509_util_ios_and_mac_unittest.cc b/net/cert/x509_util_ios_and_mac_unittest.cc
index 0a4dc6b..34dcfed 100644
--- a/net/cert/x509_util_ios_and_mac_unittest.cc
+++ b/net/cert/x509_util_ios_and_mac_unittest.cc
@@ -69,7 +69,7 @@
             BytesForSecCert(CFArrayGetValueAtIndex(sec_certs.get(), 3)));
 }
 
-TEST(X509UtilTest, DISABLED_CreateSecCertificateArrayForX509CertificateErrors) {
+TEST(X509UtilTest, CreateSecCertificateArrayForX509CertificateErrors) {
   scoped_refptr<X509Certificate> ok_cert(
       ImportCertFromFile(GetTestCertsDirectory(), "ok_cert.pem"));
   ASSERT_TRUE(ok_cert);
@@ -83,7 +83,7 @@
   ASSERT_TRUE(ok_cert);
 
   std::vector<bssl::UniquePtr<CRYPTO_BUFFER>> intermediates;
-  intermediates.push_back(std::move(bad_cert));
+  intermediates.push_back(bssl::UpRef(bad_cert));
   intermediates.push_back(bssl::UpRef(ok_cert2->cert_buffer()));
   scoped_refptr<X509Certificate> cert_with_intermediates(
       X509Certificate::CreateFromBuffer(bssl::UpRef(ok_cert->cert_buffer()),
@@ -91,25 +91,46 @@
   ASSERT_TRUE(cert_with_intermediates);
   EXPECT_EQ(2U, cert_with_intermediates->intermediate_buffers().size());
 
-  // Normal CreateSecCertificateArrayForX509Certificate fails with invalid
-  // certs in chain.
-  EXPECT_FALSE(CreateSecCertificateArrayForX509Certificate(
-      cert_with_intermediates.get()));
-
   // With InvalidIntermediateBehavior::kIgnore, invalid intermediate certs
   // should be silently dropped.
   base::ScopedCFTypeRef<CFMutableArrayRef> sec_certs(
       CreateSecCertificateArrayForX509Certificate(
           cert_with_intermediates.get(), InvalidIntermediateBehavior::kIgnore));
   ASSERT_TRUE(sec_certs);
-  ASSERT_EQ(2, CFArrayGetCount(sec_certs.get()));
-  for (int i = 0; i < 2; ++i)
+  for (int i = 0; i < CFArrayGetCount(sec_certs.get()); ++i)
     ASSERT_TRUE(CFArrayGetValueAtIndex(sec_certs.get(), i));
 
-  EXPECT_EQ(x509_util::CryptoBufferAsStringPiece(ok_cert->cert_buffer()),
-            BytesForSecCert(CFArrayGetValueAtIndex(sec_certs.get(), 0)));
-  EXPECT_EQ(x509_util::CryptoBufferAsStringPiece(ok_cert2->cert_buffer()),
-            BytesForSecCert(CFArrayGetValueAtIndex(sec_certs.get(), 1)));
+  if (CFArrayGetCount(sec_certs.get()) == 2) {
+    EXPECT_EQ(x509_util::CryptoBufferAsStringPiece(ok_cert->cert_buffer()),
+              BytesForSecCert(CFArrayGetValueAtIndex(sec_certs.get(), 0)));
+    EXPECT_EQ(x509_util::CryptoBufferAsStringPiece(ok_cert2->cert_buffer()),
+              BytesForSecCert(CFArrayGetValueAtIndex(sec_certs.get(), 1)));
+
+    // Normal CreateSecCertificateArrayForX509Certificate should fail with
+    // invalid certs in chain.
+    EXPECT_FALSE(CreateSecCertificateArrayForX509Certificate(
+        cert_with_intermediates.get()));
+  } else if (CFArrayGetCount(sec_certs.get()) == 3) {
+    // On older macOS versions that do lazy parsing of SecCertificates, the
+    // invalid certificate may be accepted, which is okay. The test is just
+    // verifying that *if* creating a SecCertificate from one of the
+    // intermediates fails, that cert is ignored and the other certs are still
+    // returned.
+    EXPECT_EQ(x509_util::CryptoBufferAsStringPiece(ok_cert->cert_buffer()),
+              BytesForSecCert(CFArrayGetValueAtIndex(sec_certs.get(), 0)));
+    EXPECT_EQ(x509_util::CryptoBufferAsStringPiece(bad_cert.get()),
+              BytesForSecCert(CFArrayGetValueAtIndex(sec_certs.get(), 1)));
+    EXPECT_EQ(x509_util::CryptoBufferAsStringPiece(ok_cert2->cert_buffer()),
+              BytesForSecCert(CFArrayGetValueAtIndex(sec_certs.get(), 2)));
+
+    // Normal CreateSecCertificateArrayForX509Certificate should also
+    // succeed in this case.
+    EXPECT_TRUE(CreateSecCertificateArrayForX509Certificate(
+        cert_with_intermediates.get()));
+  } else {
+    ADD_FAILURE() << "CFArrayGetCount(sec_certs.get()) = "
+                  << CFArrayGetCount(sec_certs.get());
+  }
 }
 
 TEST(X509UtilTest,
diff --git a/net/log/net_log_event_type_list.h b/net/log/net_log_event_type_list.h
index 09b718f..0e73ffd86 100644
--- a/net/log/net_log_event_type_list.h
+++ b/net/log/net_log_event_type_list.h
@@ -687,20 +687,6 @@
 // }
 EVENT_TYPE(CERT_CT_COMPLIANCE_CHECKED)
 
-// The EV certificate was checked for compliance with Certificate Transparency
-// requirements.
-//
-// The following parameters are attached to the event:
-// {
-//    "certificate": <An X.509 certificate, same format as in
-//                   CERT_VERIFIER_JOB.>
-//    "policy_enforcement_required": <boolean>
-//    "build_timely": <boolean>
-//    "ct_compliance_status": <string describing compliance status>
-//    "ev_whitelist_version": <optional; string representing whitelist version>
-// }
-EVENT_TYPE(EV_CERT_CT_COMPLIANCE_CHECKED)
-
 // A Certificate Transparency log entry was audited for inclusion in the
 // log.
 //
diff --git a/testing/buildbot/chrome.json b/testing/buildbot/chrome.json
index 6e3623f..a7a9576 100644
--- a/testing/buildbot/chrome.json
+++ b/testing/buildbot/chrome.json
@@ -1778,7 +1778,7 @@
       {
         "args": [],
         "cros_board": "atlas",
-        "cros_img": "atlas-release/R101-14538.0.0",
+        "cros_img": "atlas-release/R101-14541.0.0",
         "name": "lacros_all_tast_tests_ATLAS_LKGM",
         "resultdb": {
           "enable": true,
@@ -1838,7 +1838,7 @@
       {
         "args": [],
         "cros_board": "eve",
-        "cros_img": "eve-release/R101-14538.0.0",
+        "cros_img": "eve-release/R101-14541.0.0",
         "name": "lacros_all_tast_tests_EVE_LKGM",
         "resultdb": {
           "enable": true,
@@ -1943,7 +1943,7 @@
       {
         "args": [],
         "cros_board": "kevin",
-        "cros_img": "kevin-release/R101-14538.0.0",
+        "cros_img": "kevin-release/R101-14541.0.0",
         "name": "lacros_all_tast_tests_KEVIN_LKGM",
         "resultdb": {
           "enable": true,
@@ -1958,7 +1958,7 @@
       {
         "args": [],
         "cros_board": "hana",
-        "cros_img": "hana-release/R101-14538.0.0",
+        "cros_img": "hana-release/R101-14541.0.0",
         "name": "lacros_all_tast_tests_HANA_LKGM",
         "resultdb": {
           "enable": true,
@@ -1973,7 +1973,7 @@
       {
         "args": [],
         "cros_board": "kevin",
-        "cros_img": "kevin-release/R101-14538.0.0",
+        "cros_img": "kevin-release/R101-14541.0.0",
         "name": "ozone_unittests_KEVIN_LKGM",
         "resultdb": {
           "enable": true,
@@ -1987,7 +1987,7 @@
       {
         "args": [],
         "cros_board": "hana",
-        "cros_img": "hana-release/R101-14538.0.0",
+        "cros_img": "hana-release/R101-14541.0.0",
         "name": "ozone_unittests_HANA_LKGM",
         "resultdb": {
           "enable": true,
@@ -2001,7 +2001,7 @@
       {
         "args": [],
         "cros_board": "kevin",
-        "cros_img": "kevin-release/R101-14538.0.0",
+        "cros_img": "kevin-release/R101-14541.0.0",
         "name": "viz_unittests_KEVIN_LKGM",
         "resultdb": {
           "enable": true,
@@ -2015,7 +2015,7 @@
       {
         "args": [],
         "cros_board": "hana",
-        "cros_img": "hana-release/R101-14538.0.0",
+        "cros_img": "hana-release/R101-14541.0.0",
         "name": "viz_unittests_HANA_LKGM",
         "resultdb": {
           "enable": true,
diff --git a/testing/buildbot/chromium.android.fyi.json b/testing/buildbot/chromium.android.fyi.json
index a0b27aed..012d9f2f 100644
--- a/testing/buildbot/chromium.android.fyi.json
+++ b/testing/buildbot/chromium.android.fyi.json
@@ -6769,6 +6769,174 @@
           "--test-runner-outdir",
           ".",
           "--client-outdir",
+          "../../weblayer_instrumentation_test_M96/out/Release",
+          "--implementation-outdir",
+          ".",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--client-version=96",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android30.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_client_from_96"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_client_from_96",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M96",
+              "revision": "version:96.0.4664.141"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android30",
+              "path": ".android_emulator/generic_android30"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android30"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          "../../weblayer_instrumentation_test_M97/out/Release",
+          "--implementation-outdir",
+          ".",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--client-version=97",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android30.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_client_from_97"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_client_from_97",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M97",
+              "revision": "version:97.0.4692.102"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android30",
+              "path": ".android_emulator/generic_android30"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android30"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
           "../../weblayer_instrumentation_test_M99/out/Release",
           "--implementation-outdir",
           ".",
@@ -7023,6 +7191,174 @@
           "--client-outdir",
           ".",
           "--implementation-outdir",
+          "../../weblayer_instrumentation_test_M96/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--impl-version=96",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android30.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_impl_from_96"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_impl_from_96",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M96",
+              "revision": "version:96.0.4664.141"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android30",
+              "path": ".android_emulator/generic_android30"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android30"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/AOSP_SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          ".",
+          "--implementation-outdir",
+          "../../weblayer_instrumentation_test_M97/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--impl-version=97",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android30.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_impl_from_97"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_impl_from_97",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M97",
+              "revision": "version:97.0.4692.102"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android30",
+              "path": ".android_emulator/generic_android30"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android30"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/AOSP_SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          ".",
+          "--implementation-outdir",
           "../../weblayer_instrumentation_test_M99/out/Release",
           "--test-expectations",
           "../../weblayer/browser/android/javatests/skew/expectations.txt",
diff --git a/testing/buildbot/chromium.android.json b/testing/buildbot/chromium.android.json
index 4e64afc7..80539ba 100644
--- a/testing/buildbot/chromium.android.json
+++ b/testing/buildbot/chromium.android.json
@@ -41788,6 +41788,174 @@
           "--test-runner-outdir",
           ".",
           "--client-outdir",
+          "../../weblayer_instrumentation_test_M96/out/Release",
+          "--implementation-outdir",
+          ".",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--client-version=96",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android29.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_client_from_96"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_client_from_96",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M96",
+              "revision": "version:96.0.4664.141"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android29",
+              "path": ".android_emulator/generic_android29"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android29"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          "../../weblayer_instrumentation_test_M97/out/Release",
+          "--implementation-outdir",
+          ".",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--client-version=97",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android29.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_client_from_97"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_client_from_97",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M97",
+              "revision": "version:97.0.4692.102"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android29",
+              "path": ".android_emulator/generic_android29"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android29"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
           "../../weblayer_instrumentation_test_M99/out/Release",
           "--implementation-outdir",
           ".",
@@ -42042,6 +42210,174 @@
           "--client-outdir",
           ".",
           "--implementation-outdir",
+          "../../weblayer_instrumentation_test_M96/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--impl-version=96",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android29.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_impl_from_96"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_impl_from_96",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M96",
+              "revision": "version:96.0.4664.141"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android29",
+              "path": ".android_emulator/generic_android29"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android29"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/AOSP_SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          ".",
+          "--implementation-outdir",
+          "../../weblayer_instrumentation_test_M97/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--impl-version=97",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android29.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_impl_from_97"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_impl_from_97",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M97",
+              "revision": "version:97.0.4692.102"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android29",
+              "path": ".android_emulator/generic_android29"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android29"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/AOSP_SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          ".",
+          "--implementation-outdir",
           "../../weblayer_instrumentation_test_M99/out/Release",
           "--test-expectations",
           "../../weblayer/browser/android/javatests/skew/expectations.txt",
@@ -42296,6 +42632,174 @@
           "--test-runner-outdir",
           ".",
           "--client-outdir",
+          "../../weblayer_instrumentation_test_M96/out/Release",
+          "--implementation-outdir",
+          ".",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--client-version=96",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android23.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_chrome_with_client_from_96"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_chrome_with_client_from_96",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M96",
+              "revision": "version:96.0.4664.141"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android23",
+              "path": ".android_emulator/generic_android23"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android23"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests_with_chrome",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests_with_chrome/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/ChromePublic.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          "../../weblayer_instrumentation_test_M97/out/Release",
+          "--implementation-outdir",
+          ".",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--client-version=97",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android23.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_chrome_with_client_from_97"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_chrome_with_client_from_97",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M97",
+              "revision": "version:97.0.4692.102"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android23",
+              "path": ".android_emulator/generic_android23"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android23"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests_with_chrome",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests_with_chrome/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/ChromePublic.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
           "../../weblayer_instrumentation_test_M99/out/Release",
           "--implementation-outdir",
           ".",
@@ -42550,6 +43054,174 @@
           "--client-outdir",
           ".",
           "--implementation-outdir",
+          "../../weblayer_instrumentation_test_M96/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--impl-version=96",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android23.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_chrome_with_impl_from_96"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_chrome_with_impl_from_96",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M96",
+              "revision": "version:96.0.4664.141"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android23",
+              "path": ".android_emulator/generic_android23"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android23"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests_with_chrome",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests_with_chrome/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/ChromePublic.apk",
+          "--webview-apk-path=apks/AOSP_SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          ".",
+          "--implementation-outdir",
+          "../../weblayer_instrumentation_test_M97/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--impl-version=97",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android23.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_chrome_with_impl_from_97"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_chrome_with_impl_from_97",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M97",
+              "revision": "version:97.0.4692.102"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android23",
+              "path": ".android_emulator/generic_android23"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android23"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests_with_chrome",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests_with_chrome/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/ChromePublic.apk",
+          "--webview-apk-path=apks/AOSP_SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          ".",
+          "--implementation-outdir",
           "../../weblayer_instrumentation_test_M99/out/Release",
           "--test-expectations",
           "../../weblayer/browser/android/javatests/skew/expectations.txt",
@@ -42871,6 +43543,174 @@
           "--test-runner-outdir",
           ".",
           "--client-outdir",
+          "../../weblayer_instrumentation_test_M96/out/Release",
+          "--implementation-outdir",
+          ".",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--client-version=96",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android27.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_client_from_96"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_client_from_96",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M96",
+              "revision": "version:96.0.4664.141"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android27",
+              "path": ".android_emulator/generic_android27"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android27"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          "../../weblayer_instrumentation_test_M97/out/Release",
+          "--implementation-outdir",
+          ".",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--client-version=97",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android27.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_client_from_97"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_client_from_97",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M97",
+              "revision": "version:97.0.4692.102"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android27",
+              "path": ".android_emulator/generic_android27"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android27"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
           "../../weblayer_instrumentation_test_M99/out/Release",
           "--implementation-outdir",
           ".",
@@ -43125,6 +43965,174 @@
           "--client-outdir",
           ".",
           "--implementation-outdir",
+          "../../weblayer_instrumentation_test_M96/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--impl-version=96",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android27.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_impl_from_96"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_impl_from_96",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M96",
+              "revision": "version:96.0.4664.141"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android27",
+              "path": ".android_emulator/generic_android27"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android27"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          ".",
+          "--implementation-outdir",
+          "../../weblayer_instrumentation_test_M97/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--impl-version=97",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android27.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_impl_from_97"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_impl_from_97",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M97",
+              "revision": "version:97.0.4692.102"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android27",
+              "path": ".android_emulator/generic_android27"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android27"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          ".",
+          "--implementation-outdir",
           "../../weblayer_instrumentation_test_M99/out/Release",
           "--test-expectations",
           "../../weblayer/browser/android/javatests/skew/expectations.txt",
@@ -43446,6 +44454,174 @@
           "--test-runner-outdir",
           ".",
           "--client-outdir",
+          "../../weblayer_instrumentation_test_M96/out/Release",
+          "--implementation-outdir",
+          ".",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--client-version=96",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android28.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_client_from_96"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_client_from_96",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M96",
+              "revision": "version:96.0.4664.141"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android28",
+              "path": ".android_emulator/generic_android28"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android28"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          "../../weblayer_instrumentation_test_M97/out/Release",
+          "--implementation-outdir",
+          ".",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--client-version=97",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android28.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_client_from_97"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_client_from_97",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M97",
+              "revision": "version:97.0.4692.102"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android28",
+              "path": ".android_emulator/generic_android28"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android28"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
           "../../weblayer_instrumentation_test_M99/out/Release",
           "--implementation-outdir",
           ".",
@@ -43700,6 +44876,174 @@
           "--client-outdir",
           ".",
           "--implementation-outdir",
+          "../../weblayer_instrumentation_test_M96/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--impl-version=96",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android28.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_impl_from_96"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_impl_from_96",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M96",
+              "revision": "version:96.0.4664.141"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android28",
+              "path": ".android_emulator/generic_android28"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android28"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          ".",
+          "--implementation-outdir",
+          "../../weblayer_instrumentation_test_M97/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
+          "--impl-version=97",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
+          "--avd-config=../../tools/android/avd/proto/generic_android28.textpb"
+        ],
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "weblayer_skew_tests_with_impl_from_97"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "weblayer_skew_tests_with_impl_from_97",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "chromium/testing/weblayer-x86",
+              "location": "weblayer_instrumentation_test_M97",
+              "revision": "version:97.0.4692.102"
+            },
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
+          "dimension_sets": [
+            {
+              "cpu": "x86-64",
+              "device_os": null,
+              "device_type": null,
+              "machine_type": "n1-standard-4|e2-standard-4",
+              "os": "Ubuntu-16.04|Ubuntu-18.04",
+              "pool": "chromium.tests.avd"
+            }
+          ],
+          "named_caches": [
+            {
+              "name": "generic_android28",
+              "path": ".android_emulator/generic_android28"
+            }
+          ],
+          "optional_dimensions": {
+            "60": [
+              {
+                "caches": "generic_android28"
+              }
+            ]
+          },
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
+        },
+        "test": "weblayer_skew_tests",
+        "test_id_prefix": "ninja://weblayer/browser/android/javatests:weblayer_skew_tests/"
+      },
+      {
+        "args": [
+          "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
+          "--test-runner-outdir",
+          ".",
+          "--client-outdir",
+          ".",
+          "--implementation-outdir",
           "../../weblayer_instrumentation_test_M99/out/Release",
           "--test-expectations",
           "../../weblayer/browser/android/javatests/skew/expectations.txt",
diff --git a/testing/buildbot/chromium.chromiumos.json b/testing/buildbot/chromium.chromiumos.json
index bb7da7a..53f2064 100644
--- a/testing/buildbot/chromium.chromiumos.json
+++ b/testing/buildbot/chromium.chromiumos.json
@@ -5796,7 +5796,7 @@
       },
       {
         "args": [
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4918.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4919.0/test_ash_chrome",
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter"
         ],
         "isolate_profile_data": true,
@@ -5804,14 +5804,14 @@
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
-        "name": "lacros_chrome_browsertests_Lacros version skew testing ash 101.0.4918.0",
+        "name": "lacros_chrome_browsertests_Lacros version skew testing ash 101.0.4919.0",
         "swarming": {
           "can_use_on_swarming_builders": true,
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v101.0.4918.0",
-              "revision": "version:101.0.4918.0"
+              "location": "lacros_version_skew_tests_v101.0.4919.0",
+              "revision": "version:101.0.4919.0"
             }
           ],
           "dimension_sets": [
@@ -5938,7 +5938,7 @@
       },
       {
         "args": [
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4918.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4919.0/test_ash_chrome",
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter"
         ],
         "isolate_profile_data": true,
@@ -5946,14 +5946,14 @@
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
-        "name": "lacros_chrome_browsertests_run_in_series_Lacros version skew testing ash 101.0.4918.0",
+        "name": "lacros_chrome_browsertests_run_in_series_Lacros version skew testing ash 101.0.4919.0",
         "swarming": {
           "can_use_on_swarming_builders": true,
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v101.0.4918.0",
-              "revision": "version:101.0.4918.0"
+              "location": "lacros_version_skew_tests_v101.0.4919.0",
+              "revision": "version:101.0.4919.0"
             }
           ],
           "dimension_sets": [
diff --git a/testing/buildbot/chromium.fyi.json b/testing/buildbot/chromium.fyi.json
index d4523a1..cc3a390 100644
--- a/testing/buildbot/chromium.fyi.json
+++ b/testing/buildbot/chromium.fyi.json
@@ -85831,7 +85831,7 @@
       },
       {
         "args": [
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4918.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4919.0/test_ash_chrome",
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter"
         ],
         "isolate_profile_data": true,
@@ -85839,14 +85839,14 @@
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
-        "name": "lacros_chrome_browsertests_Lacros version skew testing ash 101.0.4918.0",
+        "name": "lacros_chrome_browsertests_Lacros version skew testing ash 101.0.4919.0",
         "swarming": {
           "can_use_on_swarming_builders": true,
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v101.0.4918.0",
-              "revision": "version:101.0.4918.0"
+              "location": "lacros_version_skew_tests_v101.0.4919.0",
+              "revision": "version:101.0.4919.0"
             }
           ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
@@ -85948,7 +85948,7 @@
       },
       {
         "args": [
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4918.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4919.0/test_ash_chrome",
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter"
         ],
         "isolate_profile_data": true,
@@ -85956,14 +85956,14 @@
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
-        "name": "lacros_chrome_browsertests_run_in_series_Lacros version skew testing ash 101.0.4918.0",
+        "name": "lacros_chrome_browsertests_run_in_series_Lacros version skew testing ash 101.0.4919.0",
         "swarming": {
           "can_use_on_swarming_builders": true,
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v101.0.4918.0",
-              "revision": "version:101.0.4918.0"
+              "location": "lacros_version_skew_tests_v101.0.4919.0",
+              "revision": "version:101.0.4919.0"
             }
           ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
@@ -87330,21 +87330,21 @@
       },
       {
         "args": [
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4918.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4919.0/test_ash_chrome",
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter"
         ],
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
-        "name": "lacros_chrome_browsertests_Lacros version skew testing ash 101.0.4918.0",
+        "name": "lacros_chrome_browsertests_Lacros version skew testing ash 101.0.4919.0",
         "swarming": {
           "can_use_on_swarming_builders": true,
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v101.0.4918.0",
-              "revision": "version:101.0.4918.0"
+              "location": "lacros_version_skew_tests_v101.0.4919.0",
+              "revision": "version:101.0.4919.0"
             }
           ],
           "dimension_sets": [
@@ -87472,21 +87472,21 @@
       },
       {
         "args": [
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4918.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4919.0/test_ash_chrome",
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter"
         ],
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
-        "name": "lacros_chrome_browsertests_run_in_series_Lacros version skew testing ash 101.0.4918.0",
+        "name": "lacros_chrome_browsertests_run_in_series_Lacros version skew testing ash 101.0.4919.0",
         "swarming": {
           "can_use_on_swarming_builders": true,
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v101.0.4918.0",
-              "revision": "version:101.0.4918.0"
+              "location": "lacros_version_skew_tests_v101.0.4919.0",
+              "revision": "version:101.0.4919.0"
             }
           ],
           "dimension_sets": [
@@ -89027,21 +89027,21 @@
       },
       {
         "args": [
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4918.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4919.0/test_ash_chrome",
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter"
         ],
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
-        "name": "lacros_chrome_browsertests_Lacros version skew testing ash 101.0.4918.0",
+        "name": "lacros_chrome_browsertests_Lacros version skew testing ash 101.0.4919.0",
         "swarming": {
           "can_use_on_swarming_builders": true,
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v101.0.4918.0",
-              "revision": "version:101.0.4918.0"
+              "location": "lacros_version_skew_tests_v101.0.4919.0",
+              "revision": "version:101.0.4919.0"
             }
           ],
           "dimension_sets": [
@@ -89169,21 +89169,21 @@
       },
       {
         "args": [
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4918.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4919.0/test_ash_chrome",
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter"
         ],
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
-        "name": "lacros_chrome_browsertests_run_in_series_Lacros version skew testing ash 101.0.4918.0",
+        "name": "lacros_chrome_browsertests_run_in_series_Lacros version skew testing ash 101.0.4919.0",
         "swarming": {
           "can_use_on_swarming_builders": true,
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v101.0.4918.0",
-              "revision": "version:101.0.4918.0"
+              "location": "lacros_version_skew_tests_v101.0.4919.0",
+              "revision": "version:101.0.4919.0"
             }
           ],
           "dimension_sets": [
@@ -89920,21 +89920,21 @@
       },
       {
         "args": [
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4918.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4919.0/test_ash_chrome",
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter"
         ],
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
-        "name": "lacros_chrome_browsertests_Lacros version skew testing ash 101.0.4918.0",
+        "name": "lacros_chrome_browsertests_Lacros version skew testing ash 101.0.4919.0",
         "swarming": {
           "can_use_on_swarming_builders": true,
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v101.0.4918.0",
-              "revision": "version:101.0.4918.0"
+              "location": "lacros_version_skew_tests_v101.0.4919.0",
+              "revision": "version:101.0.4919.0"
             }
           ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
@@ -90016,21 +90016,21 @@
       },
       {
         "args": [
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4918.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4919.0/test_ash_chrome",
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter"
         ],
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
-        "name": "lacros_chrome_browsertests_run_in_series_Lacros version skew testing ash 101.0.4918.0",
+        "name": "lacros_chrome_browsertests_run_in_series_Lacros version skew testing ash 101.0.4919.0",
         "swarming": {
           "can_use_on_swarming_builders": true,
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v101.0.4918.0",
-              "revision": "version:101.0.4918.0"
+              "location": "lacros_version_skew_tests_v101.0.4919.0",
+              "revision": "version:101.0.4919.0"
             }
           ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
diff --git a/testing/buildbot/internal.chromeos.fyi.json b/testing/buildbot/internal.chromeos.fyi.json
index 701b88b9..e8d237b7f 100644
--- a/testing/buildbot/internal.chromeos.fyi.json
+++ b/testing/buildbot/internal.chromeos.fyi.json
@@ -1125,7 +1125,7 @@
       {
         "args": [],
         "cros_board": "octopus",
-        "cros_img": "octopus-release/R101-14538.0.0",
+        "cros_img": "octopus-release/R101-14541.0.0",
         "name": "lacros_fyi_tast_tests_OCTOPUS_LKGM",
         "swarming": {},
         "tast_expr": "(\"group:mainline\" && \"dep:lacros\" && !informational)",
@@ -1169,7 +1169,7 @@
       {
         "args": [],
         "cros_board": "octopus",
-        "cros_img": "octopus-release/R101-14538.0.0",
+        "cros_img": "octopus-release/R101-14541.0.0",
         "name": "ozone_unittests_OCTOPUS_LKGM",
         "swarming": {},
         "test": "ozone_unittests",
@@ -1217,7 +1217,7 @@
       {
         "args": [],
         "cros_board": "kevin",
-        "cros_img": "kevin-release/R101-14538.0.0",
+        "cros_img": "kevin-release/R101-14541.0.0",
         "name": "lacros_all_tast_tests_KEVIN_LKGM",
         "swarming": {},
         "tast_expr": "(\"group:mainline\" && \"dep:lacros\" && !informational)",
@@ -1228,7 +1228,7 @@
       {
         "args": [],
         "cros_board": "hana",
-        "cros_img": "hana-release/R101-14538.0.0",
+        "cros_img": "hana-release/R101-14541.0.0",
         "name": "lacros_all_tast_tests_HANA_LKGM",
         "swarming": {},
         "tast_expr": "(\"group:mainline\" && \"dep:lacros\" && !informational)",
@@ -1239,7 +1239,7 @@
       {
         "args": [],
         "cros_board": "kevin",
-        "cros_img": "kevin-release/R101-14538.0.0",
+        "cros_img": "kevin-release/R101-14541.0.0",
         "name": "ozone_unittests_KEVIN_LKGM",
         "swarming": {},
         "test": "ozone_unittests",
@@ -1249,7 +1249,7 @@
       {
         "args": [],
         "cros_board": "hana",
-        "cros_img": "hana-release/R101-14538.0.0",
+        "cros_img": "hana-release/R101-14541.0.0",
         "name": "ozone_unittests_HANA_LKGM",
         "swarming": {},
         "test": "ozone_unittests",
@@ -1259,7 +1259,7 @@
       {
         "args": [],
         "cros_board": "kevin",
-        "cros_img": "kevin-release/R101-14538.0.0",
+        "cros_img": "kevin-release/R101-14541.0.0",
         "name": "viz_unittests_KEVIN_LKGM",
         "swarming": {},
         "test": "viz_unittests",
@@ -1269,7 +1269,7 @@
       {
         "args": [],
         "cros_board": "hana",
-        "cros_img": "hana-release/R101-14538.0.0",
+        "cros_img": "hana-release/R101-14541.0.0",
         "name": "viz_unittests_HANA_LKGM",
         "swarming": {},
         "test": "viz_unittests",
diff --git a/testing/buildbot/test_suites.pyl b/testing/buildbot/test_suites.pyl
index fe8b7394..a274b43 100644
--- a/testing/buildbot/test_suites.pyl
+++ b/testing/buildbot/test_suites.pyl
@@ -6608,10 +6608,14 @@
         'variants': [
           'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MILESTONE',
           'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_ONE_MILESTONE',
-          'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_TWO_MILESTONE',
+          'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_THREE_MILESTONE',
+          'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_FOUR_MILESTONE',
+          'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_FIVE_MILESTONE',
           'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MILESTONE',
           'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_ONE_MILESTONE',
-          'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_TWO_MILESTONE',
+          'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_THREE_MILESTONE',
+          'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_FOUR_MILESTONE',
+          'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_FIVE_MILESTONE',
         ]
       }
     },
@@ -6621,10 +6625,14 @@
         'variants': [
           'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MILESTONE',
           'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_ONE_MILESTONE',
-          'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_TWO_MILESTONE',
+          'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_THREE_MILESTONE',
+          'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_FOUR_MILESTONE',
+          'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_FIVE_MILESTONE',
           'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MILESTONE',
           'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_ONE_MILESTONE',
-          'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_TWO_MILESTONE',
+          'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_THREE_MILESTONE',
+          'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_FOUR_MILESTONE',
+          'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_FIVE_MILESTONE',
         ]
       }
     },
@@ -6639,10 +6647,14 @@
         'variants': [
           'WEBLAYER_IMPL_SKEW_TESTS_NTH_MILESTONE',
           'WEBLAYER_IMPL_SKEW_TESTS_NTH_MINUS_ONE_MILESTONE',
-          'WEBLAYER_IMPL_SKEW_TESTS_NTH_MINUS_TWO_MILESTONE',
+          'WEBLAYER_IMPL_SKEW_TESTS_NTH_MINUS_THREE_MILESTONE',
+          'WEBLAYER_IMPL_SKEW_TESTS_NTH_MINUS_FOUR_MILESTONE',
+          'WEBLAYER_IMPL_SKEW_TESTS_NTH_MINUS_FIVE_MILESTONE',
           'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MILESTONE',
           'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_ONE_MILESTONE',
-          'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_TWO_MILESTONE',
+          'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_THREE_MILESTONE',
+          'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_FOUR_MILESTONE',
+          'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_FIVE_MILESTONE',
         ]
       }
     },
diff --git a/testing/buildbot/variants.pyl b/testing/buildbot/variants.pyl
index 7d375680..fa21a4db 100644
--- a/testing/buildbot/variants.pyl
+++ b/testing/buildbot/variants.pyl
@@ -28,16 +28,16 @@
   },
   'LACROS_VERSION_SKEW_CANARY': {
     'args': [
-      '--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4918.0/test_ash_chrome',
+      '--ash-chrome-path-override=../../lacros_version_skew_tests_v101.0.4919.0/test_ash_chrome',
       '--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter',
     ],
-    'identifier': 'Lacros version skew testing ash 101.0.4918.0',
+    'identifier': 'Lacros version skew testing ash 101.0.4919.0',
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/linux-ash-chromium/x86_64/ash.zip',
-          'location': 'lacros_version_skew_tests_v101.0.4918.0',
-          'revision': 'version:101.0.4918.0',
+          'location': 'lacros_version_skew_tests_v101.0.4919.0',
+          'revision': 'version:101.0.4919.0',
         },
       ],
     },
@@ -416,7 +416,55 @@
       ],
     },
   },
-  'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_TWO_MILESTONE': {
+  'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_THREE_MILESTONE': {
+    'args': [
+      '--webview-apk-path=apks/AOSP_SystemWebView.apk',
+      '--test-runner-outdir',
+      '.',
+      '--client-outdir',
+      '.',
+      '--implementation-outdir',
+      '../../weblayer_instrumentation_test_M97/out/Release',
+      '--test-expectations',
+      '../../weblayer/browser/android/javatests/skew/expectations.txt',
+      '--impl-version=97',
+    ],
+    'identifier': 'with_impl_from_97',
+    'swarming': {
+      'cipd_packages': [
+        {
+          'cipd_package': 'chromium/testing/weblayer-x86',
+          'location': 'weblayer_instrumentation_test_M97',
+          'revision': 'version:97.0.4692.102',
+        }
+      ],
+    },
+  },
+  'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_FOUR_MILESTONE': {
+    'args': [
+      '--webview-apk-path=apks/AOSP_SystemWebView.apk',
+      '--test-runner-outdir',
+      '.',
+      '--client-outdir',
+      '.',
+      '--implementation-outdir',
+      '../../weblayer_instrumentation_test_M96/out/Release',
+      '--test-expectations',
+      '../../weblayer/browser/android/javatests/skew/expectations.txt',
+      '--impl-version=96',
+    ],
+    'identifier': 'with_impl_from_96',
+    'swarming': {
+      'cipd_packages': [
+        {
+          'cipd_package': 'chromium/testing/weblayer-x86',
+          'location': 'weblayer_instrumentation_test_M96',
+          'revision': 'version:96.0.4664.141',
+        }
+      ],
+    },
+  },
+  'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_FIVE_MILESTONE': {
     'args': [
       '--webview-apk-path=apks/AOSP_SystemWebView.apk',
       '--test-runner-outdir',
@@ -488,7 +536,55 @@
       ],
     },
   },
-  'WEBLAYER_IMPL_SKEW_TESTS_NTH_MINUS_TWO_MILESTONE': {
+  'WEBLAYER_IMPL_SKEW_TESTS_NTH_MINUS_THREE_MILESTONE': {
+    'args': [
+      '--webview-apk-path=apks/SystemWebView.apk',
+      '--test-runner-outdir',
+      '.',
+      '--client-outdir',
+      '.',
+      '--implementation-outdir',
+      '../../weblayer_instrumentation_test_M97/out/Release',
+      '--test-expectations',
+      '../../weblayer/browser/android/javatests/skew/expectations.txt',
+      '--impl-version=97',
+    ],
+    'identifier': 'with_impl_from_97',
+    'swarming': {
+      'cipd_packages': [
+        {
+          'cipd_package': 'chromium/testing/weblayer-x86',
+          'location': 'weblayer_instrumentation_test_M97',
+          'revision': 'version:97.0.4692.102',
+        }
+      ],
+    },
+  },
+  'WEBLAYER_IMPL_SKEW_TESTS_NTH_MINUS_FOUR_MILESTONE': {
+    'args': [
+      '--webview-apk-path=apks/SystemWebView.apk',
+      '--test-runner-outdir',
+      '.',
+      '--client-outdir',
+      '.',
+      '--implementation-outdir',
+      '../../weblayer_instrumentation_test_M96/out/Release',
+      '--test-expectations',
+      '../../weblayer/browser/android/javatests/skew/expectations.txt',
+      '--impl-version=96',
+    ],
+    'identifier': 'with_impl_from_96',
+    'swarming': {
+      'cipd_packages': [
+        {
+          'cipd_package': 'chromium/testing/weblayer-x86',
+          'location': 'weblayer_instrumentation_test_M96',
+          'revision': 'version:96.0.4664.141',
+        }
+      ],
+    },
+  },
+  'WEBLAYER_IMPL_SKEW_TESTS_NTH_MINUS_FIVE_MILESTONE': {
     'args': [
       '--webview-apk-path=apks/SystemWebView.apk',
       '--test-runner-outdir',
@@ -560,7 +656,55 @@
       ],
     },
   },
-  'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_TWO_MILESTONE': {
+  'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_THREE_MILESTONE': {
+    'args': [
+      '--webview-apk-path=apks/SystemWebView.apk',
+      '--test-runner-outdir',
+      '.',
+      '--client-outdir',
+      '../../weblayer_instrumentation_test_M97/out/Release',
+      '--implementation-outdir',
+      '.',
+      '--test-expectations',
+      '../../weblayer/browser/android/javatests/skew/expectations.txt',
+      '--client-version=97',
+    ],
+    'identifier': 'with_client_from_97',
+    'swarming': {
+      'cipd_packages': [
+        {
+          'cipd_package': 'chromium/testing/weblayer-x86',
+          'location': 'weblayer_instrumentation_test_M97',
+          'revision': 'version:97.0.4692.102',
+        }
+      ],
+    },
+  },
+  'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_FOUR_MILESTONE': {
+    'args': [
+      '--webview-apk-path=apks/SystemWebView.apk',
+      '--test-runner-outdir',
+      '.',
+      '--client-outdir',
+      '../../weblayer_instrumentation_test_M96/out/Release',
+      '--implementation-outdir',
+      '.',
+      '--test-expectations',
+      '../../weblayer/browser/android/javatests/skew/expectations.txt',
+      '--client-version=96',
+    ],
+    'identifier': 'with_client_from_96',
+    'swarming': {
+      'cipd_packages': [
+        {
+          'cipd_package': 'chromium/testing/weblayer-x86',
+          'location': 'weblayer_instrumentation_test_M96',
+          'revision': 'version:96.0.4664.141',
+        }
+      ],
+    },
+  },
+  'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_FIVE_MILESTONE': {
     'args': [
       '--webview-apk-path=apks/SystemWebView.apk',
       '--test-runner-outdir',
@@ -588,7 +732,7 @@
     'skylab': {
       'cros_board': 'atlas',
       'cros_chrome_version': '101.0.4907.0',
-      'cros_img': 'atlas-release/R101-14538.0.0',
+      'cros_img': 'atlas-release/R101-14541.0.0',
     },
     'enabled': True,
     'identifier': 'ATLAS_LKGM',
@@ -624,7 +768,7 @@
     'skylab': {
       'cros_board': 'eve',
       'cros_chrome_version': '101.0.4907.0',
-      'cros_img': 'eve-release/R101-14538.0.0',
+      'cros_img': 'eve-release/R101-14541.0.0',
     },
     'enabled': True,
     'identifier': 'EVE_LKGM',
@@ -660,7 +804,7 @@
     'skylab': {
       'cros_board': 'kevin',
       'cros_chrome_version': '101.0.4907.0',
-      'cros_img': 'kevin-release/R101-14538.0.0',
+      'cros_img': 'kevin-release/R101-14541.0.0',
     },
     'enabled': True,
     'identifier': 'KEVIN_LKGM',
@@ -669,7 +813,7 @@
     'skylab': {
       'cros_board': 'hana',
       'cros_chrome_version': '101.0.4907.0',
-      'cros_img': 'hana-release/R101-14538.0.0',
+      'cros_img': 'hana-release/R101-14541.0.0',
     },
     'enabled': True,
     'identifier': 'HANA_LKGM',
@@ -678,7 +822,7 @@
     'skylab': {
       'cros_board': 'octopus',
       'cros_chrome_version': '101.0.4907.0',
-      'cros_img': 'octopus-release/R101-14538.0.0',
+      'cros_img': 'octopus-release/R101-14541.0.0',
     },
     'enabled': True,
     'identifier': 'OCTOPUS_LKGM',
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 1481b05..c89c083 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -1695,6 +1695,21 @@
             ]
         }
     ],
+    "ClankAppLanguagePrompt": [
+        {
+            "platforms": [
+                "android"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "AppLanguagePrompt"
+                    ]
+                }
+            ]
+        }
+    ],
     "CleanUndecryptablePasswordsLinuxDuringInitialSync": [
         {
             "platforms": [
diff --git a/third_party/blink/public/common/attribution_reporting/constants.h b/third_party/blink/public/common/attribution_reporting/constants.h
index a059dea..a352117f 100644
--- a/third_party/blink/public/common/attribution_reporting/constants.h
+++ b/third_party/blink/public/common/attribution_reporting/constants.h
@@ -14,6 +14,8 @@
 constexpr size_t kMaxBytesPerAttributionAggregatableKeyId = 25;
 constexpr size_t kMaxAttributionAggregatableKeysPerSource = 50;
 
+constexpr size_t kMaxAttributionEventTriggerData = 10;
+
 }  // namespace blink
 
 #endif  // THIRD_PARTY_BLINK_PUBLIC_COMMON_ATTRIBUTION_REPORTING_CONSTANTS_H_
diff --git a/third_party/blink/public/mojom/conversions/attribution_data_host.mojom b/third_party/blink/public/mojom/conversions/attribution_data_host.mojom
index c3d22019..9b4e164 100644
--- a/third_party/blink/public/mojom/conversions/attribution_data_host.mojom
+++ b/third_party/blink/public/mojom/conversions/attribution_data_host.mojom
@@ -61,6 +61,44 @@
   AttributionAggregatableSources aggregatable_sources;
 };
 
+// Deduplication key set by a reporting origin which prevents duplicate triggers
+// from generating multiple attribution reports for a given source.
+struct AttributionTriggerDedupKey {
+  // Arbitrary value for deduplication set by the reporting origin.
+  uint64 value;
+};
+
+// Mojo representation of the trigger configuration provided by a reporting
+// origin. This data is provided arbitrarily by certain subresources on a
+// page which invoke Attribution Reporting.
+struct EventTriggerData {
+  // Value which identifies this trigger in attribution reports, determined by
+  // reporting origin.
+  uint64 data = 0;
+
+  // Priority of this trigger relative to other attributed triggers for a
+  // source. Reports created with high priority triggers will be reported over
+  // lower priority ones.
+  int64 priority = 0;
+
+  // Key which allows deduplication against existing attributions for the same
+  // source.
+  AttributionTriggerDedupKey? dedup_key;
+};
+
+// Represents a request from a reporting origin to trigger attribution on a
+// given site. See:
+// https://github.com/WICG/conversion-measurement-api/blob/main/EVENT.md#triggering-attribution
+struct AttributionTriggerData {
+  // Origin that registered this trigger, used to determine which source this
+  // trigger is associated with.
+  url.mojom.Origin reporting_origin;
+
+  // List of all event trigger data objects declared by the event trigger
+  // header. This data is arbitrarily set by the reporting_origin.
+  array<EventTriggerData> event_triggers;
+};
+
 // Browser-process interface responsible for processing attribution
 // configurations registered by the renderer. These configurations may be sent
 // out of the normal frame lifecycle, e.g. after the frame is destroyed.
@@ -68,4 +106,8 @@
   // Called when data from the renderer is available for a given attributionsrc
   // request.
   SourceDataAvailable(AttributionSourceData data);
-};
+
+  // Called when trigger data from the renderer is available for a given
+  // attributionsrc request.
+  TriggerDataAvailable(AttributionTriggerData data);
+};
\ No newline at end of file
diff --git a/third_party/blink/public/mojom/conversions/conversions.mojom b/third_party/blink/public/mojom/conversions/conversions.mojom
index 33f78e0..3b7d6d7 100644
--- a/third_party/blink/public/mojom/conversions/conversions.mojom
+++ b/third_party/blink/public/mojom/conversions/conversions.mojom
@@ -8,10 +8,6 @@
 import "third_party/blink/public/mojom/conversions/attribution_data_host.mojom";
 import "url/mojom/origin.mojom";
 
-struct DedupKey {
-  uint64 value;
-};
-
 struct Conversion {
   // Origin of the conversion registration redirect.
   url.mojom.Origin reporting_origin;
@@ -30,7 +26,7 @@
 
   // Key specified in conversion redirect for deduplication against existing
   // conversions with the same source.
-  DedupKey? dedup_key;
+  AttributionTriggerDedupKey? dedup_key;
 
   // The request id of the conversion redirect. In case the conversion is
   // invalid and an error is reported to DevTools, the error can be tied to the
diff --git a/third_party/blink/renderer/bindings/generated_in_core.gni b/third_party/blink/renderer/bindings/generated_in_core.gni
index 17bf49ae..b42975ba 100644
--- a/third_party/blink/renderer/bindings/generated_in_core.gni
+++ b/third_party/blink/renderer/bindings/generated_in_core.gni
@@ -25,8 +25,6 @@
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_custom_element_form_disabled_callback.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_custom_element_form_state_restore_callback.cc",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_custom_element_form_state_restore_callback.h",
-  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_document_transition_callback.cc",
-  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_document_transition_callback.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_event_handler_non_null.cc",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_event_handler_non_null.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_for_each_iterator_callback.cc",
@@ -137,8 +135,10 @@
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_custom_layout_constraints_options.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_document_timeline_options.cc",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_document_timeline_options.h",
-  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_document_transition_set_element_options.cc",
-  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_document_transition_set_element_options.h",
+  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_document_transition_prepare_options.cc",
+  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_document_transition_prepare_options.h",
+  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_document_transition_start_options.cc",
+  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_document_transition_start_options.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_dom_matrix_2d_init.cc",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_dom_matrix_2d_init.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_dom_matrix_init.cc",
diff --git a/third_party/blink/renderer/bindings/idl_in_core.gni b/third_party/blink/renderer/bindings/idl_in_core.gni
index 43598ff..c40506e0 100644
--- a/third_party/blink/renderer/bindings/idl_in_core.gni
+++ b/third_party/blink/renderer/bindings/idl_in_core.gni
@@ -126,8 +126,9 @@
           "//third_party/blink/renderer/core/css/style_sheet.idl",
           "//third_party/blink/renderer/core/css/style_sheet_list.idl",
           "//third_party/blink/renderer/core/document_transition/document_transition.idl",
-          "//third_party/blink/renderer/core/document_transition/document_transition_callback.idl",
-          "//third_party/blink/renderer/core/document_transition/document_transition_set_element_options.idl",
+          "//third_party/blink/renderer/core/document_transition/document_transition_config.idl",
+          "//third_party/blink/renderer/core/document_transition/document_transition_prepare_options.idl",
+          "//third_party/blink/renderer/core/document_transition/document_transition_start_options.idl",
           "//third_party/blink/renderer/core/document_transition/document_transition_supplement.idl",
           "//third_party/blink/renderer/core/dom/abort_controller.idl",
           "//third_party/blink/renderer/core/dom/abort_signal.idl",
diff --git a/third_party/blink/renderer/core/display_lock/display_lock_document_state.cc b/third_party/blink/renderer/core/display_lock/display_lock_document_state.cc
index 51b19989..a87ae724 100644
--- a/third_party/blink/renderer/core/display_lock/display_lock_document_state.cc
+++ b/third_party/blink/renderer/core/display_lock/display_lock_document_state.cc
@@ -103,8 +103,8 @@
     // Paint containment requires using the overflow clip edge. To do otherwise
     // results in overflow-clip-margin not being painted in certain scenarios.
     intersection_observer_ = IntersectionObserver::Create(
-        {Length::Percent(150.f)}, {std::numeric_limits<float>::min()},
-        document_,
+        {Length::Percent(kViewportMarginPercentage)},
+        {std::numeric_limits<float>::min()}, document_,
         WTF::BindRepeating(
             &DisplayLockDocumentState::ProcessDisplayLockActivationObservation,
             WrapWeakPersistent(this)),
diff --git a/third_party/blink/renderer/core/display_lock/display_lock_document_state.h b/third_party/blink/renderer/core/display_lock/display_lock_document_state.h
index 4b28969e..412f9966 100644
--- a/third_party/blink/renderer/core/display_lock/display_lock_document_state.h
+++ b/third_party/blink/renderer/core/display_lock/display_lock_document_state.h
@@ -171,6 +171,8 @@
 
   base::TimeTicks GetLockUpdateTimestamp();
 
+  static constexpr float kViewportMarginPercentage = 150.f;
+
  private:
   IntersectionObserver& EnsureIntersectionObserver();
 
diff --git a/third_party/blink/renderer/core/document_transition/document_transition.cc b/third_party/blink/renderer/core/document_transition/document_transition.cc
index 0d327e5..7c69ea8 100644
--- a/third_party/blink/renderer/core/document_transition/document_transition.cc
+++ b/third_party/blink/renderer/core/document_transition/document_transition.cc
@@ -11,13 +11,16 @@
 #include "cc/trees/paint_holding_reason.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_document_transition_config.h"
-#include "third_party/blink/renderer/bindings/core/v8/v8_document_transition_set_element_options.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_document_transition_prepare_options.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_document_transition_start_options.h"
 #include "third_party/blink/renderer/core/css/style_change_reason.h"
+#include "third_party/blink/renderer/core/dom/abort_signal.h"
 #include "third_party/blink/renderer/core/dom/document.h"
 #include "third_party/blink/renderer/core/dom/dom_node_ids.h"
 #include "third_party/blink/renderer/core/dom/pseudo_element.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/inspector/console_message.h"
 #include "third_party/blink/renderer/core/layout/layout_box_model_object.h"
 #include "third_party/blink/renderer/core/layout/layout_view.h"
 #include "third_party/blink/renderer/core/page/chrome_client.h"
@@ -33,15 +36,58 @@
 namespace blink {
 namespace {
 
-const char kAbortedFromCaptureAndHold[] =
-    "Aborted due to captureAndHold() call";
-const char kAbortedFromScript[] = "Aborted due to abort() call";
+const char kAbortedFromPrepare[] = "Aborted due to prepare() call";
+const char kAbortedFromSignal[] = "Aborted due to abortSignal";
+
+DocumentTransitionRequest::Effect ParseEffect(const String& input) {
+  using MapType = HashMap<String, DocumentTransitionRequest::Effect>;
+  DEFINE_STATIC_LOCAL(
+      MapType*, lookup_map,
+      (new MapType{
+          {"cover-down", DocumentTransitionRequest::Effect::kCoverDown},
+          {"cover-left", DocumentTransitionRequest::Effect::kCoverLeft},
+          {"cover-right", DocumentTransitionRequest::Effect::kCoverRight},
+          {"cover-up", DocumentTransitionRequest::Effect::kCoverUp},
+          {"explode", DocumentTransitionRequest::Effect::kExplode},
+          {"fade", DocumentTransitionRequest::Effect::kFade},
+          {"implode", DocumentTransitionRequest::Effect::kImplode},
+          {"reveal-down", DocumentTransitionRequest::Effect::kRevealDown},
+          {"reveal-left", DocumentTransitionRequest::Effect::kRevealLeft},
+          {"reveal-right", DocumentTransitionRequest::Effect::kRevealRight},
+          {"reveal-up", DocumentTransitionRequest::Effect::kRevealUp}}));
+
+  auto it = lookup_map->find(input);
+  return it != lookup_map->end() ? it->value
+                                 : DocumentTransitionRequest::Effect::kNone;
+}
+
+DocumentTransitionRequest::Effect ParseRootTransition(
+    const DocumentTransitionPrepareOptions* options) {
+  return options->hasRootTransition()
+             ? ParseEffect(options->rootTransition())
+             : DocumentTransitionRequest::Effect::kNone;
+}
 
 uint32_t NextDocumentTag() {
   static uint32_t next_document_tag = 1u;
   return next_document_tag++;
 }
 
+DocumentTransitionRequest::TransitionConfig ParseTransitionConfig(
+    const DocumentTransitionConfig& config) {
+  DocumentTransitionRequest::TransitionConfig transition_config;
+
+  if (config.hasDuration()) {
+    transition_config.duration = base::Milliseconds(config.duration());
+  }
+
+  if (config.hasDelay()) {
+    transition_config.delay = base::Milliseconds(config.delay());
+  }
+
+  return transition_config;
+}
+
 }  // namespace
 
 DocumentTransition::DocumentTransition(Document* document)
@@ -51,8 +97,10 @@
 
 void DocumentTransition::Trace(Visitor* visitor) const {
   visitor->Trace(document_);
-  visitor->Trace(capture_promise_resolver_);
+  visitor->Trace(prepare_promise_resolver_);
   visitor->Trace(start_promise_resolver_);
+  visitor->Trace(active_shared_elements_);
+  visitor->Trace(signal_);
   visitor->Trace(style_tracker_);
 
   ScriptWrappable::Trace(visitor);
@@ -61,9 +109,9 @@
 }
 
 void DocumentTransition::ContextDestroyed() {
-  if (capture_promise_resolver_) {
-    capture_promise_resolver_->Detach();
-    capture_promise_resolver_ = nullptr;
+  if (prepare_promise_resolver_) {
+    prepare_promise_resolver_->Detach();
+    prepare_promise_resolver_ = nullptr;
   }
   if (start_promise_resolver_) {
     start_promise_resolver_->Detach();
@@ -73,44 +121,18 @@
 }
 
 bool DocumentTransition::HasPendingActivity() const {
-  if (capture_promise_resolver_ || start_promise_resolver_)
+  if (prepare_promise_resolver_ || start_promise_resolver_)
     return true;
   return false;
 }
 
-void DocumentTransition::AssertNoTransition() {
-  DCHECK_EQ(state_, State::kIdle);
-  DCHECK(!style_tracker_);
-  DCHECK(!capture_promise_resolver_);
-  DCHECK(!start_promise_resolver_);
-}
-
-void DocumentTransition::StartNewTransition() {
-  style_tracker_ =
-      MakeGarbageCollected<DocumentTransitionStyleTracker>(*document_);
-}
-
-void DocumentTransition::FinalizeNewTransition() {}
-
-void DocumentTransition::setElement(
+ScriptPromise DocumentTransition::prepare(
     ScriptState* script_state,
-    Element* element,
-    const AtomicString& tag,
-    const DocumentTransitionSetElementOptions* opts,
-    ExceptionState& exception_state) {
-  DCHECK(style_tracker_);
-  if (tag.IsNull())
-    style_tracker_->RemoveSharedElement(element);
-  else
-    style_tracker_->AddSharedElement(element, tag);
-}
-
-ScriptPromise DocumentTransition::captureAndHold(
-    ScriptState* script_state,
+    const DocumentTransitionPrepareOptions* options,
     ExceptionState& exception_state) {
   // Reject any previous prepare promises.
-  if (state_ == State::kCapturing || state_ == State::kCaptured)
-    CancelPendingTransition(kAbortedFromCaptureAndHold);
+  if (state_ == State::kPreparing || state_ == State::kPrepared)
+    CancelPendingTransition(kAbortedFromPrepare);
 
   // Get the sequence id before any early outs so we will correctly process
   // callbacks from previous requests.
@@ -133,52 +155,148 @@
     return ScriptPromise();
   }
 
-  capture_promise_resolver_ =
+  std::string error;
+  DocumentTransitionRequest::TransitionConfig root_config;
+  if (options->hasRootConfig())
+    root_config = ParseTransitionConfig(*options->rootConfig());
+  if (!root_config.IsValid(&error)) {
+    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
+                                      String(error.data(), error.size()));
+    return ScriptPromise();
+  }
+
+  // This stores a per-shared-element configuration, if specified. Note that
+  // this is likely to change when the API is redesigned at
+  // https://github.com/WICG/shared-element-transitions.
+  //
+  // Note that we add one extra config for the "root" element, after parsing the
+  // shared elements.
+  std::vector<DocumentTransitionRequest::TransitionConfig>
+      shared_elements_config;
+  if (options->hasSharedElements()) {
+    shared_elements_config.resize(options->sharedElements().size());
+
+    // TODO(vmpstr): This is likely to be superceded by CSS customization.
+    if (options->hasSharedElementsConfig()) {
+      const auto& shared_elements_config_options =
+          options->sharedElementsConfig();
+
+      if (shared_elements_config_options.size() !=
+          shared_elements_config.size()) {
+        exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
+                                          "The sharedElementsConfig size must "
+                                          "match the list of shared elements");
+        return ScriptPromise();
+      }
+
+      for (wtf_size_t i = 0; i < shared_elements_config_options.size(); i++) {
+        shared_elements_config[i] =
+            ParseTransitionConfig(*shared_elements_config_options[i]);
+        if (!shared_elements_config[i].IsValid(&error)) {
+          exception_state.ThrowDOMException(
+              DOMExceptionCode::kInvalidStateError,
+              String(error.data(), error.size()));
+          return ScriptPromise();
+        }
+      }
+    }
+  }
+
+  // The root snapshot is handled as a shared element by the compositing stack.
+  shared_elements_config.emplace_back();
+
+  if (options->hasAbortSignal()) {
+    if (options->abortSignal()->aborted()) {
+      exception_state.ThrowDOMException(DOMExceptionCode::kAbortError,
+                                        kAbortedFromSignal);
+      return ScriptPromise();
+    }
+
+    signal_ = options->abortSignal();
+    signal_->AddAlgorithm(WTF::Bind(&DocumentTransition::Abort,
+                                    WrapWeakPersistent(this),
+                                    WrapWeakPersistent(signal_.Get())));
+  }
+
+  // We're going to be creating a new transition, parse the options.
+  auto effect = ParseRootTransition(options);
+  if (options->hasSharedElements())
+    SetActiveSharedElements(options->sharedElements());
+  prepare_shared_element_count_ = active_shared_elements_.size();
+
+  prepare_promise_resolver_ =
       MakeGarbageCollected<ScriptPromiseResolver>(script_state);
 
-  state_ = State::kCapturing;
-  pending_request_ = DocumentTransitionRequest::CreateCapture(
-      document_tag_, style_tracker_->PendingSharedElementCount() + 1,
+  state_ = State::kPreparing;
+  pending_request_ = DocumentTransitionRequest::CreatePrepare(
+      effect, document_tag_, root_config, std::move(shared_elements_config),
       ConvertToBaseOnceCallback(CrossThreadBindOnce(
-          &DocumentTransition::NotifyCaptureFinished,
-          WrapCrossThreadWeakPersistent(this), last_prepare_sequence_id_)));
+          &DocumentTransition::NotifyPrepareFinished,
+          WrapCrossThreadWeakPersistent(this), last_prepare_sequence_id_)),
+      /*is_renderer_transition=*/true);
 
-  style_tracker_->Capture();
+  style_tracker_ =
+      MakeGarbageCollected<DocumentTransitionStyleTracker>(*document_);
+  style_tracker_->Prepare(active_shared_elements_);
+
   NotifyHasChangesToCommit();
-
-  return capture_promise_resolver_->Promise();
+  return prepare_promise_resolver_->Promise();
 }
 
-ScriptPromise DocumentTransition::start(ScriptState* script_state,
-                                        ExceptionState& exception_state) {
-  if (state_ != State::kCaptured) {
+void DocumentTransition::Abort(AbortSignal* signal) {
+  // There is no RemoveAlgorithm() method on AbortSignal so compare the signal
+  // bound to this callback to the one last passed to start().
+  if (signal_ != signal)
+    return;
+
+  CancelPendingTransition(kAbortedFromSignal);
+}
+
+ScriptPromise DocumentTransition::start(
+    ScriptState* script_state,
+    const DocumentTransitionStartOptions* options,
+    ExceptionState& exception_state) {
+  if (state_ != State::kPrepared) {
     exception_state.ThrowDOMException(
         DOMExceptionCode::kInvalidStateError,
         "Transition must be prepared before it can be started.");
     return ScriptPromise();
   }
 
+  signal_ = nullptr;
   StopDeferringCommits();
 
+  if (options->hasSharedElements())
+    SetActiveSharedElements(options->sharedElements());
+
+  // We need to have the same amount of shared elements (even if null) as the
+  // prepared ones.
+  if (prepare_shared_element_count_ != active_shared_elements_.size()) {
+    exception_state.ThrowDOMException(
+        DOMExceptionCode::kInvalidStateError,
+        String::Format("Start request sharedElement count (%u) must match the "
+                       "prepare sharedElement count (%u).",
+                       active_shared_elements_.size(),
+                       prepare_shared_element_count_));
+
+    // TODO(khushalsagar) : Viz keeps copy results cached for 5 seconds at this
+    // point. We should send an early release. See crbug.com/1266500.
+    ResetState();
+    return ScriptPromise();
+  }
+
   last_start_sequence_id_ = next_sequence_id_++;
   state_ = State::kStarted;
   start_promise_resolver_ =
       MakeGarbageCollected<ScriptPromiseResolver>(script_state);
   pending_request_ =
       DocumentTransitionRequest::CreateAnimateRenderer(document_tag_);
-  style_tracker_->Start();
+  style_tracker_->Start(active_shared_elements_);
 
   NotifyHasChangesToCommit();
   return start_promise_resolver_->Promise();
 }
 
-void DocumentTransition::ignoreCSSTaggedElements(ScriptState*,
-                                                 ExceptionState&) {}
-
-void DocumentTransition::abandon(ScriptState*, ExceptionState&) {
-  CancelPendingTransition(kAbortedFromScript);
-}
-
 void DocumentTransition::NotifyHasChangesToCommit() {
   if (!document_ || !document_->GetPage() || !document_->View())
     return;
@@ -191,26 +309,27 @@
   document_->View()->SetPaintArtifactCompositorNeedsUpdate();
 }
 
-void DocumentTransition::NotifyCaptureFinished(uint32_t sequence_id) {
+void DocumentTransition::NotifyPrepareFinished(uint32_t sequence_id) {
   // This notification is for a different sequence id.
   if (sequence_id != last_prepare_sequence_id_)
     return;
 
   // We could have detached the resolver if the execution context was destroyed.
-  if (!capture_promise_resolver_)
+  if (!prepare_promise_resolver_)
     return;
 
-  DCHECK(state_ == State::kCapturing);
-  DCHECK(capture_promise_resolver_);
+  DCHECK(state_ == State::kPreparing);
+  DCHECK(prepare_promise_resolver_);
   if (style_tracker_)
-    style_tracker_->CaptureResolved();
+    style_tracker_->PrepareResolved();
 
   // Defer commits before resolving the promise to ensure any updates made in
   // the callback are deferred.
   StartDeferringCommits();
-  capture_promise_resolver_->Resolve();
-  capture_promise_resolver_ = nullptr;
-  state_ = State::kCaptured;
+  prepare_promise_resolver_->Resolve();
+  prepare_promise_resolver_ = nullptr;
+  state_ = State::kPrepared;
+  SetActiveSharedElements({});
 }
 
 void DocumentTransition::NotifyStartFinished(uint32_t sequence_id) {
@@ -245,10 +364,8 @@
 
 bool DocumentTransition::IsTransitionParticipant(
     const LayoutObject& object) const {
-  // If our state is idle and we're outside of script mutation scope, it implies
-  // that we have no style tracker.
-  DCHECK(state_ != State::kIdle || script_mutations_allowed_ ||
-         !style_tracker_);
+  // If our state is idle it implies that we have no style tracker.
+  DCHECK(state_ != State::kIdle || !style_tracker_);
 
   // The layout view is always a participant if there is a transition.
   if (auto* layout_view = DynamicTo<LayoutView>(object))
@@ -256,7 +373,7 @@
 
   // Otherwise check if the layout object has an active shared element.
   auto* element = DynamicTo<Element>(object.GetNode());
-  return element && style_tracker_ && style_tracker_->IsSharedElement(element);
+  return element && active_shared_elements_.Contains(element);
 }
 
 PaintPropertyChangeType DocumentTransition::UpdateEffect(
@@ -279,17 +396,29 @@
   if (!element) {
     // The only non-element participant is the layout view.
     DCHECK(object.IsLayoutView());
-
-    style_tracker_->UpdateRootIndexAndSnapshotId(
-        state.document_transition_shared_element_id,
-        state.shared_element_resource_id);
+    // This matches one past the size of the shared element configs generated in
+    // ::prepare().
+    state.document_transition_shared_element_id.AddIndex(
+        active_shared_elements_.size());
+    state.shared_element_resource_id = style_tracker_->GetLiveRootSnapshotId();
     DCHECK(state.document_transition_shared_element_id.valid());
     return style_tracker_->UpdateRootEffect(std::move(state), current_effect);
   }
 
-  style_tracker_->UpdateElementIndicesAndSnapshotId(
-      element, state.document_transition_shared_element_id,
-      state.shared_element_resource_id);
+  for (wtf_size_t i = 0; i < active_shared_elements_.size(); ++i) {
+    if (active_shared_elements_[i] != element)
+      continue;
+    state.document_transition_shared_element_id.AddIndex(i);
+
+    // This tags the shared element's content with the resource id used by the
+    // first pseudo element. This is okay since in the eventual API we should
+    // have a 1:1 mapping between shared elements and pseudo elements.
+    if (!state.shared_element_resource_id.IsValid()) {
+      state.shared_element_resource_id =
+          style_tracker_->GetLiveSnapshotId(element);
+    }
+  }
+
   return style_tracker_->UpdateEffect(element, std::move(state),
                                       current_effect);
 }
@@ -305,8 +434,39 @@
 }
 
 void DocumentTransition::VerifySharedElements() {
-  if (state_ != State::kIdle)
-    style_tracker_->VerifySharedElements();
+  for (auto& active_element : active_shared_elements_) {
+    if (!active_element)
+      continue;
+
+    auto* object = active_element->GetLayoutObject();
+
+    // TODO(vmpstr): Should this work for replaced elements as well?
+    if (object) {
+      if (object->ShouldApplyPaintContainment())
+        continue;
+
+      auto* console_message = MakeGarbageCollected<ConsoleMessage>(
+          mojom::ConsoleMessageSource::kRendering,
+          mojom::ConsoleMessageLevel::kError,
+          "Dropping element from transition. Shared element must have "
+          "containt:paint");
+      console_message->SetNodes(document_->GetFrame(),
+                                {DOMNodeIds::IdForNode(active_element)});
+      document_->AddConsoleMessage(console_message);
+    }
+
+    // Clear the shared element. Note that we don't remove the element from the
+    // vector, since we need to preserve the order of the elements and we
+    // support nulls as a valid active element.
+
+    // Invalidate the element since we should no longer be compositing it.
+    auto* box = active_element->GetLayoutBox();
+    if (box && box->HasSelfPaintingLayer()) {
+      box->SetNeedsPaintPropertyUpdate();
+      box->Layer()->SetNeedsCompositingInputsUpdate();
+    }
+    active_element = nullptr;
+  }
 }
 
 void DocumentTransition::RunPostLayoutSteps() {
@@ -361,9 +521,41 @@
 
 const String& DocumentTransition::UAStyleSheet() const {
   DCHECK(style_tracker_);
+
   return style_tracker_->UAStyleSheet();
 }
 
+void DocumentTransition::SetActiveSharedElements(
+    HeapVector<Member<Element>> elements) {
+  // The way this is used, we should never be overriding a non-empty set with
+  // another non-empty set of elements.
+  DCHECK(elements.IsEmpty() || active_shared_elements_.IsEmpty());
+
+  InvalidateActiveElements();
+  active_shared_elements_ = std::move(elements);
+  InvalidateActiveElements();
+}
+
+void DocumentTransition::InvalidateActiveElements() {
+  for (auto& element : active_shared_elements_) {
+    // We allow nulls.
+    if (!element)
+      continue;
+
+    auto* box = element->GetLayoutBox();
+    if (!box || !box->HasSelfPaintingLayer())
+      continue;
+
+    // We propagate the shared element id on an effect node for the object. This
+    // means that we should update the paint properties to update the shared
+    // element id.
+    box->SetNeedsPaintPropertyUpdate();
+
+    // We might need to composite or decomposite this layer.
+    box->Layer()->SetNeedsCompositingInputsUpdate();
+  }
+}
+
 void DocumentTransition::StartDeferringCommits() {
   DCHECK(!deferring_commits_);
 
@@ -401,29 +593,26 @@
 }
 
 void DocumentTransition::CancelPendingTransition(const char* abort_message) {
-  if (capture_promise_resolver_) {
-    capture_promise_resolver_->Reject(MakeGarbageCollected<DOMException>(
+  DCHECK(state_ == State::kPreparing || state_ == State::kPrepared)
+      << "Can not cancel transition at state : " << static_cast<int>(state_);
+
+  if (prepare_promise_resolver_) {
+    prepare_promise_resolver_->Reject(MakeGarbageCollected<DOMException>(
         DOMExceptionCode::kAbortError, abort_message));
-    capture_promise_resolver_ = nullptr;
-  }
-  if (start_promise_resolver_) {
-    start_promise_resolver_->Reject(MakeGarbageCollected<DOMException>(
-        DOMExceptionCode::kAbortError, abort_message));
-    start_promise_resolver_ = nullptr;
+    prepare_promise_resolver_ = nullptr;
   }
 
   ResetState();
 }
 
 void DocumentTransition::ResetState(bool abort_style_tracker) {
+  SetActiveSharedElements({});
   if (style_tracker_ && abort_style_tracker)
     style_tracker_->Abort();
   style_tracker_ = nullptr;
   StopDeferringCommits();
   state_ = State::kIdle;
-  // If script mutations are still allowed, we recreate the style tracker.
-  if (script_mutations_allowed_)
-    StartNewTransition();
+  signal_ = nullptr;
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/document_transition/document_transition.h b/third_party/blink/renderer/core/document_transition/document_transition.h
index edcca73e..17bbbd54 100644
--- a/third_party/blink/renderer/core/document_transition/document_transition.h
+++ b/third_party/blink/renderer/core/document_transition/document_transition.h
@@ -22,8 +22,10 @@
 
 namespace blink {
 
+class AbortSignal;
 class Document;
-class DocumentTransitionSetElementOptions;
+class DocumentTransitionPrepareOptions;
+class DocumentTransitionStartOptions;
 class Element;
 class ExceptionState;
 class LayoutObject;
@@ -51,46 +53,13 @@
   // ActiveScriptWrappable functionality.
   bool HasPendingActivity() const override;
 
-  bool CanCreateNewTransition() const {
-    return state_ == State::kIdle && !script_mutations_allowed_;
-  }
-
-  class ScriptMutationsAllowedScope {
-    STACK_ALLOCATED();
-
-   public:
-    ~ScriptMutationsAllowedScope() {
-      transition_->script_mutations_allowed_ = false;
-      transition_->FinalizeNewTransition();
-    }
-
-   private:
-    friend class DocumentTransition;
-
-    explicit ScriptMutationsAllowedScope(DocumentTransition* transition)
-        : transition_(transition) {
-      transition_->script_mutations_allowed_ = true;
-      transition_->AssertNoTransition();
-      transition_->StartNewTransition();
-    }
-
-    DocumentTransition* transition_;
-  };
-
-  ScriptMutationsAllowedScope CreateScriptMutationsAllowedScope() {
-    return ScriptMutationsAllowedScope{this};
-  }
-
   // JavaScript API implementation.
-  void setElement(ScriptState*,
-                  Element*,
-                  const AtomicString&,
-                  const DocumentTransitionSetElementOptions*,
-                  ExceptionState&);
-  ScriptPromise captureAndHold(ScriptState*, ExceptionState&);
-  ScriptPromise start(ScriptState*, ExceptionState&);
-  void ignoreCSSTaggedElements(ScriptState*, ExceptionState&);
-  void abandon(ScriptState*, ExceptionState&);
+  ScriptPromise prepare(ScriptState*,
+                        const DocumentTransitionPrepareOptions*,
+                        ExceptionState&);
+  ScriptPromise start(ScriptState*,
+                      const DocumentTransitionStartOptions*,
+                      ExceptionState&);
 
   // This uses std::move semantics to take the request from this object.
   std::unique_ptr<DocumentTransitionRequest> TakePendingRequest();
@@ -136,22 +105,21 @@
   // LifecycleNotificationObserver overrides.
   void WillStartLifecycleUpdate(const LocalFrameView&) override;
 
-  bool HasActiveTransition() const { return state_ != State::kIdle; }
-
  private:
   friend class DocumentTransitionTest;
 
-  enum class State { kIdle, kCapturing, kCaptured, kStarted };
-
-  void AssertNoTransition();
-  void StartNewTransition();
-  void FinalizeNewTransition();
+  enum class State { kIdle, kPreparing, kPrepared, kStarted };
 
   void NotifyHasChangesToCommit();
 
-  void NotifyCaptureFinished(uint32_t sequence_id);
+  void NotifyPrepareFinished(uint32_t sequence_id);
   void NotifyStartFinished(uint32_t sequence_id);
 
+  // Sets new active shared elements. Note that this is responsible for making
+  // sure we invalidate the right bits both on the old and new elements.
+  void SetActiveSharedElements(HeapVector<Member<Element>> elements);
+  void InvalidateActiveElements();
+
   // Used to defer visual updates between transition prepare finishing and
   // transition start to allow the page to set up the final scene
   // asynchronously.
@@ -161,6 +129,8 @@
   // Allow canceling a transition until it reaches start().
   void CancelPendingTransition(const char* abort_message);
 
+  void Abort(AbortSignal* signal);
+
   // Resets internal state, called in both abort situations and transition
   // finished situations.
   void ResetState(bool abort_style_tracker = true);
@@ -169,8 +139,20 @@
 
   State state_ = State::kIdle;
 
-  Member<ScriptPromiseResolver> capture_promise_resolver_;
+  Member<ScriptPromiseResolver> prepare_promise_resolver_;
   Member<ScriptPromiseResolver> start_promise_resolver_;
+  Member<AbortSignal> signal_;
+
+  // `active_shared_elements_` represents elements that are identified as shared
+  // during the current step of the transition. Specifically, it represents
+  // `prepare()` call sharedElements if the state is kPreparing and `start()`
+  // call sharedElements if the state is kStarted.
+  // `prepare_shared_element_count_` represents the number of shared elements
+  // that were specified in the `prepare()` call. This is used to verify that
+  // the number of shared elements specified in the `prepare()` and `start()`
+  // calls is the same.
+  HeapVector<Member<Element>> active_shared_elements_;
+  wtf_size_t prepare_shared_element_count_ = 0u;
 
   // Created conditionally if renderer based SharedElementTransitions is
   // enabled.
@@ -192,9 +174,6 @@
 
   bool deferring_commits_ = false;
 
-  // This is set to true when we allow script calls to modify state.
-  bool script_mutations_allowed_ = false;
-
   // Set only for tests.
   bool disable_end_transition_ = false;
 };
diff --git a/third_party/blink/renderer/core/document_transition/document_transition.idl b/third_party/blink/renderer/core/document_transition/document_transition.idl
index 7c304109..e1b8042 100644
--- a/third_party/blink/renderer/core/document_transition/document_transition.idl
+++ b/third_party/blink/renderer/core/document_transition/document_transition.idl
@@ -9,21 +9,13 @@
     Exposed=Window,
     RuntimeEnabled=DocumentTransition
 ] interface DocumentTransition {
-  // Set or unset (if tag is null) an element that will participate in the next
-  // transition, whether as a part of captureAndHold or start phases.
-  [CallWith=ScriptState, RaisesException] void setElement(Element element, DOMString? tag, optional DocumentTransitionSetElementOptions options = {});
+  // - This should only be called after any previous start() calls have resolved.
+  // - Rejects any previous unresolved prepare() promises.
+  // - Returns a promise that resolves after the transition has been
+  //   prepared.
+  [CallWith=ScriptState, RaisesException] Promise<void> prepare(optional DocumentTransitionPrepareOptions options = {});
 
-  // Request to capture the currently set elements, including the root, and
-  // hold visual contents until start is called
-  [CallWith=ScriptState, RaisesException] Promise<void> captureAndHold();
-
-  // Starts the transition with the captured elements and elements set for
-  // start.
-  [CallWith=ScriptState, RaisesException] Promise<void> start();
-
-  // Ignores CSS tagged elements
-  [CallWith=ScriptState, RaisesException] void ignoreCSSTaggedElements();
-
-  // Abandons the transition.
-  [CallWith=ScriptState, RaisesException] void abandon();
+  // Can only be called after prepare(), during the task during
+  // which prepare() most recently resolved.
+  [CallWith=ScriptState, RaisesException] Promise<void> start(optional DocumentTransitionStartOptions options = {});
 };
diff --git a/third_party/blink/renderer/core/document_transition/document_transition_callback.idl b/third_party/blink/renderer/core/document_transition/document_transition_callback.idl
deleted file mode 100644
index bf1e354..0000000
--- a/third_party/blink/renderer/core/document_transition/document_transition_callback.idl
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright 2022 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-[
-  RuntimeEnabled=DocumentTransition
-] callback DocumentTransitionCallback = void(DocumentTransition documentTransition);
-
diff --git a/third_party/blink/renderer/core/document_transition/document_transition_set_element_options.idl b/third_party/blink/renderer/core/document_transition/document_transition_config.idl
similarity index 66%
copy from third_party/blink/renderer/core/document_transition/document_transition_set_element_options.idl
copy to third_party/blink/renderer/core/document_transition/document_transition_config.idl
index 8c7837a..3e404d2 100644
--- a/third_party/blink/renderer/core/document_transition/document_transition_set_element_options.idl
+++ b/third_party/blink/renderer/core/document_transition/document_transition_config.idl
@@ -2,5 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-dictionary DocumentTransitionSetElementOptions {
+dictionary DocumentTransitionConfig {
+  DOMTimeStamp duration;
+  DOMTimeStamp delay;
 };
diff --git a/third_party/blink/renderer/core/document_transition/document_transition_prepare_options.idl b/third_party/blink/renderer/core/document_transition/document_transition_prepare_options.idl
new file mode 100644
index 0000000..c373f3bd
--- /dev/null
+++ b/third_party/blink/renderer/core/document_transition/document_transition_prepare_options.idl
@@ -0,0 +1,30 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+enum RootTransitionType {
+  "none",
+  "cover-down",
+  "cover-left",
+  "cover-right",
+  "cover-up",
+  "explode",
+  "fade",
+  "implode",
+  "reveal-down",
+  "reveal-left",
+  "reveal-right",
+  "reveal-up"
+};
+
+dictionary DocumentTransitionPrepareOptions {
+  RootTransitionType rootTransition;
+  DocumentTransitionConfig rootConfig;
+  sequence<Element?> sharedElements;
+  AbortSignal abortSignal;
+
+  // This config should be folded with the list of |sharedElements| into a
+  // single dictionary. Fix once we have a resolution on API shape :
+  // https://github.com/WICG/shared-element-transitions/issues/2.
+  sequence<DocumentTransitionConfig> sharedElementsConfig;
+};
diff --git a/third_party/blink/renderer/core/document_transition/document_transition_set_element_options.idl b/third_party/blink/renderer/core/document_transition/document_transition_start_options.idl
similarity index 67%
rename from third_party/blink/renderer/core/document_transition/document_transition_set_element_options.idl
rename to third_party/blink/renderer/core/document_transition/document_transition_start_options.idl
index 8c7837a..8bc91eb 100644
--- a/third_party/blink/renderer/core/document_transition/document_transition_set_element_options.idl
+++ b/third_party/blink/renderer/core/document_transition/document_transition_start_options.idl
@@ -2,5 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-dictionary DocumentTransitionSetElementOptions {
+dictionary DocumentTransitionStartOptions {
+  sequence<Element?> sharedElements;
 };
diff --git a/third_party/blink/renderer/core/document_transition/document_transition_style_tracker.cc b/third_party/blink/renderer/core/document_transition/document_transition_style_tracker.cc
index efee5fc..28e99cb 100644
--- a/third_party/blink/renderer/core/document_transition/document_transition_style_tracker.cc
+++ b/third_party/blink/renderer/core/document_transition/document_transition_style_tracker.cc
@@ -15,7 +15,6 @@
 #include "third_party/blink/renderer/core/document_transition/document_transition_utils.h"
 #include "third_party/blink/renderer/core/dom/node.h"
 #include "third_party/blink/renderer/core/dom/pseudo_element.h"
-#include "third_party/blink/renderer/core/inspector/console_message.h"
 #include "third_party/blink/renderer/core/layout/layout_view.h"
 #include "third_party/blink/renderer/core/paint/paint_layer.h"
 #include "third_party/blink/renderer/core/resize_observer/resize_observer_entry.h"
@@ -32,8 +31,6 @@
   return kRootTag;
 }
 
-constexpr int root_index = 0;
-
 const String& StaticUAStyles() {
   DEFINE_STATIC_LOCAL(
       String, kStaticUAStyles,
@@ -48,6 +45,12 @@
   return kAnimationUAStyles;
 }
 
+AtomicString IdFromIndex(wtf_size_t index) {
+  StringBuilder builder;
+  builder.AppendFormat("shared-%d", index);
+  return builder.ToAtomicString();
+}
+
 }  // namespace
 
 class DocumentTransitionStyleTracker::ImageWrapperPseudoElement
@@ -101,108 +104,71 @@
 
 DocumentTransitionStyleTracker::~DocumentTransitionStyleTracker() = default;
 
-void DocumentTransitionStyleTracker::AddSharedElement(Element* element,
-                                                      const AtomicString& tag) {
-  DCHECK(element);
-  // TODO(vmpstr): Log a console warning if we're modifying elements in a state
-  // that does not permit to do so.
-  if (state_ == State::kCapturing || state_ == State::kStarted)
-    return;
-
-  // TODO(vmpstr): One element can have multiple tags associated with it, but
-  // it isn't currently allowed to have one tag be associated with more than one
-  // element. The explainer dictates to abandon the transition. We need to
-  // detect that case and abandon the transition.
-  pending_shared_elements_.push_back(element);
-  pseudo_document_transition_tags_.push_back(tag);
-}
-
-void DocumentTransitionStyleTracker::RemoveSharedElement(Element* element) {
-  // TODO(vmpstr): Log a console warning if we're modifying elements in a state
-  // that does not permit to do so.
-  if (state_ == State::kCapturing || state_ == State::kStarted)
-    return;
-  for (wtf_size_t i = 0; i < pending_shared_elements_.size(); ++i) {
-    if (pending_shared_elements_[i] == element) {
-      pending_shared_elements_.EraseAt(i);
-      pseudo_document_transition_tags_.EraseAt(i);
-    }
-  }
-}
-
-void DocumentTransitionStyleTracker::Capture() {
+void DocumentTransitionStyleTracker::Prepare(
+    const HeapVector<Member<Element>>& old_elements) {
   DCHECK_EQ(state_, State::kIdle);
 
-  state_ = State::kCapturing;
+  state_ = State::kPreparing;
+
+  // An id for each shared element + root.
+  pseudo_document_transition_tags_.resize(old_elements.size() + 1);
 
   // The order of IDs in this list defines the DOM order and as a result the
   // paint order of these elements. This is why root needs to be first in the
   // list.
+  pseudo_document_transition_tags_[0] = RootTag();
   old_root_snapshot_id_ = viz::SharedElementResourceId::Generate();
-  element_data_map_.ReserveCapacityForSize(pending_shared_elements_.size());
-  for (wtf_size_t i = 0; i < pending_shared_elements_.size(); ++i) {
-    const auto& document_transition_tag = pseudo_document_transition_tags_[i];
+  element_data_map_.ReserveCapacityForSize(old_elements.size());
+  for (wtf_size_t i = 0; i < old_elements.size(); ++i) {
+    auto document_transition_tag = IdFromIndex(i);
 
     auto* element_data = MakeGarbageCollected<ElementData>();
-    element_data->target_element = pending_shared_elements_[i];
-    DCHECK_NE(root_index, static_cast<int>(i + 1));
-    element_data->element_index = i + 1;
-    if (pending_shared_elements_[i])
+    element_data->target_element = old_elements[i];
+    if (old_elements[i])
       element_data->old_snapshot_id = viz::SharedElementResourceId::Generate();
     element_data_map_.insert(document_transition_tag, std::move(element_data));
+
+    pseudo_document_transition_tags_[i + 1] =
+        std::move(document_transition_tag);
   }
 
-  // TODO(vmpstr): This is a bit awkward. push/set/pop
-  pseudo_document_transition_tags_.push_front(RootTag());
   document_->GetStyleEngine().SetDocumentTransitionTags(
       pseudo_document_transition_tags_);
-  pseudo_document_transition_tags_.EraseAt(0);
 
   // We need a style invalidation to generate the pseudo element tree.
   InvalidateStyle();
 }
 
-void DocumentTransitionStyleTracker::CaptureResolved() {
-  DCHECK_EQ(state_, State::kCapturing);
+void DocumentTransitionStyleTracker::PrepareResolved() {
+  DCHECK_EQ(state_, State::kPreparing);
 
-  state_ = State::kCaptured;
-
-  // Since the elements will be unset, we need to invalidate their style first.
-  // TODO(vmpstr): We don't have to invalidate the pseudo styles at this point,
-  // just the shared elements. We can split InvalidateStyle() into two functions
-  // as an optimization.
-  InvalidateStyle();
+  state_ = State::kPrepared;
 
   for (auto& entry : element_data_map_) {
     auto& element_data = entry.value;
     element_data->target_element = nullptr;
     element_data->cached_border_box_size = element_data->border_box_size;
     element_data->cached_viewport_matrix = element_data->viewport_matrix;
-    element_data->effect_node = nullptr;
   }
-  root_effect_node_ = nullptr;
 }
 
-void DocumentTransitionStyleTracker::Start() {
-  DCHECK_EQ(state_, State::kCaptured);
+void DocumentTransitionStyleTracker::Start(
+    const HeapVector<Member<Element>>& new_elements) {
+  DCHECK_EQ(state_, State::kPrepared);
+  DCHECK_EQ(element_data_map_.size(), new_elements.size());
 
   state_ = State::kStarted;
   new_root_snapshot_id_ = viz::SharedElementResourceId::Generate();
-  for (wtf_size_t i = 0; i < pending_shared_elements_.size(); ++i) {
-    const auto& document_transition_tag = pseudo_document_transition_tags_[i];
-
-    // TODO(vmpstr): Support new elements during start. It requires us to figure
-    // out what the new document tag set is as well as creating new element
-    // data.
-    if (element_data_map_.find(document_transition_tag) ==
-        element_data_map_.end())
-      continue;
+  for (wtf_size_t i = 0; i < new_elements.size(); ++i) {
+    auto document_transition_tag = IdFromIndex(i);
 
     auto& element_data = element_data_map_.find(document_transition_tag)->value;
-    element_data->target_element = pending_shared_elements_[i];
-    if (pending_shared_elements_[i])
+    element_data->target_element = new_elements[i];
+    if (new_elements[i])
       element_data->new_snapshot_id = viz::SharedElementResourceId::Generate();
+    element_data->effect_node = nullptr;
   }
+  root_effect_node_ = nullptr;
 
   // We need a style invalidation to generate new content pseudo elements for
   // new elements in the DOM.
@@ -223,39 +189,32 @@
 
   element_data_map_.clear();
   pseudo_document_transition_tags_.clear();
-  pending_shared_elements_.clear();
   document_->GetStyleEngine().SetDocumentTransitionTags({});
 
   // We need a style invalidation to remove the pseudo element tree.
   InvalidateStyle();
 }
 
-void DocumentTransitionStyleTracker::UpdateElementIndicesAndSnapshotId(
-    Element* element,
-    DocumentTransitionSharedElementId& index,
-    viz::SharedElementResourceId& resource_id) const {
+viz::SharedElementResourceId DocumentTransitionStyleTracker::GetLiveSnapshotId(
+    const Element* element) const {
   DCHECK(element);
 
   for (const auto& entry : element_data_map_) {
     if (entry.value->target_element == element) {
-      index.AddIndex(entry.value->element_index);
-      resource_id = HasLiveNewContent() ? entry.value->new_snapshot_id
-                                        : entry.value->old_snapshot_id;
-      DCHECK(resource_id.IsValid());
-      return;
+      auto snapshot_id = HasLiveNewContent() ? entry.value->new_snapshot_id
+                                             : entry.value->old_snapshot_id;
+      DCHECK(snapshot_id.IsValid());
+      return snapshot_id;
     }
   }
 
   NOTREACHED();
+  return viz::SharedElementResourceId();
 }
 
-void DocumentTransitionStyleTracker::UpdateRootIndexAndSnapshotId(
-    DocumentTransitionSharedElementId& index,
-    viz::SharedElementResourceId& resource_id) const {
-  index.AddIndex(root_index);
-  resource_id =
-      HasLiveNewContent() ? new_root_snapshot_id_ : old_root_snapshot_id_;
-  DCHECK(resource_id.IsValid());
+viz::SharedElementResourceId
+DocumentTransitionStyleTracker::GetLiveRootSnapshotId() const {
+  return HasLiveNewContent() ? new_root_snapshot_id_ : old_root_snapshot_id_;
 }
 
 PseudoElement* DocumentTransitionStyleTracker::CreatePseudoElement(
@@ -467,56 +426,6 @@
   return root_effect_node_.get();
 }
 
-void DocumentTransitionStyleTracker::VerifySharedElements() {
-  for (auto& entry : element_data_map_) {
-    auto& element_data = entry.value;
-    if (!element_data->target_element)
-      continue;
-    auto& active_element = element_data->target_element;
-
-    auto* object = active_element->GetLayoutObject();
-
-    // TODO(vmpstr): Should this work for replaced elements as well?
-    if (object) {
-      if (object->ShouldApplyPaintContainment())
-        continue;
-
-      auto* console_message = MakeGarbageCollected<ConsoleMessage>(
-          mojom::blink::ConsoleMessageSource::kRendering,
-          mojom::blink::ConsoleMessageLevel::kError,
-          "Dropping element from transition. Shared element must have "
-          "containt:paint");
-      console_message->SetNodes(document_->GetFrame(),
-                                {DOMNodeIds::IdForNode(active_element)});
-      document_->AddConsoleMessage(console_message);
-    }
-
-    // Clear the shared element. Note that we don't remove the element from the
-    // vector, since we need to preserve the order of the elements and we
-    // support nulls as a valid active element.
-
-    // Invalidate the element since we should no longer be compositing it.
-    auto* box = active_element->GetLayoutBox();
-    if (box && box->HasSelfPaintingLayer()) {
-      box->SetNeedsPaintPropertyUpdate();
-      box->Layer()->SetNeedsCompositingInputsUpdate();
-    }
-    active_element = nullptr;
-  }
-}
-
-bool DocumentTransitionStyleTracker::IsSharedElement(Element* element) const {
-  // In stable states, we don't have shared elements.
-  if (state_ == State::kIdle || state_ == State::kCaptured)
-    return false;
-
-  for (auto& entry : element_data_map_) {
-    if (entry.value->target_element == element)
-      return true;
-  }
-  return false;
-}
-
 void DocumentTransitionStyleTracker::InvalidateStyle() {
   ua_style_sheet_.reset();
   document_->GetStyleEngine().InvalidateUADocumentTransitionStyle();
@@ -540,25 +449,13 @@
     if (layout_view->HasSelfPaintingLayer())
       layout_view->Layer()->SetNeedsCompositingInputsUpdate();
   }
-
   for (auto& entry : element_data_map_) {
     if (!entry.value->target_element)
       continue;
     auto* object = entry.value->target_element->GetLayoutObject();
     if (!object)
       continue;
-
-    // We propagate the shared element id on an effect node for the object. This
-    // means that we should update the paint properties to update the shared
-    // element id.
     object->SetNeedsPaintPropertyUpdate();
-
-    auto* box = entry.value->target_element->GetLayoutBox();
-    if (!box || !box->HasSelfPaintingLayer())
-      continue;
-
-    // We might need to composite or decomposite this layer.
-    box->Layer()->SetNeedsCompositingInputsUpdate();
   }
 }
 
@@ -646,7 +543,6 @@
 void DocumentTransitionStyleTracker::Trace(Visitor* visitor) const {
   visitor->Trace(document_);
   visitor->Trace(element_data_map_);
-  visitor->Trace(pending_shared_elements_);
 }
 
 void DocumentTransitionStyleTracker::ElementData::Trace(
diff --git a/third_party/blink/renderer/core/document_transition/document_transition_style_tracker.h b/third_party/blink/renderer/core/document_transition/document_transition_style_tracker.h
index 3b22be9..3e0a759 100644
--- a/third_party/blink/renderer/core/document_transition/document_transition_style_tracker.h
+++ b/third_party/blink/renderer/core/document_transition/document_transition_style_tracker.h
@@ -9,7 +9,6 @@
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/dom/element.h"
 #include "third_party/blink/renderer/platform/geometry/layout_size.h"
-#include "third_party/blink/renderer/platform/graphics/document_transition_shared_element_id.h"
 #include "third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_hash_map.h"
 #include "third_party/blink/renderer/platform/transforms/transformation_matrix.h"
@@ -37,20 +36,19 @@
   explicit DocumentTransitionStyleTracker(Document& document);
   ~DocumentTransitionStyleTracker();
 
-  void AddSharedElement(Element*, const AtomicString&);
-  void RemoveSharedElement(Element*);
-
-  // Notifies when the transition is initiated.
-  void Capture();
+  // Notifies when the transition is initiated. |elements| is the set of shared
+  // elements in the old DOM.
+  void Prepare(const HeapVector<Member<Element>>& old_elements);
 
   // Notifies when caching snapshots for elements in the old DOM finishes. This
   // is dispatched before script is notified to ensure this class releases any
   // references to elements in the old DOM before it is mutated by script.
-  void CaptureResolved();
+  void PrepareResolved();
 
   // Notifies when the new DOM has finished loading and a transition can be
-  // started.
-  void Start();
+  // started. |elements| is the set of shared elements in the new DOM paired
+  // sequentially with the list of |elements| in the Prepare call.
+  void Start(const HeapVector<Member<Element>>& new_elements);
 
   // Notifies when the animation setup for the transition during Start have
   // finished executing.
@@ -60,12 +58,13 @@
   // is initiated.
   void Abort();
 
-  void UpdateRootIndexAndSnapshotId(DocumentTransitionSharedElementId&,
-                                    viz::SharedElementResourceId&) const;
+  // Returns the resource id that |element| should be tagged with. This
+  // |element| must be a shared element in the current DOM (specified in Prepare
+  // or Start).
+  viz::SharedElementResourceId GetLiveSnapshotId(const Element* element) const;
 
-  void UpdateElementIndicesAndSnapshotId(Element*,
-                                         DocumentTransitionSharedElementId&,
-                                         viz::SharedElementResourceId&) const;
+  // Returns the resource id for the root stacking context.
+  viz::SharedElementResourceId GetLiveRootSnapshotId() const;
 
   // Creates a PseudoElement for the corresponding |pseudo_id| and
   // |document_transition_tag|. The |pseudo_id| must be a ::transition* element.
@@ -100,20 +99,12 @@
   EffectPaintPropertyNode* GetEffect(Element* element) const;
   EffectPaintPropertyNode* GetRootEffect() const;
 
-  void VerifySharedElements();
-
-  int PendingSharedElementCount() const {
-    return pending_shared_elements_.size();
-  }
-
-  bool IsSharedElement(Element* element) const;
-
  private:
   class ImageWrapperPseudoElement;
 
   // These state transitions are executed in a serial order unless the
   // transition is aborted.
-  enum class State { kIdle, kCapturing, kCaptured, kStarted, kFinished };
+  enum class State { kIdle, kPreparing, kPrepared, kStarted, kFinished };
 
   struct ElementData : public GarbageCollected<ElementData> {
     void Trace(Visitor* visitor) const;
@@ -140,9 +131,6 @@
     // An effect used to represent the `target_element`'s contents, including
     // any of element's own effects, in a pseudo element layer.
     scoped_refptr<EffectPaintPropertyNode> effect_node;
-
-    // Index to add to the document transition shared element id.
-    int element_index;
   };
 
   void InvalidateStyle();
@@ -151,8 +139,7 @@
 
   Member<Document> document_;
   State state_ = State::kIdle;
-  VectorOf<AtomicString> pseudo_document_transition_tags_;
-  VectorOf<Element> pending_shared_elements_;
+  Vector<AtomicString> pseudo_document_transition_tags_;
   HeapHashMap<AtomicString, Member<ElementData>> element_data_map_;
   viz::SharedElementResourceId old_root_snapshot_id_;
   viz::SharedElementResourceId new_root_snapshot_id_;
diff --git a/third_party/blink/renderer/core/document_transition/document_transition_supplement.cc b/third_party/blink/renderer/core/document_transition/document_transition_supplement.cc
index 456b39f..d7349c6b 100644
--- a/third_party/blink/renderer/core/document_transition/document_transition_supplement.cc
+++ b/third_party/blink/renderer/core/document_transition/document_transition_supplement.cc
@@ -5,7 +5,6 @@
 #include "third_party/blink/renderer/core/document_transition/document_transition_supplement.h"
 
 #include "cc/document_transition/document_transition_request.h"
-#include "third_party/blink/renderer/bindings/core/v8/v8_document_transition_callback.h"
 #include "third_party/blink/renderer/core/document_transition/document_transition.h"
 #include "third_party/blink/renderer/core/dom/document.h"
 
@@ -34,25 +33,13 @@
 }
 
 // static
-DocumentTransition* DocumentTransitionSupplement::EnsureDocumentTransition(
+DocumentTransition* DocumentTransitionSupplement::documentTransition(
     Document& document) {
   auto* supplement = From(document);
   DCHECK(supplement->GetTransition());
   return supplement->GetTransition();
 }
 
-// static
-void DocumentTransitionSupplement::createDocumentTransition(
-    Document& document,
-    V8DocumentTransitionCallback* callback) {
-  auto* transition = EnsureDocumentTransition(document);
-  // TODO(vmpstr): We need to figure what to do if we already have a transition.
-  if (transition->HasActiveTransition())
-    return;
-  auto script_scope = transition->CreateScriptMutationsAllowedScope();
-  callback->InvokeAndReportException(&document, transition);
-}
-
 DocumentTransition* DocumentTransitionSupplement::GetTransition() {
   return transition_;
 }
diff --git a/third_party/blink/renderer/core/document_transition/document_transition_supplement.h b/third_party/blink/renderer/core/document_transition/document_transition_supplement.h
index a18d0a3..c998dbcb 100644
--- a/third_party/blink/renderer/core/document_transition/document_transition_supplement.h
+++ b/third_party/blink/renderer/core/document_transition/document_transition_supplement.h
@@ -12,7 +12,6 @@
 
 namespace blink {
 class DocumentTransition;
-class V8DocumentTransitionCallback;
 
 class CORE_EXPORT DocumentTransitionSupplement
     : public GarbageCollected<DocumentTransitionSupplement>,
@@ -24,10 +23,7 @@
   static DocumentTransitionSupplement* From(Document&);
   static DocumentTransitionSupplement* FromIfExists(Document&);
 
-  static DocumentTransition* EnsureDocumentTransition(Document&);
-
-  static void createDocumentTransition(Document&,
-                                       V8DocumentTransitionCallback* callback);
+  static DocumentTransition* documentTransition(Document&);
 
   DocumentTransition* GetTransition();
 
diff --git a/third_party/blink/renderer/core/document_transition/document_transition_supplement.idl b/third_party/blink/renderer/core/document_transition/document_transition_supplement.idl
index 7d9578dd..ff7541ff 100644
--- a/third_party/blink/renderer/core/document_transition/document_transition_supplement.idl
+++ b/third_party/blink/renderer/core/document_transition/document_transition_supplement.idl
@@ -6,5 +6,5 @@
     ImplementedAs=DocumentTransitionSupplement,
     RuntimeEnabled=DocumentTransition
 ] partial interface Document {
-    [MeasureAs=DocumentTransition] void createDocumentTransition(DocumentTransitionCallback callback);
+    [SameObject, MeasureAs=DocumentTransition] readonly attribute DocumentTransition documentTransition;
 };
diff --git a/third_party/blink/renderer/core/document_transition/document_transition_test.cc b/third_party/blink/renderer/core/document_transition/document_transition_test.cc
index 3eb3827..8e1d0dd 100644
--- a/third_party/blink/renderer/core/document_transition/document_transition_test.cc
+++ b/third_party/blink/renderer/core/document_transition/document_transition_test.cc
@@ -12,7 +12,8 @@
 #include "third_party/blink/public/web/web_settings.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
-#include "third_party/blink/renderer/bindings/core/v8/v8_document_transition_set_element_options.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_document_transition_prepare_options.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_document_transition_start_options.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_root_transition_type.h"
 #include "third_party/blink/renderer/core/css/style_change_reason.h"
 #include "third_party/blink/renderer/core/css/style_engine.h"
@@ -108,14 +109,14 @@
 
   void FinishTransition() {
     auto* transition =
-        DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
+        DocumentTransitionSupplement::documentTransition(GetDocument());
     transition->NotifyStartFinished(transition->last_start_sequence_id_);
   }
 
   bool ShouldCompositeForDocumentTransition(Element* e) {
     auto* layout_object = e->GetLayoutObject();
     auto* transition =
-        DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
+        DocumentTransitionSupplement::documentTransition(GetDocument());
     return layout_object && transition &&
            transition->IsTransitionParticipant(*layout_object);
   }
@@ -174,9 +175,9 @@
 
 TEST_P(DocumentTransitionTest, TransitionObjectPersists) {
   auto* first_transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
+      DocumentTransitionSupplement::documentTransition(GetDocument());
   auto* second_transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
+      DocumentTransitionSupplement::documentTransition(GetDocument());
 
   EXPECT_TRUE(first_transition);
   EXPECT_EQ(GetState(first_transition), State::kIdle);
@@ -185,9 +186,9 @@
 }
 
 TEST_P(DocumentTransitionTest, TransitionPreparePromiseResolves) {
+  DocumentTransitionPrepareOptions options;
   auto* transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
-  auto scope = transition->CreateScriptMutationsAllowedScope();
+      DocumentTransitionSupplement::documentTransition(GetDocument());
   ASSERT_TRUE(transition);
   EXPECT_EQ(GetState(transition), State::kIdle);
 
@@ -196,32 +197,35 @@
   ExceptionState& exception_state = v8_scope.GetExceptionState();
 
   ScriptPromiseTester promise_tester(
-      script_state, transition->captureAndHold(script_state, exception_state));
+      script_state,
+      transition->prepare(script_state, &options, exception_state));
 
-  EXPECT_EQ(GetState(transition), State::kCapturing);
+  EXPECT_EQ(GetState(transition), State::kPreparing);
   UpdateAllLifecyclePhasesAndFinishDirectives();
   promise_tester.WaitUntilSettled();
 
   EXPECT_TRUE(promise_tester.IsFulfilled());
-  EXPECT_EQ(GetState(transition), State::kCaptured);
+  EXPECT_EQ(GetState(transition), State::kPrepared);
 }
 
 TEST_P(DocumentTransitionTest, AdditionalPrepareRejectsPreviousPromise) {
   auto* transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
-  auto scope = transition->CreateScriptMutationsAllowedScope();
+      DocumentTransitionSupplement::documentTransition(GetDocument());
 
   V8TestingScope v8_scope;
   ScriptState* script_state = v8_scope.GetScriptState();
   ExceptionState& exception_state = v8_scope.GetExceptionState();
 
+  DocumentTransitionPrepareOptions options;
   ScriptPromiseTester first_promise_tester(
-      script_state, transition->captureAndHold(script_state, exception_state));
-  EXPECT_EQ(GetState(transition), State::kCapturing);
+      script_state,
+      transition->prepare(script_state, &options, exception_state));
+  EXPECT_EQ(GetState(transition), State::kPreparing);
 
   ScriptPromiseTester second_promise_tester(
-      script_state, transition->captureAndHold(script_state, exception_state));
-  EXPECT_EQ(GetState(transition), State::kCapturing);
+      script_state,
+      transition->prepare(script_state, &options, exception_state));
+  EXPECT_EQ(GetState(transition), State::kPreparing);
 
   UpdateAllLifecyclePhasesAndFinishDirectives();
   first_promise_tester.WaitUntilSettled();
@@ -229,7 +233,37 @@
 
   EXPECT_TRUE(first_promise_tester.IsRejected());
   EXPECT_TRUE(second_promise_tester.IsFulfilled());
-  EXPECT_EQ(GetState(transition), State::kCaptured);
+  EXPECT_EQ(GetState(transition), State::kPrepared);
+}
+
+TEST_P(DocumentTransitionTest, EffectParsing) {
+  // Test default init.
+  auto* transition =
+      DocumentTransitionSupplement::documentTransition(GetDocument());
+
+  V8TestingScope v8_scope;
+  ScriptState* script_state = v8_scope.GetScriptState();
+  ExceptionState& exception_state = v8_scope.GetExceptionState();
+  DocumentTransitionPrepareOptions default_options;
+  transition->prepare(script_state, &default_options, exception_state);
+
+  auto request = transition->TakePendingRequest();
+  ASSERT_TRUE(request);
+
+  auto directive = request->ConstructDirective({});
+  EXPECT_EQ(directive.effect(), DocumentTransitionRequest::Effect::kNone);
+
+  // Test "explode" effect parsing.
+  DocumentTransitionPrepareOptions explode_options;
+  explode_options.setRootTransition(
+      V8RootTransitionType(V8RootTransitionType::Enum::kExplode));
+  transition->prepare(script_state, &explode_options, exception_state);
+
+  request = transition->TakePendingRequest();
+  ASSERT_TRUE(request);
+
+  directive = request->ConstructDirective({});
+  EXPECT_EQ(directive.effect(), DocumentTransitionRequest::Effect::kExplode);
 }
 
 TEST_P(DocumentTransitionTest, PrepareSharedElementsWantToBeComposited) {
@@ -248,8 +282,7 @@
   auto* e3 = GetDocument().getElementById("e3");
 
   auto* transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
-  auto scope = transition->CreateScriptMutationsAllowedScope();
+      DocumentTransitionSupplement::documentTransition(GetDocument());
 
   V8TestingScope v8_scope;
   ScriptState* script_state = v8_scope.GetScriptState();
@@ -259,9 +292,10 @@
   EXPECT_FALSE(ShouldCompositeForDocumentTransition(e2));
   EXPECT_FALSE(ShouldCompositeForDocumentTransition(e3));
 
-  transition->setElement(script_state, e1, "e1", nullptr, exception_state);
-  transition->setElement(script_state, e3, "e3", nullptr, exception_state);
-  transition->captureAndHold(script_state, exception_state);
+  DocumentTransitionPrepareOptions options;
+  // Set two of the elements to be shared.
+  options.setSharedElements({e1, e3});
+  transition->prepare(script_state, &options, exception_state);
 
   // Update the lifecycle while keeping the transition active.
   UpdateAllLifecyclePhasesForTest();
@@ -302,8 +336,7 @@
   auto* e3 = GetDocument().getElementById("e3");
 
   auto* transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
-  auto scope = transition->CreateScriptMutationsAllowedScope();
+      DocumentTransitionSupplement::documentTransition(GetDocument());
 
   V8TestingScope v8_scope;
   ScriptState* script_state = v8_scope.GetScriptState();
@@ -313,10 +346,9 @@
   EXPECT_FALSE(ShouldCompositeForDocumentTransition(e2));
   EXPECT_FALSE(ShouldCompositeForDocumentTransition(e3));
 
-  transition->setElement(script_state, e1, "e1", nullptr, exception_state);
-  transition->setElement(script_state, e2, "e2", nullptr, exception_state);
-  transition->setElement(script_state, e3, "e3", nullptr, exception_state);
-  transition->captureAndHold(script_state, exception_state);
+  DocumentTransitionPrepareOptions options;
+  options.setSharedElements({e1, e2, e3});
+  transition->prepare(script_state, &options, exception_state);
 
   EXPECT_TRUE(ShouldCompositeForDocumentTransition(e1));
   EXPECT_TRUE(ShouldCompositeForDocumentTransition(e2));
@@ -335,6 +367,44 @@
   EXPECT_FALSE(ElementIsComposited("e3"));
 }
 
+TEST_P(DocumentTransitionTest, StartSharedElementCountMismatch) {
+  SetHtmlInnerHTML(R"HTML(
+    <div id=e1></div>
+    <div id=e2></div>
+    <div id=e3></div>
+  )HTML");
+
+  auto* e1 = GetDocument().getElementById("e1");
+  auto* e2 = GetDocument().getElementById("e2");
+  auto* e3 = GetDocument().getElementById("e3");
+
+  auto* transition =
+      DocumentTransitionSupplement::documentTransition(GetDocument());
+
+  V8TestingScope v8_scope;
+  ScriptState* script_state = v8_scope.GetScriptState();
+  ExceptionState& exception_state = v8_scope.GetExceptionState();
+
+  DocumentTransitionPrepareOptions prepare_options;
+  // Set two of the elements to be shared.
+  prepare_options.setSharedElements({e1, e3});
+  transition->prepare(script_state, &prepare_options, exception_state);
+
+  UpdateAllLifecyclePhasesAndFinishDirectives();
+
+  DocumentTransitionStartOptions start_options;
+  // Set all of the elements as shared. This should cause an exception.
+  start_options.setSharedElements({e1, e2, e3});
+
+  EXPECT_FALSE(exception_state.HadException());
+  transition->start(script_state, &start_options, exception_state);
+  EXPECT_TRUE(exception_state.HadException());
+
+  EXPECT_FALSE(ShouldCompositeForDocumentTransition(e1));
+  EXPECT_FALSE(ShouldCompositeForDocumentTransition(e2));
+  EXPECT_FALSE(ShouldCompositeForDocumentTransition(e3));
+}
+
 TEST_P(DocumentTransitionTest, StartSharedElementsWantToBeComposited) {
   SetHtmlInnerHTML(R"HTML(
     <div id=e1></div>
@@ -347,17 +417,16 @@
   auto* e3 = GetDocument().getElementById("e3");
 
   auto* transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
-  auto scope = transition->CreateScriptMutationsAllowedScope();
+      DocumentTransitionSupplement::documentTransition(GetDocument());
 
   V8TestingScope v8_scope;
   ScriptState* script_state = v8_scope.GetScriptState();
   ExceptionState& exception_state = v8_scope.GetExceptionState();
 
+  DocumentTransitionPrepareOptions prepare_options;
   // Set two of the elements to be shared.
-  transition->setElement(script_state, e1, "e1", nullptr, exception_state);
-  transition->setElement(script_state, e3, "e3", nullptr, exception_state);
-  transition->captureAndHold(script_state, exception_state);
+  prepare_options.setSharedElements({e1, e3});
+  transition->prepare(script_state, &prepare_options, exception_state);
 
   EXPECT_TRUE(ShouldCompositeForDocumentTransition(e1));
   EXPECT_FALSE(ShouldCompositeForDocumentTransition(e2));
@@ -365,14 +434,10 @@
 
   UpdateAllLifecyclePhasesAndFinishDirectives();
 
+  DocumentTransitionStartOptions start_options;
   // Set two different elements as shared.
-  // Unset e3.
-  transition->setElement(script_state, e3, AtomicString(), nullptr,
-                         exception_state);
-  // Set e2 to be the same tag as "e3".
-  // TODO(vmpstr): We should be able to support new tags for entry transitions.
-  transition->setElement(script_state, e2, "e3", nullptr, exception_state);
-  transition->start(script_state, exception_state);
+  start_options.setSharedElements({e1, e2});
+  transition->start(script_state, &start_options, exception_state);
 
   EXPECT_TRUE(ShouldCompositeForDocumentTransition(e1));
   EXPECT_TRUE(ShouldCompositeForDocumentTransition(e2));
@@ -387,30 +452,32 @@
 
 TEST_P(DocumentTransitionTest, AdditionalPrepareAfterPreparedSucceeds) {
   auto* transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
-  auto scope = transition->CreateScriptMutationsAllowedScope();
+      DocumentTransitionSupplement::documentTransition(GetDocument());
 
   V8TestingScope v8_scope;
   ScriptState* script_state = v8_scope.GetScriptState();
   ExceptionState& exception_state = v8_scope.GetExceptionState();
 
+  DocumentTransitionPrepareOptions options;
   ScriptPromiseTester first_promise_tester(
-      script_state, transition->captureAndHold(script_state, exception_state));
-  EXPECT_EQ(GetState(transition), State::kCapturing);
+      script_state,
+      transition->prepare(script_state, &options, exception_state));
+  EXPECT_EQ(GetState(transition), State::kPreparing);
 
   UpdateAllLifecyclePhasesAndFinishDirectives();
   first_promise_tester.WaitUntilSettled();
   EXPECT_TRUE(first_promise_tester.IsFulfilled());
-  EXPECT_EQ(GetState(transition), State::kCaptured);
+  EXPECT_EQ(GetState(transition), State::kPrepared);
 
   ScriptPromiseTester second_promise_tester(
-      script_state, transition->captureAndHold(script_state, exception_state));
-  EXPECT_EQ(GetState(transition), State::kCapturing);
+      script_state,
+      transition->prepare(script_state, &options, exception_state));
+  EXPECT_EQ(GetState(transition), State::kPreparing);
 
   UpdateAllLifecyclePhasesAndFinishDirectives();
   second_promise_tester.WaitUntilSettled();
   EXPECT_TRUE(second_promise_tester.IsFulfilled());
-  EXPECT_EQ(GetState(transition), State::kCaptured);
+  EXPECT_EQ(GetState(transition), State::kPrepared);
 }
 
 TEST_P(DocumentTransitionTest, TransitionCleanedUpBeforePromiseResolution) {
@@ -418,11 +485,11 @@
   ScriptState* script_state = v8_scope.GetScriptState();
   ExceptionState& exception_state = v8_scope.GetExceptionState();
 
-  auto* transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
-  auto scope = transition->CreateScriptMutationsAllowedScope();
+  DocumentTransitionPrepareOptions options;
   ScriptPromiseTester tester(
-      script_state, transition->captureAndHold(script_state, exception_state));
+      script_state,
+      DocumentTransitionSupplement::documentTransition(GetDocument())
+          ->prepare(script_state, &options, exception_state));
 
   // ActiveScriptWrappable should keep the transition alive.
   ThreadState::Current()->CollectAllGarbageForTesting();
@@ -434,8 +501,7 @@
 
 TEST_P(DocumentTransitionTest, StartHasNoEffectUnlessPrepared) {
   auto* transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
-  auto scope = transition->CreateScriptMutationsAllowedScope();
+      DocumentTransitionSupplement::documentTransition(GetDocument());
   EXPECT_EQ(GetState(transition), State::kIdle);
   EXPECT_FALSE(transition->TakePendingRequest());
 
@@ -443,7 +509,8 @@
   ScriptState* script_state = v8_scope.GetScriptState();
   ExceptionState& exception_state = v8_scope.GetExceptionState();
 
-  transition->start(script_state, exception_state);
+  DocumentTransitionStartOptions options;
+  transition->start(script_state, &options, exception_state);
   EXPECT_EQ(GetState(transition), State::kIdle);
   EXPECT_FALSE(transition->TakePendingRequest());
   EXPECT_TRUE(exception_state.HadException());
@@ -451,24 +518,27 @@
 
 TEST_P(DocumentTransitionTest, StartAfterPrepare) {
   auto* transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
-  auto scope = transition->CreateScriptMutationsAllowedScope();
+      DocumentTransitionSupplement::documentTransition(GetDocument());
 
   V8TestingScope v8_scope;
   ScriptState* script_state = v8_scope.GetScriptState();
   ExceptionState& exception_state = v8_scope.GetExceptionState();
 
-  ScriptPromiseTester capture_tester(
-      script_state, transition->captureAndHold(script_state, exception_state));
-  EXPECT_EQ(GetState(transition), State::kCapturing);
+  DocumentTransitionPrepareOptions prepare_options;
+  ScriptPromiseTester prepare_tester(
+      script_state,
+      transition->prepare(script_state, &prepare_options, exception_state));
+  EXPECT_EQ(GetState(transition), State::kPreparing);
 
   UpdateAllLifecyclePhasesAndFinishDirectives();
-  capture_tester.WaitUntilSettled();
-  EXPECT_TRUE(capture_tester.IsFulfilled());
-  EXPECT_EQ(GetState(transition), State::kCaptured);
+  prepare_tester.WaitUntilSettled();
+  EXPECT_TRUE(prepare_tester.IsFulfilled());
+  EXPECT_EQ(GetState(transition), State::kPrepared);
 
+  DocumentTransitionStartOptions start_options;
   ScriptPromiseTester start_tester(
-      script_state, transition->start(script_state, exception_state));
+      script_state,
+      transition->start(script_state, &start_options, exception_state));
   // Take the request.
   auto start_request = transition->TakePendingRequest();
   EXPECT_TRUE(start_request);
@@ -476,7 +546,7 @@
 
   // Subsequent starts should get an exception.
   EXPECT_FALSE(exception_state.HadException());
-  transition->start(script_state, exception_state);
+  transition->start(script_state, &start_options, exception_state);
   EXPECT_TRUE(exception_state.HadException());
   EXPECT_FALSE(transition->TakePendingRequest());
 
@@ -488,30 +558,33 @@
 
 TEST_P(DocumentTransitionTest, StartPromiseIsResolved) {
   auto* transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
-  auto scope = transition->CreateScriptMutationsAllowedScope();
+      DocumentTransitionSupplement::documentTransition(GetDocument());
 
   V8TestingScope v8_scope;
   ScriptState* script_state = v8_scope.GetScriptState();
   ExceptionState& exception_state = v8_scope.GetExceptionState();
 
-  ScriptPromiseTester capture_tester(
-      script_state, transition->captureAndHold(script_state, exception_state));
-  EXPECT_EQ(GetState(transition), State::kCapturing);
+  DocumentTransitionPrepareOptions prepare_options;
+  ScriptPromiseTester prepare_tester(
+      script_state,
+      transition->prepare(script_state, &prepare_options, exception_state));
+  EXPECT_EQ(GetState(transition), State::kPreparing);
 
-  // Visual updates are allows during capture phase.
+  // Visual updates are allows during prepare phase.
   EXPECT_FALSE(LayerTreeHost()->IsDeferringCommits());
 
   UpdateAllLifecyclePhasesAndFinishDirectives();
-  capture_tester.WaitUntilSettled();
-  EXPECT_TRUE(capture_tester.IsFulfilled());
-  EXPECT_EQ(GetState(transition), State::kCaptured);
+  prepare_tester.WaitUntilSettled();
+  EXPECT_TRUE(prepare_tester.IsFulfilled());
+  EXPECT_EQ(GetState(transition), State::kPrepared);
 
-  // Visual updates are stalled between captured and start.
+  // Visual updates are stalled between prepared and start.
   EXPECT_TRUE(LayerTreeHost()->IsDeferringCommits());
 
+  DocumentTransitionStartOptions start_options;
   ScriptPromiseTester start_tester(
-      script_state, transition->start(script_state, exception_state));
+      script_state,
+      transition->start(script_state, &start_options, exception_state));
 
   EXPECT_EQ(GetState(transition), State::kStarted);
   UpdateAllLifecyclePhasesAndFinishDirectives();
@@ -525,23 +598,26 @@
   EXPECT_EQ(GetState(transition), State::kIdle);
 }
 
-TEST_P(DocumentTransitionTest, Abandon) {
+TEST_P(DocumentTransitionTest, AbortSignal) {
   auto* transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
-  auto scope = transition->CreateScriptMutationsAllowedScope();
+      DocumentTransitionSupplement::documentTransition(GetDocument());
 
   V8TestingScope v8_scope;
   ScriptState* script_state = v8_scope.GetScriptState();
   ExceptionState& exception_state = v8_scope.GetExceptionState();
 
-  ScriptPromiseTester capture_tester(
-      script_state, transition->captureAndHold(script_state, exception_state));
-  EXPECT_EQ(GetState(transition), State::kCapturing);
+  auto* abort_signal =
+      MakeGarbageCollected<AbortSignal>(v8_scope.GetExecutionContext());
+  DocumentTransitionPrepareOptions prepare_options;
+  prepare_options.setAbortSignal(abort_signal);
+  ScriptPromiseTester prepare_tester(
+      script_state,
+      transition->prepare(script_state, &prepare_options, exception_state));
+  EXPECT_EQ(GetState(transition), State::kPreparing);
 
-  transition->abandon(script_state, exception_state);
-
-  capture_tester.WaitUntilSettled();
-  EXPECT_TRUE(capture_tester.IsRejected());
+  abort_signal->SignalAbort(script_state);
+  prepare_tester.WaitUntilSettled();
+  EXPECT_TRUE(prepare_tester.IsRejected());
   EXPECT_EQ(GetState(transition), State::kIdle);
 }
 
@@ -563,23 +639,21 @@
   auto* e3 = GetDocument().getElementById("e3");
 
   auto* transition =
-      DocumentTransitionSupplement::EnsureDocumentTransition(GetDocument());
-  auto scope = transition->CreateScriptMutationsAllowedScope();
+      DocumentTransitionSupplement::documentTransition(GetDocument());
 
   V8TestingScope v8_scope;
   ScriptState* script_state = v8_scope.GetScriptState();
   ExceptionState& exception_state = v8_scope.GetExceptionState();
 
-  transition->setElement(script_state, e1, "e1", nullptr, exception_state);
-  transition->setElement(script_state, e2, "e2", nullptr, exception_state);
-  transition->setElement(script_state, e3, "e3", nullptr, exception_state);
-  transition->captureAndHold(script_state, exception_state);
+  DocumentTransitionPrepareOptions options;
+  options.setSharedElements({e1, e2, e3});
+  transition->prepare(script_state, &options, exception_state);
   ASSERT_FALSE(exception_state.HadException());
   UpdateAllLifecyclePhasesForTest();
 
   // The prepare phase should generate the pseudo tree.
-  const Vector<AtomicString> document_transition_tags = {"root", "e1", "e2",
-                                                         "e3"};
+  const Vector<AtomicString> document_transition_tags = {"shared-0", "shared-1",
+                                                         "shared-2"};
   ValidatePseudoElementTree(document_transition_tags, false);
 
   // Finish the prepare phase, mutate the DOM and start the animation.
@@ -593,7 +667,9 @@
     <div id=e2></div>
     <div id=e3></div>
   )HTML");
-  transition->start(script_state, exception_state);
+  DocumentTransitionStartOptions start_options;
+  start_options.setSharedElements({e1, e2, e3});
+  transition->start(script_state, &start_options, exception_state);
   ASSERT_FALSE(exception_state.HadException());
 
   // The start phase should generate pseudo elements for rendering new live
diff --git a/third_party/blink/renderer/core/editing/caret_display_item_client_test.cc b/third_party/blink/renderer/core/editing/caret_display_item_client_test.cc
index d328614..8af7a484 100644
--- a/third_party/blink/renderer/core/editing/caret_display_item_client_test.cc
+++ b/third_party/blink/renderer/core/editing/caret_display_item_client_test.cc
@@ -82,12 +82,12 @@
   }
 
   RasterInvalidationTracking* CaretRasterInvalidationTracking() const {
-    for (const auto& client : GetDocument()
-                                  .View()
-                                  ->GetPaintArtifactCompositor()
-                                  ->ContentLayerClientsForTesting()) {
+    wtf_size_t i = 0;
+    auto* pac = GetDocument().View()->GetPaintArtifactCompositor();
+    while (auto* client = pac->ContentLayerClientForTesting(i)) {
       if (client->Layer().DebugName() == "Caret")
         return client->GetRasterInvalidator().GetTracking();
+      ++i;
     }
     return nullptr;
   }
diff --git a/third_party/blink/renderer/core/frame/attribution_response_parsing.cc b/third_party/blink/renderer/core/frame/attribution_response_parsing.cc
index b47394f..9c0fd2d1 100644
--- a/third_party/blink/renderer/core/frame/attribution_response_parsing.cc
+++ b/third_party/blink/renderer/core/frame/attribution_response_parsing.cc
@@ -120,4 +120,72 @@
       ResponseParseStatus::kSuccess, std::move(sources));
 }
 
+bool ParseEventTriggerData(
+    const AtomicString& json_string,
+    WTF::Vector<mojom::blink::EventTriggerDataPtr>& event_trigger_data) {
+  // Populate attribution data from provided JSON.
+  std::unique_ptr<JSONValue> json = ParseJSON(json_string);
+
+  // TODO(johnidel): Log a devtools issues if JSON parsing fails and on
+  // individual early exits below.
+  if (!json)
+    return false;
+
+  JSONArray* array_value = JSONArray::Cast(json.get());
+  if (!array_value)
+    return false;
+
+  // Do not proceed if too many event trigger data are specified.
+  if (array_value->size() > kMaxAttributionEventTriggerData)
+    return false;
+
+  // Process each event trigger.
+  for (wtf_size_t i = 0; i < array_value->size(); ++i) {
+    JSONValue* value = array_value->at(i);
+    DCHECK(value);
+
+    const auto* object_val = JSONObject::Cast(value);
+    if (!object_val)
+      return false;
+
+    mojom::blink::EventTriggerDataPtr event_trigger =
+        mojom::blink::EventTriggerData::New();
+
+    String trigger_data_string;
+    // A valid header must declare data for each sub-item.
+    if (!object_val->GetString("trigger_data", &trigger_data_string))
+      return false;
+    bool trigger_data_is_valid = false;
+    uint64_t trigger_data_value =
+        trigger_data_string.ToUInt64Strict(&trigger_data_is_valid);
+
+    // Default invalid data values to 0 so a report will get sent.
+    event_trigger->data = trigger_data_is_valid ? trigger_data_value : 0;
+
+    // Treat invalid priority and deduplication key as if they were not set.
+    String priority_string;
+    if (object_val->GetString("priority", &priority_string)) {
+      bool priority_is_valid = false;
+      int64_t priority = priority_string.ToInt64Strict(&priority_is_valid);
+      if (priority_is_valid)
+        event_trigger->priority = priority;
+    }
+
+    // Treat invalid priority and deduplication_key as if they were not set.
+    String dedup_key_string;
+    if (object_val->GetString("deduplication_key", &dedup_key_string)) {
+      bool dedup_key_is_valid = false;
+      uint64_t dedup_key = dedup_key_string.ToUInt64Strict(&dedup_key_is_valid);
+      if (dedup_key_is_valid) {
+        event_trigger->dedup_key =
+            mojom::blink::AttributionTriggerDedupKey::New(dedup_key);
+      }
+    }
+
+    event_trigger_data.push_back(std::move(event_trigger));
+  }
+
+  return true;
+}
+
 }  // namespace blink::attribution_response_parsing
diff --git a/third_party/blink/renderer/core/frame/attribution_response_parsing.h b/third_party/blink/renderer/core/frame/attribution_response_parsing.h
index d0ce380..ceb65b6dc 100644
--- a/third_party/blink/renderer/core/frame/attribution_response_parsing.h
+++ b/third_party/blink/renderer/core/frame/attribution_response_parsing.h
@@ -6,6 +6,7 @@
 #define THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_ATTRIBUTION_RESPONSE_PARSING_H_
 
 #include <utility>
+#include <vector>
 
 #include "third_party/blink/public/mojom/conversions/attribution_data_host.mojom-blink-forward.h"
 #include "third_party/blink/renderer/core/core_export.h"
@@ -47,6 +48,19 @@
 CORE_EXPORT ResponseParseResult<mojom::blink::AttributionAggregatableSources>
 ParseAttributionAggregatableSources(const AtomicString& json_string);
 
+// Parses event trigger data header of the form:
+//
+// [{
+//   "trigger_data": "5"
+//   "priority": "10",
+//   "deduplication_key": "456"
+// }]
+//
+// Returns whether parsing was successful.
+CORE_EXPORT bool ParseEventTriggerData(
+    const AtomicString& json_string,
+    WTF::Vector<mojom::blink::EventTriggerDataPtr>& event_trigger_data);
+
 }  // namespace blink::attribution_response_parsing
 
 #endif  // THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_ATTRIBUTION_RESPONSE_PARSING_H_
diff --git a/third_party/blink/renderer/core/frame/attribution_src_loader.cc b/third_party/blink/renderer/core/frame/attribution_src_loader.cc
index 88e348f6..82dd4341 100644
--- a/third_party/blink/renderer/core/frame/attribution_src_loader.cc
+++ b/third_party/blink/renderer/core/frame/attribution_src_loader.cc
@@ -166,7 +166,9 @@
   local_frame_->GetRemoteNavigationAssociatedInterfaces()->GetInterface(
       &conversion_host);
   conversion_host->RegisterDataHost(data_host.BindNewPipeAndPassReceiver());
-  resource_data_host_map_.insert(resource, std::move(data_host));
+  resource_context_map_.insert(
+      resource, AttributionSrcContext{.type = AttributionSrcType::kUndetermined,
+                                      .data_host = std::move(data_host)});
 }
 
 void AttributionSrcLoader::Shutdown() {
@@ -186,28 +188,50 @@
 }
 
 void AttributionSrcLoader::NotifyFinished(Resource* resource) {
-  DCHECK(resource_data_host_map_.Contains(resource));
-  resource_data_host_map_.erase(resource);
+  DCHECK(resource_context_map_.Contains(resource));
+  resource_context_map_.erase(resource);
 }
 
 void AttributionSrcLoader::HandleResponseHeaders(
     Resource* resource,
     const ResourceResponse& response) {
-  if (!resource_data_host_map_.Contains(resource))
+  auto it = resource_context_map_.find(resource);
+  if (it == resource_context_map_.end())
     return;
 
+  AttributionSrcContext& context = it->value;
+
   const auto& headers = response.HttpHeaderFields();
-  if (headers.Contains(http_names::kAttributionReportingRegisterSource))
-    HandleSourceRegistration(resource, response);
+
+  bool can_process_source = context.type == AttributionSrcType::kUndetermined ||
+                            context.type == AttributionSrcType::kSource;
+  if (can_process_source &&
+      headers.Contains(http_names::kAttributionReportingRegisterSource)) {
+    context.type = AttributionSrcType::kSource;
+    HandleSourceRegistration(resource, response, context);
+    return;
+  }
+
+  // TODO(johnidel): Consider surfacing an error when source and trigger headers
+  // are present together.
+  bool can_process_trigger =
+      context.type == AttributionSrcType::kUndetermined ||
+      context.type == AttributionSrcType::kTrigger;
+  if (can_process_trigger &&
+      headers.Contains(http_names::kAttributionReportingRegisterEventTrigger)) {
+    context.type = AttributionSrcType::kTrigger;
+    HandleTriggerRegistration(resource, response, context);
+  }
 
   // TODO(johnidel): Add parsing for trigger and filter headers.
 }
 
 void AttributionSrcLoader::HandleSourceRegistration(
     Resource* resource,
-    const ResourceResponse& response) {
-  auto it = resource_data_host_map_.find(resource);
-  DCHECK_NE(it, resource_data_host_map_.end());
+    const ResourceResponse& response,
+    AttributionSrcContext& context) {
+  auto it = resource_context_map_.find(resource);
+  DCHECK_NE(it, resource_context_map_.end());
 
   mojom::blink::AttributionSourceDataPtr source_data =
       mojom::blink::AttributionSourceData::New();
@@ -294,7 +318,33 @@
           aggregatable_sources_json);
   source_data->aggregatable_sources = std::move(aggregatable_sources.value);
 
-  it->value->SourceDataAvailable(std::move(source_data));
+  context.data_host->SourceDataAvailable(std::move(source_data));
+}
+
+void AttributionSrcLoader::HandleTriggerRegistration(
+    Resource* resource,
+    const ResourceResponse& response,
+    AttributionSrcContext& context) {
+  mojom::blink::AttributionTriggerDataPtr trigger_data =
+      mojom::blink::AttributionTriggerData::New();
+
+  // Verify the current url is trustworthy and capable of registering triggers.
+  scoped_refptr<const SecurityOrigin> reporting_origin =
+      SecurityOrigin::CreateFromString(response.CurrentRequestUrl());
+  if (!reporting_origin->IsPotentiallyTrustworthy())
+    return;
+  trigger_data->reporting_origin =
+      SecurityOrigin::Create(response.CurrentRequestUrl());
+
+  // Populate event triggers.
+  bool success = attribution_response_parsing::ParseEventTriggerData(
+      response.HttpHeaderField(
+          http_names::kAttributionReportingRegisterEventTrigger),
+      trigger_data->event_triggers);
+  if (!success)
+    return;
+
+  context.data_host->TriggerDataAvailable(std::move(trigger_data));
 }
 
 void AttributionSrcLoader::LogAuditIssue(
diff --git a/third_party/blink/renderer/core/frame/attribution_src_loader.h b/third_party/blink/renderer/core/frame/attribution_src_loader.h
index 07e36ca..41703df 100644
--- a/third_party/blink/renderer/core/frame/attribution_src_loader.h
+++ b/third_party/blink/renderer/core/frame/attribution_src_loader.h
@@ -41,13 +41,27 @@
 
   void Trace(Visitor* visitor) const override {
     visitor->Trace(local_frame_);
-    visitor->Trace(resource_data_host_map_);
+    visitor->Trace(resource_context_map_);
     RawResourceClient::Trace(visitor);
   }
 
   String DebugName() const override { return "AttributionSrcLoader"; }
 
  private:
+  // Represents what events are able to be registered from an attributionsrc.
+  enum class AttributionSrcType { kUndetermined, kSource, kTrigger };
+
+  // State associated with each ongoing attribution src request.
+  struct AttributionSrcContext {
+    // Type of events this request can register. In some cases, this will not be
+    // assigned until the first event is received. A single attributionsrc
+    // request can only register one type of event across redirects.
+    AttributionSrcType type;
+
+    // Remote used for registering responses with the browser-process.
+    mojo::Remote<mojom::blink::AttributionDataHost> data_host;
+  };
+
   // RawResourceClient:
   void ResponseReceived(Resource* resource,
                         const ResourceResponse& response) override;
@@ -59,16 +73,19 @@
   void HandleResponseHeaders(Resource* resource,
                              const ResourceResponse& response);
   void HandleSourceRegistration(Resource* resource,
-                                const ResourceResponse& response);
+                                const ResourceResponse& response,
+                                AttributionSrcContext& context);
+  void HandleTriggerRegistration(Resource* resource,
+                                 const ResourceResponse& response,
+                                 AttributionSrcContext& context);
 
   void LogAuditIssue(AttributionReportingIssueType issue_type,
                      const String& string,
                      HTMLElement* element = nullptr);
 
   Member<LocalFrame> local_frame_;
-  HeapHashMap<WeakMember<Resource>,
-              mojo::Remote<mojom::blink::AttributionDataHost>>
-      resource_data_host_map_;
+  HeapHashMap<WeakMember<Resource>, AttributionSrcContext>
+      resource_context_map_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/frame/local_frame_view.cc b/third_party/blink/renderer/core/frame/local_frame_view.cc
index 3bc77d4d..170c74e21 100644
--- a/third_party/blink/renderer/core/frame/local_frame_view.cc
+++ b/third_party/blink/renderer/core/frame/local_frame_view.cc
@@ -858,8 +858,7 @@
               !frame_->PagePopupOwner() &&
               !FirstMeaningfulPaintDetector::From(*frame_->GetDocument())
                    .SeenFirstMeaningfulPaint());
-      DeferredShapingViewportScope viewport_scope(
-          *this, GetLayoutView()->InitialContainingBlockSize().block_size);
+      DeferredShapingViewportScope viewport_scope(*this, *GetLayoutView());
       GetLayoutView()->UpdateLayout();
     }
   }
diff --git a/third_party/blink/renderer/core/layout/build.gni b/third_party/blink/renderer/core/layout/build.gni
index 2a072722..658f98e7 100644
--- a/third_party/blink/renderer/core/layout/build.gni
+++ b/third_party/blink/renderer/core/layout/build.gni
@@ -40,6 +40,7 @@
   "custom_scrollbar.h",
   "depth_ordered_layout_object_list.cc",
   "depth_ordered_layout_object_list.h",
+  "deferred_shaping.cc",
   "deferred_shaping.h",
   "flexible_box_algorithm.cc",
   "flexible_box_algorithm.h",
diff --git a/third_party/blink/renderer/core/layout/deferred_shaping.cc b/third_party/blink/renderer/core/layout/deferred_shaping.cc
new file mode 100644
index 0000000..7b79c83
--- /dev/null
+++ b/third_party/blink/renderer/core/layout/deferred_shaping.cc
@@ -0,0 +1,30 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/layout/deferred_shaping.h"
+
+#include "third_party/blink/renderer/core/display_lock/display_lock_document_state.h"
+#include "third_party/blink/renderer/core/layout/layout_view.h"
+
+namespace blink {
+
+DeferredShapingViewportScope::DeferredShapingViewportScope(
+    LocalFrameView& view,
+    const LayoutView& layout_view)
+    : view_(view), previous_value_(view.CurrentViewportBottom()) {
+  LayoutUnit viewport_top =
+      LayoutUnit(layout_view.GetScrollableArea()
+                     ? view.GetScrollableArea()->GetScrollOffset().y()
+                     : 0);
+  LayoutUnit viewport_height =
+      layout_view.InitialContainingBlockSize().block_size;
+  view_.SetCurrentViewportBottom(
+      PassKey(),
+      viewport_top + viewport_height +
+          LayoutUnit(viewport_height *
+                     DisplayLockDocumentState::kViewportMarginPercentage /
+                     100));
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/core/layout/deferred_shaping.h b/third_party/blink/renderer/core/layout/deferred_shaping.h
index 0213bd01..280a808 100644
--- a/third_party/blink/renderer/core/layout/deferred_shaping.h
+++ b/third_party/blink/renderer/core/layout/deferred_shaping.h
@@ -16,11 +16,8 @@
   using PassKey = base::PassKey<DeferredShapingViewportScope>;
 
  public:
-  DeferredShapingViewportScope(LocalFrameView& view, LayoutUnit viewport_bottom)
-      : view_(view), previous_value_(view.CurrentViewportBottom()) {
-    view_.SetCurrentViewportBottom(PassKey(), viewport_bottom);
-  }
-
+  DeferredShapingViewportScope(LocalFrameView& view,
+                               const LayoutView& layout_view);
   ~DeferredShapingViewportScope() {
     view_.SetCurrentViewportBottom(PassKey(), previous_value_);
   }
diff --git a/third_party/blink/renderer/core/layout/deferred_shaping_test.cc b/third_party/blink/renderer/core/layout/deferred_shaping_test.cc
index 3004004..13a7496 100644
--- a/third_party/blink/renderer/core/layout/deferred_shaping_test.cc
+++ b/third_party/blink/renderer/core/layout/deferred_shaping_test.cc
@@ -46,6 +46,18 @@
   EXPECT_FALSE(IsLocked("target"));
 }
 
+TEST_F(DeferredShapingTest, ViewportMargin) {
+  // The box starting around y=1200 (viewport height * 2) is not deferred due to
+  // a viewport margin setting for IntersectionObserver.
+  SetBodyInnerHTML(R"HTML(
+<div style="height:1200px"></div>
+<div id="target">IFC</div>
+)HTML");
+  UpdateAllLifecyclePhasesForTest();
+  EXPECT_FALSE(IsDefer("target"));
+  EXPECT_FALSE(IsLocked("target"));
+}
+
 TEST_F(DeferredShapingTest, AlreadyAuto) {
   // If the element has content-visibility:auto, it never be deferred.
   SetBodyInnerHTML(R"HTML(
diff --git a/third_party/blink/renderer/core/layout/ng/ng_out_of_flow_positioned_node.h b/third_party/blink/renderer/core/layout/ng/ng_out_of_flow_positioned_node.h
index 4ccfacd1..9021038 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_out_of_flow_positioned_node.h
+++ b/third_party/blink/renderer/core/layout/ng/ng_out_of_flow_positioned_node.h
@@ -298,12 +298,6 @@
     return {descendants.data(), descendants.size()};
   }
 
-  void Clear() override {
-    oof_positioned_fragmentainer_descendants.clear();
-    multicols_with_pending_oofs.clear();
-    NGPhysicalFragment::OutOfFlowData::Clear();
-  }
-
   void Trace(Visitor* visitor) const override {
     visitor->Trace(oof_positioned_fragmentainer_descendants);
     visitor->Trace(multicols_with_pending_oofs);
diff --git a/third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.cc b/third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.cc
index 9358837d..d6a42e3 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.cc
+++ b/third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.cc
@@ -444,10 +444,13 @@
   }
 }
 
-NGPhysicalBoxFragment::~NGPhysicalBoxFragment() = default;
+NGPhysicalBoxFragment::~NGPhysicalBoxFragment() {
+  // Note: This function may not always be called because the dtor of
+  // NGPhysicalFragment is made non-virtual for memory efficiency.
+  ink_overflow_type_ = ink_overflow_.Reset(InkOverflowType());
+}
 
 void NGPhysicalBoxFragment::Dispose() {
-  ink_overflow_.Reset(InkOverflowType());
   if (const_has_fragment_items_)
     ComputeItemsAddress()->~NGFragmentItems();
   if (const_has_rare_data_)
diff --git a/third_party/blink/renderer/core/layout/ng/ng_physical_fragment.cc b/third_party/blink/renderer/core/layout/ng/ng_physical_fragment.cc
index 94ea260..c6e961e7 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_physical_fragment.cc
+++ b/third_party/blink/renderer/core/layout/ng/ng_physical_fragment.cc
@@ -29,23 +29,10 @@
 
 struct SameSizeAsNGPhysicalFragment
     : public GarbageCollected<SameSizeAsNGPhysicalFragment> {
-  // It is necessary to have not only the member variables but also
-  // USING_PRE_FINALIZER baceuse Win bots fail without it.
-  USING_PRE_FINALIZER(SameSizeAsNGPhysicalFragment, Dispose);
-
- public:
-  void Dispose() {}
-  void Trace(Visitor* visitor) const {
-    visitor->Trace(layout_object);
-    visitor->Trace(break_token);
-    visitor->Trace(oof_data);
-  }
-
-  Member<LayoutObject> layout_object;
+  Member<void*> layout_object;
   PhysicalSize size;
-  [[maybe_unused]] unsigned flags;
-  Member<const NGBreakToken> break_token;
-  const Member<NGPhysicalFragment::OutOfFlowData> oof_data;
+  unsigned flags;
+  Member<void*> members[2];
 };
 
 ASSERT_SIZE(NGPhysicalFragment, SameSizeAsNGPhysicalFragment);
@@ -450,11 +437,11 @@
   DCHECK(children_valid_);
 }
 
-NGPhysicalFragment::~NGPhysicalFragment() = default;
+NGPhysicalFragment::~NGPhysicalFragment() {
+  Dispose();
+}
 
 void NGPhysicalFragment::Dispose() {
-  if (UNLIKELY(oof_data_ && has_fragmented_out_of_flow_data_))
-    const_cast<NGPhysicalFragment*>(this)->ClearOutOfFlowData();
   switch (Type()) {
     case kFragmentBox:
       static_cast<NGPhysicalBoxFragment*>(this)->Dispose();
@@ -504,13 +491,6 @@
   return !!oof_data_;
 }
 
-void NGPhysicalFragment::ClearOutOfFlowData() {
-  CHECK(oof_data_ && has_fragmented_out_of_flow_data_);
-  auto* oof_data = const_cast<Member<OutOfFlowData>*>(&oof_data_);
-  oof_data->Get()->Clear();
-  oof_data->Clear();
-}
-
 NGPhysicalFragment::OutOfFlowData* NGPhysicalFragment::CloneOutOfFlowData()
     const {
   DCHECK(oof_data_);
@@ -1058,10 +1038,6 @@
   return false;
 }
 
-void NGPhysicalFragment::OutOfFlowData::Clear() {
-  oof_positioned_descendants.clear();
-}
-
 void NGPhysicalFragment::OutOfFlowData::Trace(Visitor* visitor) const {
   visitor->Trace(oof_positioned_descendants);
 }
diff --git a/third_party/blink/renderer/core/layout/ng/ng_physical_fragment.h b/third_party/blink/renderer/core/layout/ng/ng_physical_fragment.h
index dfd5b00..59f67c0 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_physical_fragment.h
+++ b/third_party/blink/renderer/core/layout/ng/ng_physical_fragment.h
@@ -52,8 +52,6 @@
 // coordinate system.
 class CORE_EXPORT NGPhysicalFragment
     : public GarbageCollected<NGPhysicalFragment> {
-  USING_PRE_FINALIZER(NGPhysicalFragment, Dispose);
-
  public:
   enum NGFragmentType {
     kFragmentBox = 0,
@@ -602,10 +600,7 @@
 
   struct OutOfFlowData : public GarbageCollected<OutOfFlowData> {
    public:
-    virtual void Clear();
-
     virtual void Trace(Visitor* visitor) const;
-
     HeapVector<NGPhysicalOutOfFlowPositionedNode> oof_positioned_descendants;
   };
 
diff --git a/third_party/blink/renderer/core/loader/frame_fetch_context.cc b/third_party/blink/renderer/core/loader/frame_fetch_context.cc
index c0b8195..f54e5f1 100644
--- a/third_party/blink/renderer/core/loader/frame_fetch_context.cc
+++ b/third_party/blink/renderer/core/loader/frame_fetch_context.cc
@@ -932,7 +932,8 @@
 
   if (String string = search_params->get("dedup-key")) {
     if (absl::optional<uint64_t> value = parse_uint64(string)) {
-      conversion->dedup_key = mojom::blink::DedupKey::New(*value);
+      conversion->dedup_key =
+          mojom::blink::AttributionTriggerDedupKey::New(*value);
     } else {
       AuditsIssue::ReportAttributionIssue(
           document_->domWindow(),
diff --git a/third_party/blink/renderer/core/paint/paint_and_raster_invalidation_test.cc b/third_party/blink/renderer/core/paint/paint_and_raster_invalidation_test.cc
index 66eeb8a..bc2e94e 100644
--- a/third_party/blink/renderer/core/paint/paint_and_raster_invalidation_test.cc
+++ b/third_party/blink/renderer/core/paint/paint_and_raster_invalidation_test.cc
@@ -18,9 +18,8 @@
 static ContentLayerClientImpl* GetContentLayerClient(
     const LocalFrameView& root_frame_view,
     wtf_size_t index) {
-  const auto& clients = root_frame_view.GetPaintArtifactCompositor()
-                            ->ContentLayerClientsForTesting();
-  return index < clients.size() ? clients[index].get() : nullptr;
+  return root_frame_view.GetPaintArtifactCompositor()
+      ->ContentLayerClientForTesting(index);
 }
 
 const RasterInvalidationTracking* GetRasterInvalidationTracking(
diff --git a/third_party/blink/renderer/core/web_test/web_test_web_frame_widget_impl.cc b/third_party/blink/renderer/core/web_test/web_test_web_frame_widget_impl.cc
index c8f83c7e..b3caefd 100644
--- a/third_party/blink/renderer/core/web_test/web_test_web_frame_widget_impl.cc
+++ b/third_party/blink/renderer/core/web_test/web_test_web_frame_widget_impl.cc
@@ -120,7 +120,7 @@
 }
 
 void WebTestWebFrameWidgetImpl::DisableEndDocumentTransition() {
-  DocumentTransitionSupplement::EnsureDocumentTransition(
+  DocumentTransitionSupplement::documentTransition(
       *LocalRootImpl()->GetFrame()->GetDocument())
       ->DisableEndTransition();
 }
diff --git a/third_party/blink/renderer/platform/graphics/compositing/content_layer_client_impl.cc b/third_party/blink/renderer/platform/graphics/compositing/content_layer_client_impl.cc
index 613d059..ec4d357 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/content_layer_client_impl.cc
+++ b/third_party/blink/renderer/platform/graphics/compositing/content_layer_client_impl.cc
@@ -61,14 +61,9 @@
 #endif
 }
 
-scoped_refptr<cc::PictureLayer> ContentLayerClientImpl::UpdateCcPictureLayer(
+void ContentLayerClientImpl::UpdateCcPictureLayer(
     const PendingLayer& pending_layer) {
   const auto& paint_chunks = pending_layer.Chunks();
-  if (paint_chunks.begin()->is_cacheable)
-    id_.emplace(paint_chunks.begin()->id);
-  else
-    id_ = absl::nullopt;
-
 #if EXPENSIVE_DCHECKS_ARE_ON()
   paint_chunk_debug_data_ = std::make_unique<JSONArray>();
   for (auto it = paint_chunks.begin(); it != paint_chunks.end(); ++it) {
@@ -119,7 +114,7 @@
       cc_picture_layer_->draws_content() == pending_layer.DrawsContent() &&
       !raster_under_invalidation_params) {
     DCHECK_EQ(cc_picture_layer_->bounds(), layer_bounds);
-    return cc_picture_layer_;
+    return;
   }
 
   cc_display_item_list_ = PaintChunksToCcLayer::Convert(
@@ -131,7 +126,14 @@
   cc_picture_layer_->SetHitTestable(true);
   cc_picture_layer_->SetIsDrawable(pending_layer.DrawsContent());
 
-  return cc_picture_layer_;
+  bool contents_opaque = pending_layer.RectKnownToBeOpaque().Contains(
+      gfx::RectF(gfx::PointAtOffsetFromOrigin(pending_layer.LayerOffset()),
+                 gfx::SizeF(pending_layer.LayerBounds())));
+  cc_picture_layer_->SetContentsOpaque(contents_opaque);
+  if (!contents_opaque) {
+    cc_picture_layer_->SetContentsOpaqueForText(
+        pending_layer.TextKnownToBeOnOpaqueBackground());
+  }
 }
 
 void ContentLayerClientImpl::InvalidateRect(const gfx::Rect& rect) {
diff --git a/third_party/blink/renderer/platform/graphics/compositing/content_layer_client_impl.h b/third_party/blink/renderer/platform/graphics/compositing/content_layer_client_impl.h
index 71a1279..05b19d6 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/content_layer_client_impl.h
+++ b/third_party/blink/renderer/platform/graphics/compositing/content_layer_client_impl.h
@@ -47,11 +47,7 @@
   cc::Layer& Layer() const { return *cc_picture_layer_.get(); }
   const PropertyTreeState& State() const { return layer_state_; }
 
-  bool Matches(const PaintChunk& paint_chunk) const {
-    return id_ && paint_chunk.Matches(*id_);
-  }
-
-  scoped_refptr<cc::PictureLayer> UpdateCcPictureLayer(const PendingLayer&);
+  void UpdateCcPictureLayer(const PendingLayer&);
 
   RasterInvalidator& GetRasterInvalidator() { return raster_invalidator_; }
 
@@ -61,7 +57,6 @@
   // Callback from raster_invalidator_.
   void InvalidateRect(const gfx::Rect&);
 
-  absl::optional<PaintChunk::Id> id_;
   scoped_refptr<cc::PictureLayer> cc_picture_layer_;
   scoped_refptr<cc::DisplayItemList> cc_display_item_list_;
   RasterInvalidator raster_invalidator_;
diff --git a/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc b/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc
index 9993bbd..1847908 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc
+++ b/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc
@@ -9,7 +9,6 @@
 
 #include "base/logging.h"
 #include "cc/document_transition/document_transition_request.h"
-#include "cc/layers/scrollbar_layer_base.h"
 #include "cc/paint/display_item_list.h"
 #include "cc/paint/paint_flags.h"
 #include "cc/trees/effect_node.h"
@@ -18,7 +17,6 @@
 #include "third_party/blink/public/platform/platform.h"
 #include "third_party/blink/renderer/platform/geometry/geometry_as_json.h"
 #include "third_party/blink/renderer/platform/graphics/compositing/content_layer_client_impl.h"
-#include "third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.h"
 #include "third_party/blink/renderer/platform/graphics/graphics_context.h"
 #include "third_party/blink/renderer/platform/graphics/paint/clip_paint_property_node.h"
 #include "third_party/blink/renderer/platform/graphics/paint/display_item.h"
@@ -45,6 +43,35 @@
 // http://crbug.com/692842#c4.
 static int g_s_property_tree_sequence_number = 1;
 
+class PaintArtifactCompositor::OldPendingLayerMatcher {
+ public:
+  explicit OldPendingLayerMatcher(PendingLayers pending_layers)
+      : pending_layers_(std::move(pending_layers)) {}
+
+  // Finds the next PendingLayer that can be matched by |new_layer|.
+  // It's efficient if most of the pending layers can be matched sequentially.
+  PendingLayer* Find(const PendingLayer& new_layer) {
+    if (pending_layers_.IsEmpty())
+      return nullptr;
+    if (!new_layer.FirstPaintChunk().CanMatchOldChunk())
+      return nullptr;
+    wtf_size_t i = next_index_;
+    do {
+      wtf_size_t next = (i + 1) % pending_layers_.size();
+      if (new_layer.Matches(pending_layers_[i])) {
+        next_index_ = next;
+        return &pending_layers_[i];
+      }
+      i = next;
+    } while (i != next_index_);
+    return nullptr;
+  }
+
+ private:
+  wtf_size_t next_index_ = 0;
+  PendingLayers pending_layers_;
+};
+
 PaintArtifactCompositor::PaintArtifactCompositor(
     base::WeakPtr<CompositorScrollCallbacks> scroll_callbacks)
     : scroll_callbacks_(std::move(scroll_callbacks)),
@@ -56,8 +83,10 @@
 
 void PaintArtifactCompositor::SetTracksRasterInvalidations(bool should_track) {
   tracks_raster_invalidations_ = should_track || VLOG_IS_ON(3);
-  for (auto& client : content_layer_clients_)
-    client->GetRasterInvalidator().SetTracksRasterInvalidations(should_track);
+  for (auto& pending_layer : pending_layers_) {
+    if (auto* client = pending_layer.GetContentLayerClient())
+      client->GetRasterInvalidator().SetTracksRasterInvalidations(should_track);
+  }
 }
 
 void PaintArtifactCompositor::WillBeRemovedFromFrame() {
@@ -91,10 +120,10 @@
   for (const auto& layer : root_layer_->children()) {
     const LayerAsJSONClient* json_client = nullptr;
     const TransformPaintPropertyNode* transform = nullptr;
-    for (const auto& client : content_layer_clients_) {
-      if (&client->Layer() == layer.get()) {
-        json_client = client.get();
-        transform = &client->State().Transform();
+    for (const auto& pending_layer : pending_layers_) {
+      if (layer.get() == &pending_layer.CcLayer()) {
+        json_client = pending_layer.GetContentLayerClient();
+        transform = &pending_layer.GetPropertyTreeState().Transform();
         break;
       }
     }
@@ -114,27 +143,6 @@
   return layers_as_json.Finalize();
 }
 
-scoped_refptr<cc::Layer> PaintArtifactCompositor::WrappedCcLayerForPendingLayer(
-    const PendingLayer& pending_layer) {
-  if (pending_layer.GetCompositingType() != PendingLayer::kForeignLayer)
-    return nullptr;
-
-  // UpdateTouchActionRects() depends on the layer's offset, but when the
-  // layer's offset changes, we do not call SetNeedsUpdate() (this is an
-  // optimization because the update would only cause an extra commit) This is
-  // only OK if the ForeignLayer doesn't have hit test data.
-  DCHECK(!pending_layer.FirstPaintChunk().hit_test_data);
-  const auto& foreign_layer_display_item =
-      To<ForeignLayerDisplayItem>(pending_layer.FirstDisplayItem());
-
-  gfx::Vector2dF layer_offset = gfx::Vector2dF(
-      foreign_layer_display_item.VisualRect().OffsetFromOrigin());
-  cc::Layer* layer = foreign_layer_display_item.GetLayer();
-  layer->SetOffsetToTransformParent(
-      layer_offset + pending_layer.OffsetOfDecompositedTransforms());
-  return layer;
-}
-
 const TransformPaintPropertyNode&
 PaintArtifactCompositor::NearestScrollTranslationForLayer(
     const PendingLayer& pending_layer) {
@@ -148,140 +156,6 @@
   return transform.NearestScrollTranslationNode();
 }
 
-scoped_refptr<cc::Layer>
-PaintArtifactCompositor::ScrollHitTestLayerForPendingLayer(
-    const PendingLayer& pending_layer) {
-  if (pending_layer.GetCompositingType() != PendingLayer::kScrollHitTestLayer)
-    return nullptr;
-
-  // We shouldn't decomposite scroll transform nodes.
-  DCHECK_EQ(gfx::Vector2dF(), pending_layer.OffsetOfDecompositedTransforms());
-
-  const auto& scroll_node =
-      *pending_layer.ScrollTranslationForScrollHitTestLayer().ScrollNode();
-
-  scoped_refptr<cc::Layer> scroll_layer;
-  auto scroll_element_id = scroll_node.GetCompositorElementId();
-  for (auto& existing_layer : scroll_hit_test_layers_) {
-    if (existing_layer && existing_layer->element_id() == scroll_element_id) {
-      scroll_layer = std::move(existing_layer);
-      break;
-    }
-  }
-
-  if (scroll_layer) {
-    DCHECK_EQ(scroll_layer->element_id(), scroll_node.GetCompositorElementId());
-  } else {
-    scroll_layer = cc::Layer::Create();
-    scroll_layer->SetElementId(scroll_node.GetCompositorElementId());
-    scroll_layer->SetHitTestable(true);
-  }
-
-  scroll_layer->SetOffsetToTransformParent(
-      gfx::Vector2dF(scroll_node.ContainerRect().OffsetFromOrigin()));
-  // TODO(pdr): The scroll layer's bounds are currently set to the clipped
-  // container bounds but this does not include the border. We may want to
-  // change this behavior to make non-composited and composited hit testing
-  // match (see: crbug.com/753124). To do this, use
-  // |scroll_hit_test->scroll_container_bounds|. Set the layer's bounds equal
-  // to the container because the scroll layer does not scroll.
-  scroll_layer->SetBounds(scroll_node.ContainerRect().size());
-
-  if (scroll_node.NodeChanged() != PaintPropertyChangeType::kUnchanged) {
-    scroll_layer->SetNeedsPushProperties();
-    scroll_layer->SetNeedsCommit();
-  }
-
-  return scroll_layer;
-}
-
-scoped_refptr<cc::ScrollbarLayerBase>
-PaintArtifactCompositor::ScrollbarLayerForPendingLayer(
-    const PendingLayer& pending_layer) {
-  if (pending_layer.GetCompositingType() != PendingLayer::kScrollbarLayer)
-    return nullptr;
-
-  const auto& item = pending_layer.FirstDisplayItem();
-  DCHECK(item.IsScrollbar());
-
-  const auto& scrollbar_item = To<ScrollbarDisplayItem>(item);
-  scoped_refptr<cc::ScrollbarLayerBase> scrollbar_layer;
-  for (auto& layer : scrollbar_layers_) {
-    if (layer && layer->element_id() == scrollbar_item.ElementId()) {
-      scrollbar_layer = std::move(layer);
-      break;
-    }
-  }
-
-  scrollbar_layer = scrollbar_item.CreateOrReuseLayer(scrollbar_layer.get());
-  scrollbar_layer->SetOffsetToTransformParent(
-      scrollbar_layer->offset_to_transform_parent() +
-      gfx::Vector2dF(pending_layer.OffsetOfDecompositedTransforms()));
-  return scrollbar_layer;
-}
-
-std::unique_ptr<ContentLayerClientImpl>
-PaintArtifactCompositor::ClientForPaintChunk(const PaintChunk& paint_chunk) {
-  // TODO(chrishtr): for now, just using a linear walk. In the future we can
-  // optimize this by using the same techniques used in PaintController for
-  // display lists.
-  for (auto& client : content_layer_clients_) {
-    if (client && client->Matches(paint_chunk))
-      return std::move(client);
-  }
-
-  auto client = std::make_unique<ContentLayerClientImpl>();
-  client->GetRasterInvalidator().SetTracksRasterInvalidations(
-      tracks_raster_invalidations_);
-  return client;
-}
-
-scoped_refptr<cc::Layer>
-PaintArtifactCompositor::CompositedLayerForPendingLayer(
-    const PendingLayer& pending_layer,
-    Vector<std::unique_ptr<ContentLayerClientImpl>>& new_content_layer_clients,
-    Vector<scoped_refptr<cc::Layer>>& new_scroll_hit_test_layers,
-    Vector<scoped_refptr<cc::ScrollbarLayerBase>>& new_scrollbar_layers) {
-  // If the paint chunk is a foreign layer or pre-composited layer, just return
-  // its cc::Layer.
-  if (auto cc_layer = WrappedCcLayerForPendingLayer(pending_layer))
-    return cc_layer;
-
-  // If the paint chunk is a scroll hit test layer, lookup/create the layer.
-  if (auto scroll_layer = ScrollHitTestLayerForPendingLayer(pending_layer)) {
-    new_scroll_hit_test_layers.push_back(scroll_layer);
-    return scroll_layer;
-  }
-
-  if (auto scrollbar_layer = ScrollbarLayerForPendingLayer(pending_layer)) {
-    new_scrollbar_layers.push_back(scrollbar_layer);
-    return scrollbar_layer;
-  }
-
-  // The common case: create or reuse a PictureLayer for painted content.
-  std::unique_ptr<ContentLayerClientImpl> content_layer_client =
-      ClientForPaintChunk(pending_layer.FirstPaintChunk());
-
-  gfx::Vector2dF layer_offset = pending_layer.LayerOffset();
-  gfx::Size layer_bounds = pending_layer.LayerBounds();
-  auto cc_layer = content_layer_client->UpdateCcPictureLayer(pending_layer);
-  new_content_layer_clients.push_back(std::move(content_layer_client));
-
-  // Set properties that foreign layers would normally control for themselves
-  // here to avoid changing foreign layers. This includes things set by video
-  // clients etc.
-  bool contents_opaque = pending_layer.RectKnownToBeOpaque().Contains(
-      gfx::RectF(gfx::PointAtOffsetFromOrigin(layer_offset),
-                 gfx::SizeF(layer_bounds)));
-  cc_layer->SetContentsOpaque(contents_opaque);
-  if (!contents_opaque) {
-    cc_layer->SetContentsOpaqueForText(
-        pending_layer.TextKnownToBeOnOpaqueBackground());
-  }
-
-  return cc_layer;
-}
-
 namespace {
 
 cc::Layer* ForeignLayer(const PaintChunk& chunk,
@@ -447,7 +321,7 @@
   PendingLayer& layer = pending_layers_[layer_index];
   if (&layer.GetPropertyTreeState().Effect() != &effect)
     return false;
-  if (layer.RequiresOwnLayer())
+  if (layer.ChunkRequiresOwnLayer())
     return false;
   if (effect.HasDirectCompositingReasons())
     return false;
@@ -525,7 +399,7 @@
       // force_draws_content doesn't apply to pending layers that require own
       // layer, specifically scrollbar layers, foreign layers, scroll hit
       // testing layers.
-      if (pending_layers_.back().RequiresOwnLayer())
+      if (pending_layers_.back().ChunkRequiresOwnLayer())
         continue;
     } else {
       const EffectPaintPropertyNode* subgroup =
@@ -555,7 +429,7 @@
     // processed. Now determine whether it could be merged into a previous
     // layer.
     PendingLayer& new_layer = pending_layers_.back();
-    DCHECK(!new_layer.RequiresOwnLayer());
+    DCHECK(!new_layer.ChunkRequiresOwnLayer());
     DCHECK_EQ(&current_group, &new_layer.GetPropertyTreeState().Effect());
     if (force_draws_content)
       new_layer.ForceDrawsContent();
@@ -578,8 +452,6 @@
 
 void PaintArtifactCompositor::CollectPendingLayers(
     scoped_refptr<const PaintArtifact> artifact) {
-  // Shrink, but do not release the backing. Re-use it from the last frame.
-  pending_layers_.Shrink(0);
   PaintChunkSubset subset(artifact);
   auto cursor = subset.begin();
   LayerizeGroup(subset, EffectPaintPropertyNode::Root(), cursor,
@@ -769,6 +641,10 @@
   root_layer_->set_property_tree_sequence_number(
       g_s_property_tree_sequence_number);
 
+  wtf_size_t old_size = pending_layers_.size();
+  OldPendingLayerMatcher old_pending_layer_matcher(std::move(pending_layers_));
+  pending_layers_.ReserveCapacity(old_size);
+
   // Make compositing decisions, storing the result in |pending_layers_|.
   CollectPendingLayers(artifact);
   PendingLayer::DecompositeTransforms(pending_layers_);
@@ -786,11 +662,6 @@
   if (RuntimeEnabledFeatures::ScrollUnificationEnabled())
     property_tree_manager.EnsureCompositorScrollNodes(scroll_translation_nodes);
 
-  Vector<std::unique_ptr<ContentLayerClientImpl>> new_content_layer_clients;
-  new_content_layer_clients.ReserveCapacity(pending_layers_.size());
-  Vector<scoped_refptr<cc::Layer>> new_scroll_hit_test_layers;
-  Vector<scoped_refptr<cc::ScrollbarLayerBase>> new_scrollbar_layers;
-
   for (auto& entry : synthesized_clip_cache_)
     entry.in_use = false;
 
@@ -798,33 +669,28 @@
       ->effect_tree_mutable()
       .ClearTransitionPseudoElementEffectNodes();
   cc::LayerSelection layer_selection;
-  for (const auto& pending_layer : pending_layers_) {
+  for (auto& pending_layer : pending_layers_) {
+    pending_layer.UpdateCompositedLayer(
+        old_pending_layer_matcher.Find(pending_layer), layer_selection,
+        tracks_raster_invalidations_);
+    cc::Layer& layer = pending_layer.CcLayer();
+    layer.SetLayerTreeHost(root_layer_->layer_tree_host());
+
     const auto& property_state = pending_layer.GetPropertyTreeState();
     const auto& transform = property_state.Transform();
     const auto& clip = property_state.Clip();
     const auto& effect = property_state.Effect();
-
-    scoped_refptr<cc::Layer> layer = CompositedLayerForPendingLayer(
-        pending_layer, new_content_layer_clients, new_scroll_hit_test_layers,
-        new_scrollbar_layers);
-
-    UpdateLayerProperties(*layer, pending_layer);
-    UpdateLayerSelection(*layer, pending_layer, layer_selection);
-
-    layer->SetLayerTreeHost(root_layer_->layer_tree_host());
-
     int transform_id =
         property_tree_manager.EnsureCompositorTransformNode(transform);
     int clip_id = property_tree_manager.EnsureCompositorClipNode(clip);
     int effect_id = property_tree_manager.SwitchToEffectNodeWithSynthesizedClip(
-        effect, clip, layer->draws_content());
+        effect, clip, layer.draws_content());
 
     // We need additional bookkeeping for backdrop-filter mask.
     if (effect.RequiresCompositingForBackdropFilterMask() &&
         effect.CcNodeId(g_s_property_tree_sequence_number) == effect_id) {
-      static_cast<cc::PictureLayer*>(layer.get())
-          ->SetIsBackdropFilterMask(true);
-      layer->SetElementId(effect.GetCompositorElementId());
+      static_cast<cc::PictureLayer&>(layer).SetIsBackdropFilterMask(true);
+      layer.SetElementId(effect.GetCompositorElementId());
       auto& effect_tree = host->property_trees()->effect_tree_mutable();
       auto* cc_node = effect_tree.Node(effect_id);
       effect_tree.Node(cc_node->parent_id)->backdrop_mask_element_id =
@@ -842,29 +708,29 @@
     if (RuntimeEnabledFeatures::ScrollUnificationEnabled())
       property_tree_manager.SetCcScrollNodeIsComposited(scroll_id);
 
-    layer_list_builder.Add(layer);
+    layer_list_builder.Add(&layer);
 
-    layer->set_property_tree_sequence_number(
+    layer.set_property_tree_sequence_number(
         root_layer_->property_tree_sequence_number());
-    layer->SetTransformTreeIndex(transform_id);
-    layer->SetScrollTreeIndex(scroll_id);
-    layer->SetClipTreeIndex(clip_id);
-    layer->SetEffectTreeIndex(effect_id);
+    layer.SetTransformTreeIndex(transform_id);
+    layer.SetScrollTreeIndex(scroll_id);
+    layer.SetClipTreeIndex(clip_id);
+    layer.SetEffectTreeIndex(effect_id);
     bool backface_hidden = transform.IsBackfaceHidden();
-    layer->SetShouldCheckBackfaceVisibility(backface_hidden);
+    layer.SetShouldCheckBackfaceVisibility(backface_hidden);
 
     // If the property tree state has changed between the layer and the root,
     // we need to inform the compositor so damage can be calculated. Calling
     // |PropertyTreeStateChanged| for every pending layer is O(|property
     // nodes|^2) and could be optimized by caching the lookup of nodes known
     // to be changed/unchanged.
-    if (layer->subtree_property_changed() ||
+    if (layer.subtree_property_changed() ||
         pending_layer.PropertyTreeStateChanged()) {
-      layer->SetSubtreePropertyChanged();
+      layer.SetSubtreePropertyChanged();
       root_layer_->SetNeedsCommit();
     }
 
-    auto shared_element_id = layer->DocumentTransitionResourceId();
+    auto shared_element_id = layer.DocumentTransitionResourceId();
     if (shared_element_id.IsValid()) {
       host->property_trees()
           ->effect_tree_mutable()
@@ -875,9 +741,6 @@
   root_layer_->layer_tree_host()->RegisterSelection(layer_selection);
 
   property_tree_manager.Finalize();
-  content_layer_clients_.swap(new_content_layer_clients);
-  scroll_hit_test_layers_.swap(new_scroll_hit_test_layers);
-  scrollbar_layers_.swap(new_scrollbar_layers);
 
   auto* new_end = std::remove_if(
       synthesized_clip_cache_.begin(), synthesized_clip_cache_.end(),
@@ -915,44 +778,6 @@
                   .Utf8();
 }
 
-void PaintArtifactCompositor::UpdateLayerProperties(
-    cc::Layer& layer,
-    const PendingLayer& pending_layer) {
-  // Properties of foreign layers are managed by their owners.
-  if (pending_layer.GetCompositingType() == PendingLayer::kForeignLayer)
-    return;
-  PaintChunksToCcLayer::UpdateLayerProperties(
-      layer, pending_layer.GetPropertyTreeState(), pending_layer.Chunks());
-}
-
-void PaintArtifactCompositor::UpdateLayerSelection(
-    cc::Layer& layer,
-    const PendingLayer& pending_layer,
-    cc::LayerSelection& layer_selection) {
-  // Foreign layers cannot contain selection.
-  if (pending_layer.GetCompositingType() == PendingLayer::kForeignLayer)
-    return;
-  PaintChunksToCcLayer::UpdateLayerSelection(
-      layer, pending_layer.GetPropertyTreeState(), pending_layer.Chunks(),
-      layer_selection);
-}
-
-void PaintArtifactCompositor::UpdateRepaintedContentLayerClient(
-    const PendingLayer& pending_layer,
-    bool pending_layer_chunks_unchanged,
-    ContentLayerClientImpl& content_layer_client) {
-  // Checking |pending_layer_chunks_unchanged| is an optimization to avoid the
-  // expensive call to |UpdateCcPictureLayer| when no repainting occurs for this
-  // PendingLayer.
-  if (pending_layer_chunks_unchanged) {
-    // See RasterInvalidator::SetOldPaintArtifact() for the reason for this.
-    content_layer_client.GetRasterInvalidator().SetOldPaintArtifact(
-        &pending_layer.Chunks().GetPaintArtifact());
-  } else {
-    content_layer_client.UpdateCcPictureLayer(pending_layer);
-  }
-}
-
 void PaintArtifactCompositor::UpdateRepaintedLayers(
     scoped_refptr<const PaintArtifact> repainted_artifact) {
   // |Update| should be used for full updates.
@@ -973,18 +798,14 @@
 
   // The loop below iterates over the existing PendingLayers and issues updates.
   auto* repainted_chunk_iterator = repainted_chunks.begin();
-  auto* content_layer_client_it = content_layer_clients_.begin();
-  auto* scroll_hit_test_layer_it = scroll_hit_test_layers_.begin();
-  auto* scrollbar_layer_it = scrollbar_layers_.begin();
-  for (auto* pending_layer_it = pending_layers_.begin();
-       pending_layer_it != pending_layers_.end(); pending_layer_it++) {
+  for (auto& pending_layer : pending_layers_) {
     // We need to both copy the repainted paint chunks and update the cc::Layer.
     // To do this, we need the previous PaintChunks (from the PendingLayer) and
     // the matching repainted PaintChunks (from |repainted_chunks|). Because
     // repaint-only updates cannot add, remove, or re-order PaintChunks,
     // |repainted_chunk_iterator| searches forward in |repainted_chunks| for
     // the matching paint chunk, ensuring this function is O(chunks).
-    const PaintChunk& first = *pending_layer_it->Chunks().begin();
+    const PaintChunk& first = *pending_layer.Chunks().begin();
     while (repainted_chunk_iterator != repainted_chunks.end()) {
       if (repainted_chunk_iterator->Matches(first))
         break;
@@ -995,51 +816,8 @@
     // instead of a repaint update.
     CHECK(repainted_chunk_iterator != repainted_chunks.end());
 
-    // Essentially replace the paint chunks of the pending layer with the
-    // repainted chunks in |repainted_artifact|. The pending layer's paint
-    // chunks (a |PaintChunkSubset|) actually store indices to |PaintChunk|s
-    // in a |PaintArtifact|. In repaint updates, chunks are not added,
-    // removed, or re-ordered, so we can simply swap in a repainted
-    // |PaintArtifact| instead of copying |PaintChunk|s individually.
-    const PaintArtifact& previous_artifact =
-        pending_layer_it->Chunks().GetPaintArtifact();
-    DCHECK_EQ(previous_artifact.PaintChunks().size(),
-              repainted_artifact->PaintChunks().size());
-    pending_layer_it->SetPaintArtifact(repainted_artifact);
-
-    bool pending_layer_chunks_unchanged = true;
-    for (const auto& chunk : pending_layer_it->Chunks()) {
-      if (!chunk.is_moved_from_cached_subsequence) {
-        pending_layer_chunks_unchanged = false;
-        break;
-      }
-    }
-
-    cc::Layer* cc_layer = nullptr;
-    switch (pending_layer_it->GetCompositingType()) {
-      case PendingLayer::kForeignLayer:
-        continue;
-      case PendingLayer::kScrollbarLayer:
-        cc_layer = scrollbar_layer_it->get();
-        ++scrollbar_layer_it;
-        break;
-      case PendingLayer::kScrollHitTestLayer:
-        cc_layer = scroll_hit_test_layer_it->get();
-        ++scroll_hit_test_layer_it;
-        break;
-      default:
-        UpdateRepaintedContentLayerClient(*pending_layer_it,
-                                          pending_layer_chunks_unchanged,
-                                          **content_layer_client_it);
-        cc_layer = &(*content_layer_client_it)->Layer();
-        ++content_layer_client_it;
-        break;
-    }
-    DCHECK(cc_layer);
-
-    if (!pending_layer_chunks_unchanged)
-      UpdateLayerProperties(*cc_layer, *pending_layer_it);
-    UpdateLayerSelection(*cc_layer, *pending_layer_it, layer_selection);
+    pending_layer.UpdateCompositedLayerForRepaint(repainted_artifact,
+                                                  layer_selection);
   }
 
   root_layer_->layer_tree_host()->RegisterSelection(layer_selection);
@@ -1180,35 +958,14 @@
   if (!layer_debug_info_enabled_)
     return;
 
-  auto* content_layer_client_it = content_layer_clients_.begin();
-  auto* scroll_hit_test_layer_it = scroll_hit_test_layers_.begin();
-  auto* scrollbar_layer_it = scrollbar_layers_.begin();
   const PendingLayer* previous_pending_layer = nullptr;
   for (const auto& pending_layer : pending_layers_) {
-    cc::Layer* layer;
+    cc::Layer& layer = pending_layer.CcLayer();
     RasterInvalidationTracking* tracking = nullptr;
-    switch (pending_layer.GetCompositingType()) {
-      case PendingLayer::kForeignLayer:
-        layer = To<ForeignLayerDisplayItem>(pending_layer.FirstDisplayItem())
-                    .GetLayer();
-        break;
-      case PendingLayer::kScrollbarLayer:
-        layer = scrollbar_layer_it->get();
-        ++scrollbar_layer_it;
-        break;
-      case PendingLayer::kScrollHitTestLayer:
-        layer = scroll_hit_test_layer_it->get();
-        ++scroll_hit_test_layer_it;
-        break;
-      default:
-        tracking =
-            (*content_layer_client_it)->GetRasterInvalidator().GetTracking();
-        layer = &(*content_layer_client_it)->Layer();
-        ++content_layer_client_it;
-        break;
-    }
+    if (auto* client = pending_layer.GetContentLayerClient())
+      tracking = client->GetRasterInvalidator().GetTracking();
     UpdateLayerDebugInfo(
-        *layer, pending_layer.FirstPaintChunk().id,
+        layer, pending_layer.FirstPaintChunk().id,
         pending_layer.Chunks().GetPaintArtifact(),
         GetCompositingReasons(pending_layer, previous_pending_layer), tracking);
     previous_pending_layer = &pending_layer;
@@ -1220,7 +977,7 @@
     const PendingLayer* previous_layer) const {
   DCHECK(layer_debug_info_enabled_);
 
-  if (layer.RequiresOwnLayer()) {
+  if (layer.ChunkRequiresOwnLayer()) {
     if (layer.GetCompositingType() == PendingLayer::kScrollHitTestLayer)
       return CompositingReason::kOverflowScrolling;
     switch (layer.FirstDisplayItem().GetType()) {
@@ -1313,28 +1070,26 @@
 }
 
 size_t PaintArtifactCompositor::ApproximateUnsharedMemoryUsage() const {
-  size_t result = sizeof(*this) + content_layer_clients_.CapacityInBytes() +
-                  synthesized_clip_cache_.CapacityInBytes() +
-                  scroll_hit_test_layers_.CapacityInBytes() +
-                  scrollbar_layers_.CapacityInBytes() +
+  size_t result = sizeof(*this) + synthesized_clip_cache_.CapacityInBytes() +
                   pending_layers_.CapacityInBytes();
 
-  for (auto& client : content_layer_clients_)
-    result += client->ApproximateUnsharedMemoryUsage();
-
   for (auto& layer : pending_layers_) {
+    if (auto* client = layer.GetContentLayerClient())
+      result += client->ApproximateUnsharedMemoryUsage();
     size_t chunks_size = layer.Chunks().ApproximateUnsharedMemoryUsage();
     DCHECK_GE(chunks_size, sizeof(layer.Chunks()));
     result += chunks_size - sizeof(layer.Chunks());
   }
+
   return result;
 }
 
 void PaintArtifactCompositor::SetScrollbarNeedsDisplay(
     CompositorElementId element_id) {
-  for (auto& layer : scrollbar_layers_) {
-    if (layer->element_id() == element_id) {
-      layer->SetNeedsDisplay();
+  for (auto& pending_layer : pending_layers_) {
+    if (pending_layer.GetCompositingType() == PendingLayer::kScrollbarLayer &&
+        pending_layer.CcLayer().element_id() == element_id) {
+      pending_layer.CcLayer().SetNeedsDisplay();
       return;
     }
   }
@@ -1364,4 +1119,16 @@
 }
 #endif
 
+ContentLayerClientImpl* PaintArtifactCompositor::ContentLayerClientForTesting(
+    wtf_size_t i) const {
+  for (auto& pending_layer : pending_layers_) {
+    if (auto* client = pending_layer.GetContentLayerClient()) {
+      if (i == 0)
+        return client;
+      --i;
+    }
+  }
+  return nullptr;
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.h b/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.h
index c041cd7..1eed2ef 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.h
+++ b/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.h
@@ -10,7 +10,6 @@
 #include "base/dcheck_is_on.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/scoped_refptr.h"
-#include "cc/input/layer_selection_bound.h"
 #include "cc/layers/content_layer_client.h"
 #include "cc/layers/layer_collections.h"
 #include "cc/layers/picture_layer.h"
@@ -30,7 +29,6 @@
 #endif
 
 namespace cc {
-class ScrollbarLayerBase;
 class DocumentTransitionRequest;
 }
 
@@ -189,10 +187,8 @@
   void ShowDebugData();
 #endif
 
-  const Vector<std::unique_ptr<ContentLayerClientImpl>>&
-  ContentLayerClientsForTesting() const {
-    return content_layer_clients_;
-  }
+  // Returns the ith ContentLayerClientImpl for testing.
+  ContentLayerClientImpl* ContentLayerClientForTesting(wtf_size_t i) const;
 
   // Mark this as needing a full compositing update. Repaint-only updates that
   // do not affect compositing can use a fast-path in |UpdateRepaintedLayers|
@@ -233,18 +229,6 @@
   void SetScrollbarNeedsDisplay(CompositorElementId element_id);
 
  private:
-  static void UpdateLayerProperties(cc::Layer&, const PendingLayer&);
-  static void UpdateLayerSelection(cc::Layer&,
-                                   const PendingLayer&,
-                                   cc::LayerSelection& layer_selection);
-
-  // Updates |content_layer_client| associated with a |pending_layer| following
-  // a paint. This includes updating the drawings and raster invalidation.
-  void UpdateRepaintedContentLayerClient(
-      const PendingLayer& pending_layer,
-      bool pending_layer_chunks_unchanged,
-      ContentLayerClientImpl& content_layer_client);
-
   // Collects the PaintChunks into groups which will end up in the same
   // cc layer. This is the entry point of the layerization algorithm.
   void CollectPendingLayers(scoped_refptr<const PaintArtifact>);
@@ -275,37 +259,9 @@
                          const EffectPaintPropertyNode& effect,
                          wtf_size_t layer_index);
 
-  // Builds a leaf layer that represents a single paint chunk.
-  scoped_refptr<cc::Layer> CompositedLayerForPendingLayer(
-      const PendingLayer&,
-      Vector<std::unique_ptr<ContentLayerClientImpl>>&
-          new_content_layer_clients,
-      Vector<scoped_refptr<cc::Layer>>& new_scroll_hit_test_layers,
-      Vector<scoped_refptr<cc::ScrollbarLayerBase>>& new_scrollbar_layers);
-
   const TransformPaintPropertyNode& NearestScrollTranslationForLayer(
       const PendingLayer&);
 
-  // Returns the cc::Layer if the pending layer contains a foreign layer or a
-  // wrapper of a GraphicsLayer. If it's the latter and the graphics layer has
-  // been repainted, also updates the layer properties.
-  scoped_refptr<cc::Layer> WrappedCcLayerForPendingLayer(const PendingLayer&);
-
-  // Finds an existing or creates a new scroll hit test layer for the pending
-  // layer, returning nullptr if the layer is not a scroll hit test layer.
-  scoped_refptr<cc::Layer> ScrollHitTestLayerForPendingLayer(
-      const PendingLayer&);
-
-  // Finds an existing or creates a new scrollbar layer for the pending layer,
-  // returning nullptr if the layer is not a scrollbar layer.
-  scoped_refptr<cc::ScrollbarLayerBase> ScrollbarLayerForPendingLayer(
-      const PendingLayer&);
-
-  // Finds a client among the current vector of clients that matches the paint
-  // chunk's id, or otherwise allocates a new one.
-  std::unique_ptr<ContentLayerClientImpl> ClientForPaintChunk(
-      const PaintChunk&);
-
   // if |needs_layer| is false, no cc::Layer is created, |mask_effect_id| is
   // not set, and the Layer() method on the returned SynthesizedClip returns
   // nullptr.
@@ -340,7 +296,6 @@
   bool prefers_lcd_text_ = false;
 
   scoped_refptr<cc::Layer> root_layer_;
-  Vector<std::unique_ptr<ContentLayerClientImpl>> content_layer_clients_;
   struct SynthesizedClipEntry {
     const ClipPaintPropertyNode* key;
     std::unique_ptr<SynthesizedClip> synthesized_clip;
@@ -348,10 +303,9 @@
   };
   Vector<SynthesizedClipEntry> synthesized_clip_cache_;
 
-  Vector<scoped_refptr<cc::Layer>> scroll_hit_test_layers_;
-  Vector<scoped_refptr<cc::ScrollbarLayerBase>> scrollbar_layers_;
-
-  Vector<PendingLayer, 0> pending_layers_;
+  using PendingLayers = Vector<PendingLayer, 0>;
+  class OldPendingLayerMatcher;
+  PendingLayers pending_layers_;
 
   friend class StubChromeClientForCAP;
   friend class PaintArtifactCompositorTest;
diff --git a/third_party/blink/renderer/platform/graphics/compositing/pending_layer.cc b/third_party/blink/renderer/platform/graphics/compositing/pending_layer.cc
index 44e6bf7..703aa552 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/pending_layer.cc
+++ b/third_party/blink/renderer/platform/graphics/compositing/pending_layer.cc
@@ -4,8 +4,11 @@
 
 #include "third_party/blink/renderer/platform/graphics/compositing/pending_layer.h"
 
+#include "cc/layers/scrollbar_layer_base.h"
 #include "third_party/blink/renderer/platform/geometry/geometry_as_json.h"
+#include "third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.h"
 #include "third_party/blink/renderer/platform/graphics/paint/drawing_display_item.h"
+#include "third_party/blink/renderer/platform/graphics/paint/foreign_layer_display_item.h"
 #include "third_party/blink/renderer/platform/graphics/paint/geometry_mapper.h"
 #include "third_party/blink/renderer/platform/wtf/hash_set.h"
 #include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
@@ -88,7 +91,7 @@
       property_tree_state_(
           first_chunk.properties.GetPropertyTreeState().Unalias()),
       compositing_type_(kOther) {
-  DCHECK(!RequiresOwnLayer() || first_chunk.size() <= 1u);
+  DCHECK(!ChunkRequiresOwnLayer() || first_chunk.size() <= 1u);
   // Though text_known_to_be_on_opaque_background is only meaningful when
   // has_text is true, we expect text_known_to_be_on_opaque_background to be
   // true when !has_text to simplify code.
@@ -173,7 +176,7 @@
 }
 
 void PendingLayer::Upcast(const PropertyTreeState& new_state) {
-  DCHECK(!RequiresOwnLayer());
+  DCHECK(!ChunkRequiresOwnLayer());
   if (property_tree_state_ == new_state)
     return;
 
@@ -187,21 +190,22 @@
 }
 
 const PaintChunk& PendingLayer::FirstPaintChunk() const {
-  DCHECK(!RequiresOwnLayer() || chunks_.size() == 1);
   return *chunks_.begin();
 }
 
 const DisplayItem& PendingLayer::FirstDisplayItem() const {
-#if DCHECK_IS_ON()
-  // This method should never be called if the first paint chunk is empty.
-  if (RequiresOwnLayer())
-    DCHECK_EQ(FirstPaintChunk().size(), 1u);
-  else
-    DCHECK_GE(FirstPaintChunk().size(), 1u);
-#endif
   return *chunks_.begin().DisplayItems().begin();
 }
 
+bool PendingLayer::Matches(const PendingLayer& old_pending_layer) const {
+  if (ChunkRequiresOwnLayer() != old_pending_layer.ChunkRequiresOwnLayer())
+    return false;
+  if (ChunkRequiresOwnLayer() &&
+      compositing_type_ != old_pending_layer.compositing_type_)
+    return false;
+  return FirstPaintChunk().Matches(old_pending_layer.FirstPaintChunk());
+}
+
 // We will only allow merging if
 // merged_area - (home_area + guest_area) <= kMergeSparsityAreaTolerance
 static constexpr float kMergeSparsityAreaTolerance = 10000;
@@ -212,7 +216,7 @@
                                  bool dry_run) {
   if (&Chunks().GetPaintArtifact() != &guest.Chunks().GetPaintArtifact())
     return false;
-  if (RequiresOwnLayer() || guest.RequiresOwnLayer())
+  if (ChunkRequiresOwnLayer() || guest.ChunkRequiresOwnLayer())
     return false;
   if (&GetPropertyTreeState().Effect() != &guest_state.Effect())
     return false;
@@ -455,6 +459,176 @@
   }
 }
 
+void PendingLayer::UpdateForeignLayer() {
+  DCHECK_EQ(compositing_type_, PendingLayer::kForeignLayer);
+
+  // UpdateTouchActionRects() depends on the layer's offset, but when the
+  // layer's offset changes, we do not call SetNeedsUpdate() (this is an
+  // optimization because the update would only cause an extra commit) This is
+  // only OK if the ForeignLayer doesn't have hit test data.
+  DCHECK(!FirstPaintChunk().hit_test_data);
+  const auto& foreign_layer_display_item =
+      To<ForeignLayerDisplayItem>(FirstDisplayItem());
+
+  gfx::Vector2dF layer_offset(
+      foreign_layer_display_item.VisualRect().OffsetFromOrigin());
+  cc_layer_ = foreign_layer_display_item.GetLayer();
+  cc_layer_->SetOffsetToTransformParent(layer_offset +
+                                        offset_of_decomposited_transforms_);
+}
+
+void PendingLayer::UpdateScrollHitTestLayer(PendingLayer* old_pending_layer) {
+  DCHECK_EQ(compositing_type_, kScrollHitTestLayer);
+
+  // We shouldn't decomposite scroll transform nodes.
+  DCHECK_EQ(gfx::Vector2dF(), offset_of_decomposited_transforms_);
+
+  const auto& scroll_node =
+      *ScrollTranslationForScrollHitTestLayer().ScrollNode();
+
+  DCHECK(!cc_layer_);
+  if (old_pending_layer)
+    cc_layer_ = std::move(old_pending_layer->cc_layer_);
+
+  if (cc_layer_) {
+    DCHECK_EQ(cc_layer_->element_id(), scroll_node.GetCompositorElementId());
+  } else {
+    cc_layer_ = cc::Layer::Create();
+    cc_layer_->SetElementId(scroll_node.GetCompositorElementId());
+    cc_layer_->SetHitTestable(true);
+  }
+
+  cc_layer_->SetOffsetToTransformParent(
+      gfx::Vector2dF(scroll_node.ContainerRect().OffsetFromOrigin()));
+  // TODO(pdr): The scroll layer's bounds are currently set to the clipped
+  // container bounds but this does not include the border. We may want to
+  // change this behavior to make non-composited and composited hit testing
+  // match (see: crbug.com/753124). To do this, use
+  // |scroll_hit_test->scroll_container_bounds|. Set the layer's bounds equal
+  // to the container because the scroll layer does not scroll.
+  cc_layer_->SetBounds(scroll_node.ContainerRect().size());
+
+  if (scroll_node.NodeChanged() != PaintPropertyChangeType::kUnchanged) {
+    cc_layer_->SetNeedsPushProperties();
+    cc_layer_->SetNeedsCommit();
+  }
+}
+
+void PendingLayer::UpdateScrollbarLayer(PendingLayer* old_pending_layer) {
+  DCHECK_EQ(compositing_type_, kScrollbarLayer);
+
+  const auto& item = FirstDisplayItem();
+  DCHECK(item.IsScrollbar());
+
+  const auto& scrollbar_item = To<ScrollbarDisplayItem>(item);
+  scoped_refptr<cc::ScrollbarLayerBase> scrollbar_layer;
+  if (old_pending_layer) {
+    scrollbar_layer = static_cast<cc::ScrollbarLayerBase*>(
+        std::move(old_pending_layer->cc_layer_).get());
+  }
+
+  scrollbar_layer = scrollbar_item.CreateOrReuseLayer(scrollbar_layer.get());
+  scrollbar_layer->SetOffsetToTransformParent(
+      scrollbar_layer->offset_to_transform_parent() +
+      gfx::Vector2dF(offset_of_decomposited_transforms_));
+  DCHECK(!cc_layer_);
+  cc_layer_ = std::move(scrollbar_layer);
+}
+
+void PendingLayer::UpdateContentLayer(PendingLayer* old_pending_layer,
+                                      bool tracks_raster_invalidations) {
+  DCHECK(!ChunkRequiresOwnLayer());
+  DCHECK(!content_layer_client_);
+  if (old_pending_layer)
+    content_layer_client_ = std::move(old_pending_layer->content_layer_client_);
+  if (!content_layer_client_) {
+    content_layer_client_ = std::make_unique<ContentLayerClientImpl>();
+    content_layer_client_->GetRasterInvalidator().SetTracksRasterInvalidations(
+        tracks_raster_invalidations);
+  }
+  content_layer_client_->UpdateCcPictureLayer(*this);
+}
+
+void PendingLayer::UpdateCompositedLayer(PendingLayer* old_pending_layer,
+                                         cc::LayerSelection& layer_selection,
+                                         bool tracks_raster_invalidations) {
+  switch (compositing_type_) {
+    case PendingLayer::kForeignLayer:
+      UpdateForeignLayer();
+      break;
+    case PendingLayer::kScrollHitTestLayer:
+      UpdateScrollHitTestLayer(old_pending_layer);
+      break;
+    case PendingLayer::kScrollbarLayer:
+      UpdateScrollbarLayer(old_pending_layer);
+      break;
+    default:
+      DCHECK(!ChunkRequiresOwnLayer());
+      UpdateContentLayer(old_pending_layer, tracks_raster_invalidations);
+      break;
+  }
+
+  UpdateLayerProperties();
+  UpdateLayerSelection(layer_selection);
+}
+
+void PendingLayer::UpdateCompositedLayerForRepaint(
+    scoped_refptr<const PaintArtifact> repainted_artifact,
+    cc::LayerSelection& layer_selection) {
+  // Essentially replace the paint chunks of the pending layer with the
+  // repainted chunks in |repainted_artifact|. The pending layer's paint
+  // chunks (a |PaintChunkSubset|) actually store indices to |PaintChunk|s
+  // in a |PaintArtifact|. In repaint updates, chunks are not added,
+  // removed, or re-ordered, so we can simply swap in a repainted
+  // |PaintArtifact| instead of copying |PaintChunk|s individually.
+  const PaintArtifact& old_artifact = Chunks().GetPaintArtifact();
+  DCHECK_EQ(old_artifact.PaintChunks().size(),
+            repainted_artifact->PaintChunks().size());
+  SetPaintArtifact(std::move(repainted_artifact));
+
+  bool chunks_unchanged = true;
+  for (const auto& chunk : Chunks()) {
+    if (!chunk.is_moved_from_cached_subsequence) {
+      chunks_unchanged = false;
+      break;
+    }
+  }
+
+  if (!ChunkRequiresOwnLayer()) {
+    DCHECK(content_layer_client_);
+    // Checking |pending_layer_chunks_unchanged| is an optimization to avoid
+    // the expensive call to |UpdateCcPictureLayer| when no repainting occurs
+    // for this PendingLayer.
+    if (chunks_unchanged) {
+      // See RasterInvalidator::SetOldPaintArtifact() for the reason for this.
+      content_layer_client_->GetRasterInvalidator().SetOldPaintArtifact(
+          &Chunks().GetPaintArtifact());
+    } else {
+      content_layer_client_->UpdateCcPictureLayer(*this);
+    }
+  }
+
+  if (!chunks_unchanged)
+    UpdateLayerProperties();
+  UpdateLayerSelection(layer_selection);
+}
+
+void PendingLayer::UpdateLayerProperties() {
+  // Properties of foreign layers are managed by their owners.
+  if (compositing_type_ == PendingLayer::kForeignLayer)
+    return;
+  PaintChunksToCcLayer::UpdateLayerProperties(CcLayer(), GetPropertyTreeState(),
+                                              Chunks());
+}
+
+void PendingLayer::UpdateLayerSelection(cc::LayerSelection& layer_selection) {
+  // Foreign layers cannot contain selection.
+  if (compositing_type_ == PendingLayer::kForeignLayer)
+    return;
+  PaintChunksToCcLayer::UpdateLayerSelection(CcLayer(), GetPropertyTreeState(),
+                                             Chunks(), layer_selection);
+}
+
 bool PendingLayer::IsSolidColor() const {
   if (Chunks().size() != 1)
     return false;
diff --git a/third_party/blink/renderer/platform/graphics/compositing/pending_layer.h b/third_party/blink/renderer/platform/graphics/compositing/pending_layer.h
index 969dcea..df4af518 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/pending_layer.h
+++ b/third_party/blink/renderer/platform/graphics/compositing/pending_layer.h
@@ -5,6 +5,8 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_GRAPHICS_COMPOSITING_PENDING_LAYER_H_
 #define THIRD_PARTY_BLINK_RENDERER_PLATFORM_GRAPHICS_COMPOSITING_PENDING_LAYER_H_
 
+#include "cc/input/layer_selection_bound.h"
+#include "third_party/blink/renderer/platform/graphics/compositing/content_layer_client_impl.h"
 #include "third_party/blink/renderer/platform/graphics/paint/paint_chunk_subset.h"
 #include "third_party/blink/renderer/platform/graphics/paint/property_tree_state.h"
 #include "ui/gfx/geometry/rect_f.h"
@@ -88,6 +90,8 @@
   const PaintChunk& FirstPaintChunk() const;
   const DisplayItem& FirstDisplayItem() const;
 
+  bool Matches(const PendingLayer& old_pending_layer) const;
+
   const TransformPaintPropertyNode& ScrollTranslationForScrollHitTestLayer()
       const;
 
@@ -96,8 +100,18 @@
   void ForceDrawsContent() { draws_content_ = true; }
   bool DrawsContent() const { return draws_content_; }
 
-  bool RequiresOwnLayer() const {
-    return compositing_type_ != kOverlap && compositing_type_ != kOther;
+  bool ChunkRequiresOwnLayer() const {
+    bool result = compositing_type_ != kOverlap && compositing_type_ != kOther;
+#if DCHECK_IS_ON()
+    if (result) {
+      DCHECK(!content_layer_client_);
+      DCHECK_EQ(chunks_.size(), 1u);
+    } else {
+      DCHECK(!cc_layer_);
+      DCHECK_GE(chunks_.size(), 1u);
+    }
+#endif
+    return result;
   }
 
   bool PropertyTreeStateChanged() const;
@@ -106,6 +120,32 @@
 
   static void DecompositeTransforms(Vector<PendingLayer>& pending_layers);
 
+  // This is valid only when SetCclayer() or SetContentLayerClient() has been
+  // called.
+  cc::Layer& CcLayer() const {
+    if (content_layer_client_)
+      return content_layer_client_->Layer();
+    DCHECK(cc_layer_);
+    return *cc_layer_;
+  }
+
+  ContentLayerClientImpl* GetContentLayerClient() const {
+    return content_layer_client_.get();
+  }
+
+  // For this PendingLayer, creates a composited layer or uses the existing
+  // one in |old_pending_layer|, and updates the layer according to the current
+  // contents and properties of this PendingLayer.
+  void UpdateCompositedLayer(PendingLayer* old_pending_layer,
+                             cc::LayerSelection&,
+                             bool tracks_raster_invalidations);
+
+  // A lighter version of UpdateCompositedLayer(). Called when the existing
+  // composited layer has only repainted since the last update.
+  void UpdateCompositedLayerForRepaint(
+      scoped_refptr<const PaintArtifact> repainted_artifact,
+      cc::LayerSelection&);
+
  private:
   PendingLayer(const PaintChunkSubset&,
                const PaintChunk& first_chunk,
@@ -121,6 +161,17 @@
   // True if this contains only a single solid color DrawingDisplayItem.
   bool IsSolidColor() const;
 
+  // The following methods are called by UpdateCompositedLayer(), each for a
+  // particular type of composited layer.
+  void UpdateForeignLayer();
+  void UpdateScrollHitTestLayer(PendingLayer* old_pending_layer);
+  void UpdateScrollbarLayer(PendingLayer* old_pending_layer);
+  void UpdateContentLayer(PendingLayer* old_pending_layer,
+                          bool tracks_raster_invalidations);
+
+  void UpdateLayerProperties();
+  void UpdateLayerSelection(cc::LayerSelection&);
+
   // The rects are in the space of property_tree_state.
   gfx::RectF bounds_;
   gfx::RectF rect_known_to_be_opaque_;
@@ -133,6 +184,11 @@
   PaintPropertyChangeType change_of_decomposited_transforms_ =
       PaintPropertyChangeType::kUnchanged;
   CompositingType compositing_type_;
+
+  // This is set to non-null after layerization if ChunkRequiresOwnLayer().
+  scoped_refptr<cc::Layer> cc_layer_;
+  // This is set to non-null after layerization if !ChunkRequiresOwnLayer().
+  std::unique_ptr<ContentLayerClientImpl> content_layer_client_;
 };
 }  // namespace blink
 
diff --git a/third_party/blink/renderer/platform/graphics/paint/paint_chunk.h b/third_party/blink/renderer/platform/graphics/paint/paint_chunk.h
index 4de8a024..70b7bae 100644
--- a/third_party/blink/renderer/platform/graphics/paint/paint_chunk.h
+++ b/third_party/blink/renderer/platform/graphics/paint/paint_chunk.h
@@ -88,8 +88,8 @@
     return old.is_cacheable && Matches(old.id);
   }
 
-  bool Matches(const Id& other_id) const {
-    if (!is_cacheable || id != other_id)
+  bool CanMatchOldChunk() const {
+    if (!is_cacheable)
       return false;
     // A chunk whose client is just created should not match any cached chunk,
     // even if it's id equals the old chunk's id (which may happen if this
@@ -98,6 +98,10 @@
     return !client_is_just_created;
   }
 
+  bool Matches(const Id& other_id) const {
+    return CanMatchOldChunk() && id == other_id;
+  }
+
   bool EqualsForUnderInvalidationChecking(const PaintChunk& other) const;
 
   HitTestData& EnsureHitTestData() {
diff --git a/third_party/blink/renderer/platform/graphics/paint/raster_invalidator.cc b/third_party/blink/renderer/platform/graphics/paint/raster_invalidator.cc
index f1eee9d..a5d6501 100644
--- a/third_party/blink/renderer/platform/graphics/paint/raster_invalidator.cc
+++ b/third_party/blink/renderer/platform/graphics/paint/raster_invalidator.cc
@@ -43,7 +43,7 @@
 wtf_size_t RasterInvalidator::MatchNewChunkToOldChunk(
     const PaintChunk& new_chunk,
     wtf_size_t old_index) const {
-  if (!new_chunk.is_cacheable)
+  if (!new_chunk.CanMatchOldChunk())
     return kNotFound;
 
   for (wtf_size_t i = old_index; i < old_paint_chunks_info_.size(); i++) {
diff --git a/third_party/blink/renderer/platform/network/http_names.json5 b/third_party/blink/renderer/platform/network/http_names.json5
index 9d7ec51..63880453 100644
--- a/third_party/blink/renderer/platform/network/http_names.json5
+++ b/third_party/blink/renderer/platform/network/http_names.json5
@@ -24,6 +24,7 @@
     "Access-Control-Request-Method",
     "Allow-CSP-From",
     "Attribution-Reporting-Register-Aggregatable-Source",
+    "Attribution-Reporting-Register-Event-Trigger",
     "Attribution-Reporting-Register-Source",
     "Cache-Control",
     "Content-DPR",
diff --git a/third_party/blink/web_tests/document-transition/multiple-shared-elements-animate-crash.html b/third_party/blink/web_tests/document-transition/multiple-shared-elements-animate-crash.html
index afd028e..b9ff763 100644
--- a/third_party/blink/web_tests/document-transition/multiple-shared-elements-animate-crash.html
+++ b/third_party/blink/web_tests/document-transition/multiple-shared-elements-animate-crash.html
@@ -46,16 +46,16 @@
 async_test((t) => {
   t.step(() => {
     requestAnimationFrame(() => requestAnimationFrame(async () => {
-      document.createDocumentTransition(async (transition) => {
-        transition.setElement(e1, "e1");
-        transition.setElement(e2, "e2");
+      await document.documentTransition.prepare({
+        rootTransition: "none",
+        sharedElements: [e1, e2]
+      });
 
-        await transition.captureAndHold();
+      container.classList.remove("left");
+      container.classList.add("right");
 
-        container.classList.remove("left");
-        container.classList.add("right");
-
-        await transition.start();
+      await document.documentTransition.start({
+        sharedElements: [e1, e2]
       });
 
       requestAnimationFrame(() => {
diff --git a/third_party/blink/web_tests/document-transition/paint-order.html b/third_party/blink/web_tests/document-transition/paint-order.html
index 1daff159..5590d8c 100644
--- a/third_party/blink/web_tests/document-transition/paint-order.html
+++ b/third_party/blink/web_tests/document-transition/paint-order.html
@@ -9,12 +9,13 @@
   background-color: blue;
   contain: paint;
 }
-html::page-transition-outgoing-image(shared) {
+html::page-transition-container(shared-0) { isolation: isolate; }
+html::page-transition-outgoing-image(shared-0) {
   opacity: 1;
   mix-blend-mode: normal;
   animation: unset;
 }
-html::page-transition-incoming-image(shared) {
+html::page-transition-incoming-image(shared-0) {
   opacity: 1;
   mix-blend-mode: multiply;
   animation: unset;
@@ -32,19 +33,20 @@
 async function doTransition() {
   let elem = document.getElementsByTagName("div")[0];
 
-  document.createDocumentTransition(async (t) => {
-    t.setElement(elem, "shared");
-    await t.captureAndHold();
-
-    elem.style.backgroundColor = "red";
-
-    await t.start();
-
-    if (window.testRunner) {
-      requestAnimationFrame(() => requestAnimationFrame(() => testRunner.notifyDone()));
-    }
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: [elem]
   });
+
+  elem.style.backgroundColor = "red";
+  await document.documentTransition.start({
+    sharedElements: [elem]
+  });
+
+  if (window.testRunner) {
+    requestAnimationFrame(() => requestAnimationFrame(() => testRunner.notifyDone()));
+  }
 }
 
-onload = requestAnimationFrame(() => requestAnimationFrame(doTransition));
+onload = doTransition;
 </script>
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-clip-path.html b/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-clip-path.html
index 893b1856..9a578cb 100644
--- a/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-clip-path.html
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-clip-path.html
@@ -34,14 +34,15 @@
 <div id=e1 class=box></div>
 <script>
 async function runTest() {
-  document.createDocumentTransition(async (t) => {
-    t.setElement(e1, "e1");
-    await t.captureAndHold();
-    e1.classList.add("dst");
-    t.start();
-
-    requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: [e1]
   });
+  e1.classList.add("dst");
+  document.documentTransition.start({
+    sharedElements: [e1]
+  });
+  requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
 }
 onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
 </script>
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-different-size.html b/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-different-size.html
index e854acb..6cd4cb8 100644
--- a/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-different-size.html
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-different-size.html
@@ -45,17 +45,15 @@
 <div id=e3 class=box>three</div>
 <script>
 async function runTest() {
-  document.createDocumentTransition(async (t) => {
-    t.setElement(e1, "e1");
-    t.setElement(e2, "e2");
-    t.setElement(e3, "e3");
-    await t.captureAndHold();
-
-    e1.classList.add("dst");
-    e2.classList.add("dst");
-    e3.classList.add("dst");
-
-    t.start();
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: [e1, e2, e3]
+  });
+  e1.classList.add("dst");
+  e2.classList.add("dst");
+  e3.classList.add("dst");
+  document.documentTransition.start({
+    sharedElements: [e1, e2, e3]
   });
   requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
 }
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-opacity.html b/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-opacity.html
index f1a79e9..72f348a0 100644
--- a/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-opacity.html
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-opacity.html
@@ -33,20 +33,17 @@
 <div id=e3 class=box></div>
 <script>
 async function runTest() {
-  document.createDocumentTransition(async (t) => {
-    t.setElement(e1, "e1");
-    t.setElement(e2, "e2");
-    t.setElement(e3, "e3");
-    await t.captureAndHold();
-
-    e1.classList.add("dst");
-    e2.classList.add("dst");
-    e3.classList.add("dst");
-
-    t.start();
-
-    requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: [e1, e2, e3]
   });
+  e1.classList.add("dst");
+  e2.classList.add("dst");
+  e3.classList.add("dst");
+  document.documentTransition.start({
+    sharedElements: [e1, e2, e3]
+  });
+  requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
 }
 onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
 </script>
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-root-ref.html b/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-root-ref.html
deleted file mode 100644
index d649fc5f..0000000
--- a/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-root-ref.html
+++ /dev/null
@@ -1,20 +0,0 @@
-<!DOCTYPE html>
-<title>Shared transitions: capture opacity elements (ref)</title>
-<link rel="help" href="https://github.com/WICG/shared-element-transitions">
-<link rel="author" href="mailto:vmpstr@chromium.org">
-<style>
-.box {
-  background: lightgreen;
-  width: 100px;
-  height: 100px;
-  contain: paint;
-  position: absolute;
-  will-change: transform;
-}
-#e1 {
-  top: 10px;
-  left: 30px;
-}
-</style>
-<div id=e1 class=box></div>
-
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-root.html b/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-root.html
deleted file mode 100644
index 8f1ae23..0000000
--- a/third_party/blink/web_tests/wpt_internal/document-transition/new-content-captures-root.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<html class=reftest-wait>
-<title>Shared transitions: capture root elements</title>
-<link rel="help" href="https://github.com/WICG/shared-element-transitions">
-<link rel="author" href="mailto:vmpstr@chromium.org">
-<link rel="match" href="new-content-captures-root-ref.html">
-<script src="/common/reftest-wait.js"></script>
-<style>
-.box {
-  background: lightblue;
-  width: 100px;
-  height: 100px;
-  contain: paint;
-  position: absolute;
-  will-change: transform;
-}
-#e1 {
-  top: 10px;
-  left: 30px;
-}
-#shared {
-  contain: paint;
-  width: 100px;
-  height: 100px;
-  background: red;
-}
-
-div.dst { background: lightgreen; }
-/* We're verifying what we capture, so just display the old contents for 5 minutes.  */
-html::page-transition { background: pink; }
-html::page-transition-container(shared) { animation-duration: 300s; }
-html::page-transition-image-wrapper(shared) { visibility: hidden }
-html::page-transition-outgoing-image(root) { animation-duration: 0s; opacity: 0 }
-html::page-transition-incoming-image(root) { animation-duration: 0s; opacity: 1 }
-</style>
-<div id=e1 class=box></div>
-<div id=shared></div>
-<script>
-async function runTest() {
-  document.createDocumentTransition(async (t) => {
-    t.setElement(shared, "shared");
-
-    await t.captureAndHold();
-
-    e1.classList.add("dst");
-
-    t.start();
-
-    requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
-  });
-}
-onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
-</script>
-
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-clip-path.html b/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-clip-path.html
index 9958196..30e7af5 100644
--- a/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-clip-path.html
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-clip-path.html
@@ -33,16 +33,15 @@
 <div id=e1 class=box></div>
 <script>
 async function runTest() {
-  document.createDocumentTransition(async (t) => {
-    t.setElement(e1, "e1");
-    await t.captureAndHold();
-
-    e1.classList.add("dst");
-
-    t.start();
-
-    requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: [e1]
   });
+  e1.classList.add("dst");
+  document.documentTransition.start({
+    sharedElements: [e1]
+  });
+  requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
 }
 onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
 </script>
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-different-size.html b/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-different-size.html
index 2e5c66b4d..2c8677a 100644
--- a/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-different-size.html
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-different-size.html
@@ -45,20 +45,17 @@
 <div id=e3 class=box>three</div>
 <script>
 async function runTest() {
-  document.createDocumentTransition(async (t) => {
-    t.setElement(e1, "e1");
-    t.setElement(e2, "e2");
-    t.setElement(e3, "e3");
-    await t.captureAndHold();
-
-    e1.classList.add("dst");
-    e2.classList.add("dst");
-    e3.classList.add("dst");
-
-    t.start();
-
-    requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: [e1, e2, e3]
   });
+  e1.classList.add("dst");
+  e2.classList.add("dst");
+  e3.classList.add("dst");
+  document.documentTransition.start({
+    sharedElements: [e1, e2, e3]
+  });
+  requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
 }
 onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
 </script>
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-opacity.html b/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-opacity.html
index db57c1b..5f02095 100644
--- a/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-opacity.html
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-opacity.html
@@ -34,20 +34,17 @@
 <div id=e3 class=box>three</div>
 <script>
 async function runTest() {
-  document.createDocumentTransition(async (t) => {
-    t.setElement(e1, "e1");
-    t.setElement(e2, "e2");
-    t.setElement(e3, "e3");
-    await t.captureAndHold();
-
-    e1.classList.add("dst");
-    e2.classList.add("dst");
-    e3.classList.add("dst");
-
-    t.start();
-
-    requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: [e1, e2, e3]
   });
+  e1.classList.add("dst");
+  e2.classList.add("dst");
+  e3.classList.add("dst");
+  document.documentTransition.start({
+    sharedElements: [e1, e2, e3]
+  });
+  requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
 }
 onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
 </script>
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-root.html b/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-root.html
index ce187204..dac3abab 100644
--- a/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-root.html
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/old-content-captures-root.html
@@ -24,12 +24,11 @@
   height: 100px;
   background: red;
 }
-
 div.dst { background: lightgreen; }
 /* We're verifying what we capture, so just display the old contents for 5 minutes.  */
 html::page-transition { background: pink; }
-html::page-transition-container(shared) { animation-duration: 300s; }
-html::page-transition-image-wrapper(shared) { visibility: hidden }
+html::page-transition-container(shared-0) { animation-duration: 300s; }
+html::page-transition-image-wrapper(shared-0) { visibility: hidden }
 html::page-transition-outgoing-image(root) { animation: unset; opacity: 1 }
 html::page-transition-incoming-image(root) { animation: unset; opacity: 0 }
 </style>
@@ -37,17 +36,17 @@
 <div id=shared></div>
 <script>
 async function runTest() {
-  document.createDocumentTransition(async (t) => {
-    t.setElement(shared, "shared");
-
-    await t.captureAndHold();
-
-    e1.classList.add("dst");
-
-    t.start();
-
-    requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: [shared]
   });
+  e1.classList.add("dst");
+  document.documentTransition.start({
+    sharedElements: [shared]
+  });
+
+  requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
 }
 onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
 </script>
+
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-author-style.manual.html b/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-author-style.manual.html
index 38e8c7c..00b8128c 100644
--- a/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-author-style.manual.html
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-author-style.manual.html
@@ -57,16 +57,16 @@
     background-color: grey;
   }
 
-  html::page-transition-container(target) {
+  html::page-transition-container(shared-0) {
     left: 50px;
   }
 
-  html::page-transition-outgoing-image(target) {
+  html::page-transition-outgoing-image(shared-0) {
     opacity: 0.5;
     animation-name: none;
   }
 
-  html::page-transition-incoming-image(target) {
+  html::page-transition-incoming-image(shared-0) {
     opacity: 0.5;
   }
   `
@@ -75,20 +75,20 @@
 pseudoStyle.appendChild(document.createTextNode(transitionStyle));
 
 async function runAnimation() {
-  document.createDocumentTransition(async (t) => {
-    t.setElement(target, "target");
-    await t.captureAndHold();
-
-    target.classList.remove(classes[i]);
-    i = (i + 1) % classes.length;
-    target.classList.add(classes[i]);
-
-    document.head.appendChild(pseudoStyle);
-
-    await t.start();
-
-    document.head.removeChild(pseudoStyle);
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: [target]
   });
+
+  target.classList.remove(classes[i]);
+  i = (i + 1) % classes.length;
+  target.classList.add(classes[i]);
+
+  document.head.appendChild(pseudoStyle);
+  await document.documentTransition.start({
+    sharedElements: [target]
+  });
+  document.head.removeChild(pseudoStyle);
 }
 
 function init() {
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-half-with-config.manual.html b/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-half-with-config.manual.html
new file mode 100644
index 0000000..0514f2b
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-half-with-config.manual.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+<title>Shared transitions of different elements and shapes</title>
+<link rel="help" href="https://github.com/vmpstr/shared-element-transitions">
+<link rel="author" href="mailto:vmpstr@chromium.org">
+
+<style>
+body {
+  background: lightpink;
+  overflow: hidden;
+}
+
+input {
+  position: absolute;
+  left: 8px;
+  top: 8px;
+  z-index: 10;
+}
+
+.top {
+  top: 0px;
+}
+.bottom {
+  bottom: 0px;
+}
+
+div {
+  position: absolute;
+  left: 0px;
+  right: 0px;
+  height: 40vh;
+  background: green;
+  contain: paint;
+}
+</style>
+
+<input id=toggle type=button value="Toggle!"></input>
+<div id=target class=top>
+The green div should alternate being at the bottom and at the top.
+Other than green and pink background no other colors should appear.
+</div>
+
+<script>
+let classes = ["top", "bottom"]
+let backgroundColors = ["lightpink", "lightblue"]
+let i = 0;
+async function runAnimation() {
+  await document.documentTransition.prepare({
+    rootTransition: "explode",
+    rootConfig: {duration:"500", delay: "500"},
+    sharedElements: [target],
+    sharedElementsConfig: [{duration:"1000", delay:"1000"}]
+  });
+
+  document.body.style.background = backgroundColors[i];
+  target.classList.remove(classes[i]);
+  i = (i + 1) % classes.length;
+  target.classList.add(classes[i]);
+
+  await document.documentTransition.start({
+    sharedElements: [target]
+  });
+}
+
+function init() {
+  toggle.addEventListener("click", runAnimation);
+}
+onload = init;
+</script>
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-half.manual.html b/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-half.manual.html
index 90ee5b8..4c041b6c 100644
--- a/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-half.manual.html
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-half.manual.html
@@ -44,15 +44,17 @@
 let classes = ["top", "bottom"]
 let i = 0;
 async function runAnimation() {
-  document.createDocumentTransition(async (t) => {
-    t.setElement(target, "target");
-    await t.captureAndHold();
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: [target]
+  });
 
-    target.classList.remove(classes[i]);
-    i = (i + 1) % classes.length;
-    target.classList.add(classes[i]);
+  target.classList.remove(classes[i]);
+  i = (i + 1) % classes.length;
+  target.classList.add(classes[i]);
 
-    await t.start();
+  await document.documentTransition.start({
+    sharedElements: [target]
   });
 }
 
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-repeated-elements.manual.html b/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-repeated-elements.manual.html
new file mode 100644
index 0000000..d6251e2a
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-repeated-elements.manual.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html>
+<title>Shared transitions: repeated elements (one -> two elements and back)</title>
+<link rel="help" href="https://github.com/vmpstr/shared-element-transitions">
+<link rel="author" href="mailto:vmpstr@chromium.org">
+
+<style>
+body {
+  background: lightpink;
+}
+
+#container {
+  width: max-content;
+  position: relative;
+}
+
+.hidden { display: none; }
+
+.shape {
+  width: 100px;
+  height: 100px;
+  border-radius: 50%;
+  border: 1px solid black;
+  position: absolute;
+  contain: paint;
+}
+
+#yellow {
+  background: yellow;
+  left: 300px;
+  top: 50px;
+}
+#green {
+  background: green;
+  left: 50px;
+  top: 150px;
+}
+#blue {
+  background: blue;
+  left: 300px;
+  top: 250px;
+}
+</style>
+
+<input id=toggle type=button value="Toggle!"></input>
+<span>One shape becomes two and vice versa</span>
+<div id=green class=shape></div>
+<div id=blue class="shape hidden"></div>
+<div id=yellow class="shape hidden"></div>
+
+<script>
+function visibleSharedElements() {
+  if (green.classList.contains("hidden")) {
+    return [blue, yellow];
+  } else {
+    return [green, green];
+  }
+}
+
+async function runAnimation() {
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: visibleSharedElements()
+  });
+
+  green.classList.toggle("hidden");
+  blue.classList.toggle("hidden");
+  yellow.classList.toggle("hidden");
+
+  await document.documentTransition.start({
+    sharedElements: visibleSharedElements()
+  });
+}
+
+function init() {
+  toggle.addEventListener("click", runAnimation);
+}
+onload = init;
+</script>
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-shapes.manual.html b/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-shapes.manual.html
index 8a06dbad..7de5adbf 100644
--- a/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-shapes.manual.html
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/shared-transition-shapes.manual.html
@@ -60,19 +60,17 @@
 let classes = ["left", "right"]
 let i = 0;
 async function runAnimation() {
-  document.createDocumentTransition(async (t) => {
-    t.setElement(e1, "e1");
-    t.setElement(e2, "e2");
-    t.setElement(e3, "e3");
-    t.setElement(e4, "e4");
-    t.setElement(e5, "e5");
-    await t.captureAndHold();
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: [e1, e2, e3, e4, e5]
+  });
 
-    container.classList.remove(classes[i]);
-    i = (i + 1) % classes.length;
-    container.classList.add(classes[i]);
+  container.classList.remove(classes[i]);
+  i = (i + 1) % classes.length;
+  container.classList.add(classes[i]);
 
-    await t.start();
+  await document.documentTransition.start({
+    sharedElements: [e1, e2, e3, e4, e5]
   });
 }
 
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/transition-waits-for-animations.html b/third_party/blink/web_tests/wpt_internal/document-transition/transition-waits-for-animations.html
index 204e891..a7c75ab3 100644
--- a/third_party/blink/web_tests/wpt_internal/document-transition/transition-waits-for-animations.html
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/transition-waits-for-animations.html
@@ -34,14 +34,16 @@
 
 <script>
 async function startTransition() {
-  document.createDocumentTransition(async (t) => {
-    t.setElement(e1, "e1");
-    await t.captureAndHold();
+  await document.documentTransition.prepare({
+    rootTransition: "none",
+    sharedElements: [e1]
+  });
 
-    e1.classList.remove("left");
-    e1.classList.add("right");
+  e1.classList.remove("left");
+  e1.classList.add("right");
 
-    t.start();
+  document.documentTransition.start({
+    sharedElements: [e1]
   });
 }
 
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/uncontained-transition-crash.html b/third_party/blink/web_tests/wpt_internal/document-transition/uncontained-transition-crash.html
index 1f6c8836..038c5c9 100644
--- a/third_party/blink/web_tests/wpt_internal/document-transition/uncontained-transition-crash.html
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/uncontained-transition-crash.html
@@ -31,22 +31,13 @@
 async function runTest() {
   await waitForAtLeastOneFrame();
   // Prepare with a shared element
-  document.createDocumentTransition(async (t) => {
-    t.setElement(first, "shared");
-    let promise = t.captureAndHold();
-
-    // Force a hit test, which will determine compositing reasons.
-    document.elementFromPoint(0, 0);
-
-    // Now wait for the capture to happen. This will note that we don't have
-    // paint containment and should de-composite the element.
-    await promise;
-
-    t.setElement(first, null);
-    t.setElement(second, "shared");
-
-    t.start();
-  });
+  let promise = document.documentTransition.prepare({ sharedElements: [first] });
+  // Force a hit test, which will determine compositing reasons.
+  document.elementFromPoint(0, 0);
+  // Now wait for the prepare to happen. This will note that we don't have
+  // paint containment and should de-composite the element.
+  await promise;
+  document.documentTransition.start({ sharedElements: [second] });
 
   takeScreenshot();
 }
diff --git a/tools/mb/mb_config.pyl b/tools/mb/mb_config.pyl
index c395644fa..0be53f3 100644
--- a/tools/mb/mb_config.pyl
+++ b/tools/mb/mb_config.pyl
@@ -367,7 +367,7 @@
       'linux-lacros-code-coverage': 'lacros_on_linux_clang_code_coverage',
       'linux-lacros-version-skew-fyi': 'lacros_on_linux_release_not_build_ash_bot',
       'linux-perfetto-rel': 'perfetto_release_bot',
-      'linux-upload-perfetto': 'release_bot',
+      'linux-upload-perfetto': 'release_bot_perfetto_zlib',
       'linux-wpt-fyi-rel': 'release_bot_minimal_symbols_dcheck_always_on',
       'linux-wpt-identity-fyi-rel': 'release_bot_minimal_symbols',
       'linux-wpt-input-fyi-rel': 'release_bot_minimal_symbols',
@@ -377,13 +377,13 @@
       'mac-hermetic-upgrade-rel': 'release_bot',
       'mac-paeverywhere-x64-fyi-dbg': 'debug_bot_paeverywhere_x64',
       'mac-paeverywhere-x64-fyi-rel': 'release_trybot_paeverywhere_x64',
-      'mac-upload-perfetto': 'release_bot',
+      'mac-upload-perfetto': 'release_bot_perfetto_zlib',
       'win-annotator-rel': 'release_bot',
       'win-celab-builder-rel': 'release_bot_minimal_symbols',
       'win-backuprefptr-x86-fyi-rel': 'release_trybot_backuprefptr_x86',
       'win-backuprefptr-x64-fyi-rel': 'release_trybot_backuprefptr_x64',
       'win-pixel-builder-rel': 'release_bot',
-      'win-upload-perfetto': 'release_bot',
+      'win-upload-perfetto': 'release_bot_perfetto_zlib',
       'win10-code-coverage': 'clang_code_coverage',
       'win32-archive-rel-goma-rbe-canary': 'release_bot_x86_minimal_symbols_enable_archive_compression',
       'win32-archive-rel-goma-rbe-latest': 'release_bot_x86_minimal_symbols_enable_archive_compression',
@@ -3087,6 +3087,10 @@
       'release', 'official_optimize_goma', 'fuchsia', 'arm64'
     ],
 
+    'release_bot_perfetto_zlib' : [
+      'release_bot', 'perfetto_zlib',
+    ],
+
     'release_trybot': [
       'release_trybot',
     ],
@@ -3977,6 +3981,10 @@
       'gn_args': 'use_perfetto_client_library=true',
     },
 
+    'perfetto_zlib': {
+      'gn_args': 'enable_perfetto_zlib=true',
+    },
+
     'pgo_phase_0': {
       'mixins': ['strip_absolute_paths_from_debug_symbols'],
       'gn_args': 'chrome_pgo_phase=0'
diff --git a/tools/mb/mb_config_expectations/chromium.fyi.json b/tools/mb/mb_config_expectations/chromium.fyi.json
index da0d7811..c0a74d46 100644
--- a/tools/mb/mb_config_expectations/chromium.fyi.json
+++ b/tools/mb/mb_config_expectations/chromium.fyi.json
@@ -1072,6 +1072,7 @@
   "linux-upload-perfetto": {
     "gn_args": {
       "dcheck_always_on": false,
+      "enable_perfetto_zlib": true,
       "is_component_build": false,
       "is_debug": false,
       "use_goma": true
@@ -1158,6 +1159,7 @@
   "mac-upload-perfetto": {
     "gn_args": {
       "dcheck_always_on": false,
+      "enable_perfetto_zlib": true,
       "is_component_build": false,
       "is_debug": false,
       "use_goma": true
@@ -1219,6 +1221,7 @@
   "win-upload-perfetto": {
     "gn_args": {
       "dcheck_always_on": false,
+      "enable_perfetto_zlib": true,
       "is_component_build": false,
       "is_debug": false,
       "use_goma": true
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index cca404d..4c5a66cb 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -93653,11 +93653,6 @@
   <int value="4" label="STORAGE"/>
 </enum>
 
-<enum name="WebApkUninstallSourceChromeOS">
-  <int value="0" label="Uninstalled from Ash"/>
-  <int value="1" label="Uninstalled from ARC"/>
-</enum>
-
 <enum name="WebApkUpdateRequestQueued">
   <int value="0" label="Queued for the first time"/>
   <int value="1" label="Queued for the second time"/>
diff --git a/tools/metrics/histograms/metadata/arc/histograms.xml b/tools/metrics/histograms/metadata/arc/histograms.xml
index 7345d43..0e94fe03 100644
--- a/tools/metrics/histograms/metadata/arc/histograms.xml
+++ b/tools/metrics/histograms/metadata/arc/histograms.xml
@@ -83,7 +83,7 @@
 </histogram>
 
 <histogram name="Arc.AccessibilityWithTalkBack" enum="BooleanEnabled"
-    expires_after="2022-04-10">
+    expires_after="2023-04-10">
   <owner>hirokisato@chromium.org</owner>
   <owner>arc-framework@google.com</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/ash/histograms.xml b/tools/metrics/histograms/metadata/ash/histograms.xml
index 67e7de0..347f029 100644
--- a/tools/metrics/histograms/metadata/ash/histograms.xml
+++ b/tools/metrics/histograms/metadata/ash/histograms.xml
@@ -1303,7 +1303,7 @@
 </histogram>
 
 <histogram name="Ash.Desks.NumberOfWindowsOnDesk_2" units="units"
-    expires_after="2022-04-10">
+    expires_after="2022-10-03">
   <owner>afakhry@chromium.org</owner>
   <owner>tclaiborne@chromium.org</owner>
   <summary>
@@ -1313,7 +1313,7 @@
 </histogram>
 
 <histogram name="Ash.Desks.NumberOfWindowsOnDesk_3" units="units"
-    expires_after="2022-04-10">
+    expires_after="2022-10-03">
   <owner>afakhry@chromium.org</owner>
   <owner>tclaiborne@chromium.org</owner>
   <summary>
@@ -1323,7 +1323,7 @@
 </histogram>
 
 <histogram name="Ash.Desks.NumberOfWindowsOnDesk_4" units="units"
-    expires_after="2022-04-10">
+    expires_after="2022-10-03">
   <owner>afakhry@chromium.org</owner>
   <owner>tclaiborne@chromium.org</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/chromeos/histograms.xml b/tools/metrics/histograms/metadata/chromeos/histograms.xml
index 30012693..9ebb9684 100644
--- a/tools/metrics/histograms/metadata/chromeos/histograms.xml
+++ b/tools/metrics/histograms/metadata/chromeos/histograms.xml
@@ -848,12 +848,12 @@
 </histogram>
 
 <histogram base="true" name="ChromeOS.HardwareVerifier.Report"
-    enum="HardwareVerifierQualificationStatus" expires_after="2022-04-10">
+    enum="HardwareVerifierQualificationStatus" expires_after="2023-03-01">
 <!-- Name completed by histogram_suffixes name="HardwareVerifierSupportCategory" -->
 
   <owner>itspeter@chromium.org</owner>
   <owner>stimim@chromium.org</owner>
-  <owner>chromeos-hw-checker@google.com</owner>
+  <owner>chromeos-runtime-probe@google.com</owner>
   <summary>
     Qualification status of each component types. This entry is generated by
     hardware_verifier.conf at boot time.
@@ -861,28 +861,28 @@
 </histogram>
 
 <histogram name="ChromeOS.HardwareVerifier.Report.IsCompliant" enum="Boolean"
-    expires_after="2022-07-03">
+    expires_after="2023-03-01">
   <owner>itspeter@chromium.org</owner>
   <owner>stimim@chromium.org</owner>
-  <owner>chromeos-hw-checker@google.com</owner>
+  <owner>chromeos-runtime-probe@google.com</owner>
   <summary>Aggregated result of hardware verifier check.</summary>
 </histogram>
 
 <histogram name="ChromeOS.HardwareVerifier.TimeToFinish" units="ms"
-    expires_after="2022-04-17">
+    expires_after="2023-03-01">
   <owner>itspeter@chromium.org</owner>
   <owner>stimim@chromium.org</owner>
-  <owner>chromeos-hw-checker@google.com</owner>
+  <owner>chromeos-runtime-probe@google.com</owner>
   <summary>
     The amount of time it takes to finish one hardware verification run.
   </summary>
 </histogram>
 
 <histogram name="ChromeOS.HardwareVerifier.TimeToProbe" units="ms"
-    expires_after="2022-04-10">
+    expires_after="2023-03-01">
   <owner>itspeter@chromium.org</owner>
   <owner>stimim@chromium.org</owner>
-  <owner>chromeos-hw-checker@google.com</owner>
+  <owner>chromeos-runtime-probe@google.com</owner>
   <summary>The amount of time it takes to probe hardware components.</summary>
 </histogram>
 
@@ -1772,44 +1772,8 @@
   </summary>
 </histogram>
 
-<histogram name="ChromeOS.WebAPK.MinterResponseOrErrorCode"
-    enum="CombinedHttpResponseAndNetErrorCode" expires_after="2022-04-14">
-  <owner>tsergeant@chromium.org</owner>
-  <owner>chromeos-apps-foundation-team@google.com</owner>
-  <summary>
-    HTTP response code or net error code for requests made to the WebAPK minter
-    service. Logged after a request to generate a WebAPK finishes, which happens
-    when a PWA which supports Web Share Target is installed or updated.
-  </summary>
-</histogram>
-
-<histogram name="ChromeOS.WebApk.UninstallSource"
-    enum="WebApkUninstallSourceChromeOS" expires_after="2022-04-01">
-  <owner>tsergeant@chromium.org</owner>
-  <owner>chromeos-apps-foundation-team@google.com</owner>
-  <summary>
-    Records the source of a WebAPK uninstall event. A WebAPK is installed when a
-    PWA which supports Web Share Target is installed in the browser, and is
-    uninstalled when the user uninstalls the PWA or uninstalls the WebAPK
-    directly through ARC Android settings.
-  </summary>
-</histogram>
-
-<histogram name="ChromeOS.WebAPK.UnlinkedWebAPKCount" units="count"
-    expires_after="2022-03-14">
-  <owner>tsergeant@chromium.org</owner>
-  <owner>chromeos-apps-foundation-team@google.com</owner>
-  <summary>
-    Records the number of installed WebAPKs that were not linked to a Web App
-    which were found and removed. The presence of these apps indicates that
-    something went wrong in the WebAPK installation process. Unlinked apps are
-    detected every startup, this histogram is only recorded when at least 1 app
-    is found.
-  </summary>
-</histogram>
-
 <histogram name="ChromeOS.WebAPK.{InstallType}.ArcInstallResult"
-    enum="WebApkArcInstallResultChromeOS" expires_after="2022-04-14">
+    enum="WebApkArcInstallResultChromeOS" expires_after="2022-10-14">
   <owner>tsergeant@chromium.org</owner>
   <owner>chromeos-apps-foundation-team@google.com</owner>
   <summary>
@@ -1826,7 +1790,7 @@
 </histogram>
 
 <histogram name="ChromeOS.WebAPK.{InstallType}.Result"
-    enum="WebApkInstallResultChromeOS" expires_after="2022-04-14">
+    enum="WebApkInstallResultChromeOS" expires_after="2022-10-14">
   <owner>tsergeant@chromium.org</owner>
   <owner>chromeos-apps-foundation-team@google.com</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/file/histograms.xml b/tools/metrics/histograms/metadata/file/histograms.xml
index 9285f580..4fbb522 100644
--- a/tools/metrics/histograms/metadata/file/histograms.xml
+++ b/tools/metrics/histograms/metadata/file/histograms.xml
@@ -47,7 +47,7 @@
 </histogram>
 
 <histogram name="FileBrowser.ChangeDirectory.RootType"
-    enum="FileManagerRootType" expires_after="2022-04-10">
+    enum="FileManagerRootType" expires_after="2023-02-28">
   <owner>simmonsjosh@google.com</owner>
   <owner>src/ui/file_manager/OWNERS</owner>
   <summary>
@@ -455,7 +455,7 @@
 </histogram>
 
 <histogram name="FileBrowser.Notification.Show"
-    enum="FileManagerNotificationType" expires_after="2022-04-10">
+    enum="FileManagerNotificationType" expires_after="2023-02-28">
   <owner>simmonsjosh@google.com</owner>
   <owner>src/ui/file_manager/OWNERS</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/network/histograms.xml b/tools/metrics/histograms/metadata/network/histograms.xml
index 51deb50..611bdbf 100644
--- a/tools/metrics/histograms/metadata/network/histograms.xml
+++ b/tools/metrics/histograms/metadata/network/histograms.xml
@@ -707,7 +707,7 @@
 </histogram>
 
 <histogram name="Network.DnsProxy.DnsOverHttpsMode"
-    enum="DnsProxy.DnsOverHttpsMode" expires_after="2022-04-03">
+    enum="DnsProxy.DnsOverHttpsMode" expires_after="2022-12-31">
   <owner>garrick@chromium.org</owner>
   <owner>cros-network-metrics@google.com</owner>
   <summary>
@@ -717,7 +717,7 @@
 </histogram>
 
 <histogram name="Network.DnsProxy.DnsOverHttpsQuery.HttpErrors"
-    enum="DnsProxy.HttpError" expires_after="2022-04-03">
+    enum="DnsProxy.HttpError" expires_after="2022-12-31">
   <owner>garrick@chromium.org</owner>
   <owner>cros-network-metrics@google.com</owner>
   <summary>
@@ -728,7 +728,7 @@
 </histogram>
 
 <histogram name="Network.DnsProxy.NameserverTypes"
-    enum="DnsProxy.NameserverType" expires_after="2022-04-03">
+    enum="DnsProxy.NameserverType" expires_after="2022-12-31">
   <owner>garrick@chromium.org</owner>
   <owner>cros-network-metrics@google.com</owner>
   <summary>
@@ -739,7 +739,7 @@
 </histogram>
 
 <histogram name="Network.DnsProxy.Query.Failed{Stage}Duration" units="ms"
-    expires_after="2022-04-03">
+    expires_after="2022-12-31">
   <owner>garrick@chromium.org</owner>
   <owner>cros-network-metrics@google.com</owner>
   <summary>
@@ -775,7 +775,7 @@
 </histogram>
 
 <histogram name="Network.DnsProxy.Query.{Stage}Duration" units="ms"
-    expires_after="2022-04-03">
+    expires_after="2022-12-31">
   <owner>garrick@chromium.org</owner>
   <owner>cros-network-metrics@google.com</owner>
   <summary>
@@ -811,7 +811,7 @@
 </histogram>
 
 <histogram name="Network.DnsProxy.{Family}Nameservers" units="units"
-    expires_after="2022-04-03">
+    expires_after="2022-12-31">
   <owner>garrick@chromium.org</owner>
   <owner>cros-network-metrics@google.com</owner>
   <summary>
@@ -826,7 +826,7 @@
 </histogram>
 
 <histogram name="Network.DnsProxy.{ProcessType}.Event"
-    enum="DnsProxy.ProcessEvent" expires_after="2022-04-03">
+    enum="DnsProxy.ProcessEvent" expires_after="2022-12-31">
   <owner>garrick@chromium.org</owner>
   <owner>cros-network-metrics@google.com</owner>
   <summary>
@@ -844,7 +844,7 @@
 </histogram>
 
 <histogram name="Network.DnsProxy.{Type}Query.Errors"
-    enum="DnsProxy.QueryError" expires_after="2022-04-03">
+    enum="DnsProxy.QueryError" expires_after="2022-12-31">
   <owner>garrick@chromium.org</owner>
   <owner>cros-network-metrics@google.com</owner>
   <summary>
@@ -859,7 +859,7 @@
 </histogram>
 
 <histogram name="Network.DnsProxy.{Type}Query.FailedResolveDuration" units="ms"
-    expires_after="2022-04-03">
+    expires_after="2022-12-31">
   <owner>garrick@chromium.org</owner>
   <owner>cros-network-metrics@google.com</owner>
   <summary>
@@ -880,7 +880,7 @@
 </histogram>
 
 <histogram name="Network.DnsProxy.{Type}Query.ResolveDuration" units="ms"
-    expires_after="2022-04-03">
+    expires_after="2022-12-31">
   <owner>garrick@chromium.org</owner>
   <owner>cros-network-metrics@google.com</owner>
   <summary>
@@ -901,7 +901,7 @@
 </histogram>
 
 <histogram name="Network.DnsProxy.{Type}Query.Results"
-    enum="DnsProxy.QueryResult" expires_after="2022-04-03">
+    enum="DnsProxy.QueryResult" expires_after="2022-12-31">
   <owner>garrick@chromium.org</owner>
   <owner>cros-network-metrics@google.com</owner>
   <summary>
diff --git a/ui/display/manager/touch_transform_controller.cc b/ui/display/manager/touch_transform_controller.cc
index 267ebee..a4ac305 100644
--- a/ui/display/manager/touch_transform_controller.cc
+++ b/ui/display/manager/touch_transform_controller.cc
@@ -8,7 +8,6 @@
 #include <vector>
 
 #include "base/logging.h"
-#include "skia/ext/skia_matrix_44.h"
 #include "ui/display/display_layout.h"
 #include "ui/display/manager/display_manager.h"
 #include "ui/display/manager/managed_display_info.h"
@@ -19,6 +18,7 @@
 #include "ui/display/types/display_snapshot.h"
 #include "ui/events/devices/device_data_manager.h"
 #include "ui/events/devices/touch_device_transform.h"
+#include "ui/gfx/geometry/transform.h"
 
 namespace display {
 
@@ -48,14 +48,16 @@
 
   // Vector of the X-coordinate of display points corresponding to each of the
   // touch points.
-  skia::Vector4 display_points_x(
-      touch_point_pairs[0].first.x(), touch_point_pairs[1].first.x(),
-      touch_point_pairs[2].first.x(), touch_point_pairs[3].first.x());
+  SkV4 display_points_x = {static_cast<float>(touch_point_pairs[0].first.x()),
+                           static_cast<float>(touch_point_pairs[1].first.x()),
+                           static_cast<float>(touch_point_pairs[2].first.x()),
+                           static_cast<float>(touch_point_pairs[3].first.x())};
   // Vector of the Y-coordinate of display points corresponding to each of the
   // touch points.
-  skia::Vector4 display_points_y(
-      touch_point_pairs[0].first.y(), touch_point_pairs[1].first.y(),
-      touch_point_pairs[2].first.y(), touch_point_pairs[3].first.y());
+  SkV4 display_points_y = {static_cast<float>(touch_point_pairs[0].first.y()),
+                           static_cast<float>(touch_point_pairs[1].first.y()),
+                           static_cast<float>(touch_point_pairs[2].first.y()),
+                           static_cast<float>(touch_point_pairs[3].first.y())};
 
   // Initialize |touch_point_matrix|
   // If {(xt_1, yt_1), (xt_2, yt_2), (xt_3, yt_3)....} are a set of touch points
@@ -65,48 +67,55 @@
   // |xt_2  yt_2  1  0|
   // |xt_3  yt_3  1  0|
   // |xt_4  yt_4  1  0|
-  skia::Matrix44 touch_point_matrix;
+  gfx::Transform touch_point_matrix;
   for (int row = 0; row < 4; row++) {
-    touch_point_matrix.setRC(row, 0, touch_point_pairs[row].second.x());
-    touch_point_matrix.setRC(row, 1, touch_point_pairs[row].second.y());
-    touch_point_matrix.setRC(row, 2, 1);
-    touch_point_matrix.setRC(row, 3, 0);
+    touch_point_matrix.matrix().setRC(row, 0,
+                                      touch_point_pairs[row].second.x());
+    touch_point_matrix.matrix().setRC(row, 1,
+                                      touch_point_pairs[row].second.y());
+    touch_point_matrix.matrix().setRC(row, 2, 1);
+    touch_point_matrix.matrix().setRC(row, 3, 0);
   }
-  skia::Matrix44 touch_point_matrix_transpose(touch_point_matrix);
-  touch_point_matrix_transpose.transpose();
+  gfx::Transform touch_point_matrix_transpose = touch_point_matrix;
+  touch_point_matrix_transpose.Transpose();
 
-  skia::Matrix44 product_matrix =
+  gfx::Transform product_matrix =
       touch_point_matrix_transpose * touch_point_matrix;
 
   // Set (3, 3) = 1 so that |determinent| of the matrix is != 0 and the inverse
   // can be calculated.
-  product_matrix.setRC(3, 3, 1);
+  product_matrix.matrix().setRC(3, 3, 1);
 
-  skia::Matrix44 product_matrix_inverse;
+  gfx::Transform product_matrix_inverse;
 
   // NOTE: If the determinent is zero then the inverse cannot be computed. The
   // only solution is to restart touch calibration and get new points from user.
-  if (!product_matrix.invert(&product_matrix_inverse)) {
+  if (!product_matrix.GetInverse(&product_matrix_inverse)) {
     NOTREACHED() << "Touch Calibration failed. Determinent is zero.";
     return false;
   }
 
-  product_matrix_inverse.setRC(3, 3, 0);
+  product_matrix_inverse.matrix().setRC(3, 3, 0);
 
   product_matrix = product_matrix_inverse * touch_point_matrix_transpose;
 
-  // Constants [A, B, C, 0] used to calibrate the x-coordinate of touch input.
-  // x_new = x_old * A + y_old * B + C;
-  skia::Vector4 x_constants = product_matrix * display_points_x;
-  // Constants [D, E, F, 0] used to calibrate the y-coordinate of touch input.
-  // y_new = x_old * D + y_old * E + F;
-  skia::Vector4 y_constants = product_matrix * display_points_y;
+  // The result [A, B, C, 0] will be used to calibrate the x-coordinate of
+  // touch input:
+  //   x_new = x_old * A + y_old * B + C;
+  product_matrix.TransformVector4(&display_points_x);
+  // The result [D, E, F, 0] will be used to calibrate the y-coordinate of
+  // touch input:
+  //   y_new = x_old * D + y_old * E + F;
+  product_matrix.TransformVector4(&display_points_y);
 
   // Create a transform matrix using the touch calibration data.
+  // clang-format off
   ctm->ConcatTransform(gfx::Transform(
-      x_constants.fData[0], x_constants.fData[1], 0, x_constants.fData[2],
-      y_constants.fData[0], y_constants.fData[1], 0, y_constants.fData[2], 0, 0,
-      1, 0, 0, 0, 0, 1));
+      display_points_x.x, display_points_x.y, 0, display_points_x.z,
+      display_points_y.x, display_points_y.y, 0, display_points_y.z,
+      0, 0, 1, 0,
+      0, 0, 0, 1));
+  // clang-format on
   return true;
 }
 
diff --git a/ui/file_manager/file_manager/foreground/js/file_manager_commands.js b/ui/file_manager/file_manager/foreground/js/file_manager_commands.js
index 0e2ada0..72c5273 100644
--- a/ui/file_manager/file_manager/foreground/js/file_manager_commands.js
+++ b/ui/file_manager/file_manager/foreground/js/file_manager_commands.js
@@ -2108,13 +2108,20 @@
     const dirEntry = fileManager.getCurrentDirectoryEntry();
     const selection = fileManager.getSelection();
 
-    // Enable this only for a single selected file which is an archive.
-    // TODO(crbug.com/953256) allow more selections and check for ZIP only.
-    if (selection.entries.length === 1 && selection.iconType === 'archive') {
-      event.command.setHidden(false);
-      event.canExecute = dirEntry && !fileManager.directoryModel.isReadOnly() &&
-          selection && selection.totalCount > 0;
+    if (!dirEntry || fileManager.directoryModel.isReadOnly() || !selection ||
+        selection.totalCount === 0) {
+      event.command.setHidden(true);
+      event.canExecute = false;
     } else {
+      // Check the selected entries for a ZIP archive in the selected set.
+      for (const entry of selection.entries) {
+        if (FileType.getExtension(entry) === '.zip') {
+          event.command.setHidden(false);
+          event.canExecute = true;
+          return;
+        }
+      }
+      // Didn't find any ZIP files, disable extract-all.
       event.command.setHidden(true);
       event.canExecute = false;
     }
diff --git a/ui/file_manager/file_manager/foreground/js/file_manager_commands_unittest.m.js b/ui/file_manager/file_manager/foreground/js/file_manager_commands_unittest.m.js
index b60179c..e1773c22 100644
--- a/ui/file_manager/file_manager/foreground/js/file_manager_commands_unittest.m.js
+++ b/ui/file_manager/file_manager/foreground/js/file_manager_commands_unittest.m.js
@@ -289,6 +289,7 @@
   const folderEntry = MockDirectoryEntry.create(downloadsFileSystem, '/folder');
   const textFileEntry = new MockEntry(downloadsFileSystem, '/file.txt');
   const zipFileEntry = new MockEntry(downloadsFileSystem, '/archive.zip');
+  const imageFileEntry = new MockEntry(downloadsFileSystem, '/image.jpg');
 
   // Mock `Event`.
   const event = {
@@ -341,13 +342,20 @@
   assertFalse(event.canExecute);
   assertTrue(event.command.hidden);
 
-  // Check: canExecute is false and command hidden for multiple selection.
-  currentSelection.entries = [zipFileEntry, textFileEntry];
+  // Check: canExecute is false and command hidden for no ZIP multi-selection.
+  currentSelection.entries = [imageFileEntry, textFileEntry];
   currentSelection.totalCount = 2;
   command.canExecute(event, fileManager);
   assertFalse(event.canExecute);
   assertTrue(event.command.hidden);
 
+  // Check: canExecute is true and command visible for ZIP multiple selection.
+  currentSelection.entries = [zipFileEntry, textFileEntry];
+  currentSelection.totalCount = 2;
+  command.canExecute(event, fileManager);
+  assertTrue(event.canExecute);
+  assertFalse(event.command.hidden);
+
   // Check: ZIP canExecute is true and command visible for multiple selection.
   zipCommand.canExecute(event, fileManager);
   assertTrue(event.canExecute);
diff --git a/ui/gfx/geometry/transform.cc b/ui/gfx/geometry/transform.cc
index 9e60204d..15fe7f32 100644
--- a/ui/gfx/geometry/transform.cc
+++ b/ui/gfx/geometry/transform.cc
@@ -443,6 +443,11 @@
   TransformVectorInternal(matrix_, vector);
 }
 
+void Transform::TransformVector4(SkV4* vector) const {
+  DCHECK(vector);
+  matrix_.mapScalars(vector->ptr());
+}
+
 bool Transform::TransformPointReverse(Point* point) const {
   DCHECK(point);
 
diff --git a/ui/gfx/geometry/transform.h b/ui/gfx/geometry/transform.h
index b7fbfd2..b8a4c8c5 100644
--- a/ui/gfx/geometry/transform.h
+++ b/ui/gfx/geometry/transform.h
@@ -237,6 +237,9 @@
   // Applies the transformation to the vector.
   void TransformVector(Vector3dF* vector) const;
 
+  // Applies the transformation to the vector.
+  void TransformVector4(SkV4* vector) const;
+
   // Applies the reverse transformation on the point. Returns true if the
   // transformation can be inverted.
   bool TransformPointReverse(Point3F* point) const;
diff --git a/ui/gfx/geometry/transform_unittest.cc b/ui/gfx/geometry/transform_unittest.cc
index 6de703cd..ec1eab9 100644
--- a/ui/gfx/geometry/transform_unittest.cc
+++ b/ui/gfx/geometry/transform_unittest.cc
@@ -2760,6 +2760,20 @@
   EXPECT_TRUE(backface_invisible.IsBackFaceVisible());
 }
 
+TEST(XFormTest, TransformVector4) {
+  Transform transform;
+  transform.matrix().setRC(0, 0, 2.5f);
+  transform.matrix().setRC(1, 1, 3.5f);
+  transform.matrix().setRC(2, 2, 4.5f);
+  transform.matrix().setRC(3, 3, 5.5f);
+  SkV4 v = {11.5f, 22.5f, 33.5f, 44.5f};
+  transform.TransformVector4(&v);
+  EXPECT_EQ(28.75f, v.x);
+  EXPECT_EQ(78.75f, v.y);
+  EXPECT_EQ(150.75f, v.z);
+  EXPECT_EQ(244.75f, v.w);
+}
+
 }  // namespace
 
 }  // namespace gfx
diff --git a/ui/ozone/platform/wayland/fuzzer/wayland_buffer_fuzzer.cc b/ui/ozone/platform/wayland/fuzzer/wayland_buffer_fuzzer.cc
index e30535e..01f472dc 100644
--- a/ui/ozone/platform/wayland/fuzzer/wayland_buffer_fuzzer.cc
+++ b/ui/ozone/platform/wayland/fuzzer/wayland_buffer_fuzzer.cc
@@ -222,7 +222,7 @@
     env.SetTerminateGpuCallback(manager_host);
   }
 
-  manager_host->DestroyBuffer(widget, kBufferId);
+  manager_host->DestroyBuffer(kBufferId);
 
   // Wait until the buffers are destroyed.
   SyncServer(&server, &env.task_environment);
diff --git a/ui/ozone/platform/wayland/gpu/gbm_pixmap_wayland.cc b/ui/ozone/platform/wayland/gpu/gbm_pixmap_wayland.cc
index 4a63438..2ec4e4e 100644
--- a/ui/ozone/platform/wayland/gpu/gbm_pixmap_wayland.cc
+++ b/ui/ozone/platform/wayland/gpu/gbm_pixmap_wayland.cc
@@ -34,7 +34,7 @@
 
 GbmPixmapWayland::~GbmPixmapWayland() {
   if (gbm_bo_ && widget_ != gfx::kNullAcceleratedWidget)
-    buffer_manager_->DestroyBuffer(widget_, buffer_id_);
+    buffer_manager_->DestroyBuffer(buffer_id_);
 }
 
 bool GbmPixmapWayland::InitializeBuffer(
diff --git a/ui/ozone/platform/wayland/gpu/gbm_surfaceless_wayland.cc b/ui/ozone/platform/wayland/gpu/gbm_surfaceless_wayland.cc
index d3cd402..43110b6 100644
--- a/ui/ozone/platform/wayland/gpu/gbm_surfaceless_wayland.cc
+++ b/ui/ozone/platform/wayland/gpu/gbm_surfaceless_wayland.cc
@@ -77,8 +77,7 @@
 
 void GbmSurfacelessWayland::SolidColorBufferHolder::OnSubmission(
     BufferId buffer_id,
-    WaylandBufferManagerGpu* buffer_manager,
-    gfx::AcceleratedWidget widget) {
+    WaylandBufferManagerGpu* buffer_manager) {
   // Solid color buffers do not require on submission as skia doesn't track
   // them. Instead, they are tracked by GbmSurfacelessWayland. In the future,
   // when SharedImageFactory allows to create non-backed shared images, this
@@ -96,7 +95,7 @@
     // ones until the maximum number of available solid color buffer.
     while (available_solid_color_buffers_.size() > kMaxSolidColorBuffers) {
       buffer_manager->DestroyBuffer(
-          widget, available_solid_color_buffers_.begin()->buffer_id);
+          available_solid_color_buffers_.begin()->buffer_id);
       available_solid_color_buffers_.erase(
           available_solid_color_buffers_.begin());
     }
@@ -104,10 +103,9 @@
 }
 
 void GbmSurfacelessWayland::SolidColorBufferHolder::EraseBuffers(
-    WaylandBufferManagerGpu* buffer_manager,
-    gfx::AcceleratedWidget widget) {
+    WaylandBufferManagerGpu* buffer_manager) {
   for (const auto& buffer : available_solid_color_buffers_)
-    buffer_manager->DestroyBuffer(widget, buffer.buffer_id);
+    buffer_manager->DestroyBuffer(buffer.buffer_id);
   available_solid_color_buffers_.clear();
 }
 
@@ -289,7 +287,7 @@
   surface_scale_factor_ = scale_factor;
 
   // Remove all the buffers.
-  solid_color_buffers_holder_->EraseBuffers(buffer_manager_, widget_);
+  solid_color_buffers_holder_->EraseBuffers(buffer_manager_);
 
   return gl::SurfacelessEGL::Resize(size, scale_factor, color_space, has_alpha);
 }
@@ -408,8 +406,7 @@
   submitted_frames_.erase(submitted_frames_.begin());
   for (auto& plane : submitted_frame->planes) {
     // Let the holder mark this buffer as free to reuse.
-    solid_color_buffers_holder_->OnSubmission(plane.first, buffer_manager_,
-                                              widget_);
+    solid_color_buffers_holder_->OnSubmission(plane.first, buffer_manager_);
   }
   submitted_frame->planes.clear();
   submitted_frame->overlays.clear();
diff --git a/ui/ozone/platform/wayland/gpu/gbm_surfaceless_wayland.h b/ui/ozone/platform/wayland/gpu/gbm_surfaceless_wayland.h
index fd3911e..9bf10dc 100644
--- a/ui/ozone/platform/wayland/gpu/gbm_surfaceless_wayland.h
+++ b/ui/ozone/platform/wayland/gpu/gbm_surfaceless_wayland.h
@@ -91,10 +91,8 @@
         WaylandBufferManagerGpu* buffer_manager);
 
     void OnSubmission(BufferId buffer_id,
-                      WaylandBufferManagerGpu* buffer_manager,
-                      gfx::AcceleratedWidget widget);
-    void EraseBuffers(WaylandBufferManagerGpu* buffer_manager,
-                      gfx::AcceleratedWidget widget);
+                      WaylandBufferManagerGpu* buffer_manager);
+    void EraseBuffers(WaylandBufferManagerGpu* buffer_manager);
 
    private:
     // Gpu-size holder for the solid color buffers. These are not backed by
diff --git a/ui/ozone/platform/wayland/gpu/gl_surface_egl_readback_wayland.cc b/ui/ozone/platform/wayland/gpu/gl_surface_egl_readback_wayland.cc
index 8df7730..273df35 100644
--- a/ui/ozone/platform/wayland/gpu/gl_surface_egl_readback_wayland.cc
+++ b/ui/ozone/platform/wayland/gpu/gl_surface_egl_readback_wayland.cc
@@ -163,12 +163,12 @@
 
 void GLSurfaceEglReadbackWayland::DestroyBuffers() {
   for (const auto& pixel_buffer : available_buffers_)
-    buffer_manager_->DestroyBuffer(widget_, pixel_buffer->buffer_id_);
+    buffer_manager_->DestroyBuffer(pixel_buffer->buffer_id_);
   for (const auto& pixel_buffer : in_flight_pixel_buffers_)
-    buffer_manager_->DestroyBuffer(widget_, pixel_buffer->buffer_id_);
+    buffer_manager_->DestroyBuffer(pixel_buffer->buffer_id_);
 
   if (displayed_buffer_)
-    buffer_manager_->DestroyBuffer(widget_, displayed_buffer_->buffer_id_);
+    buffer_manager_->DestroyBuffer(displayed_buffer_->buffer_id_);
 
   available_buffers_.clear();
   in_flight_pixel_buffers_.clear();
diff --git a/ui/ozone/platform/wayland/gpu/wayland_buffer_manager_gpu.cc b/ui/ozone/platform/wayland/gpu/wayland_buffer_manager_gpu.cc
index 84c7091..34912fa8 100644
--- a/ui/ozone/platform/wayland/gpu/wayland_buffer_manager_gpu.cc
+++ b/ui/ozone/platform/wayland/gpu/wayland_buffer_manager_gpu.cc
@@ -295,20 +295,19 @@
   RunOrQueueTask(std::move(task));
 }
 
-void WaylandBufferManagerGpu::DestroyBuffer(gfx::AcceleratedWidget widget,
-                                            uint32_t buffer_id) {
+void WaylandBufferManagerGpu::DestroyBuffer(uint32_t buffer_id) {
   DCHECK(gpu_thread_runner_);
   if (!gpu_thread_runner_->BelongsToCurrentThread()) {
     // Do the mojo call on the GpuMainThread.
     gpu_thread_runner_->PostTask(
         FROM_HERE, base::BindOnce(&WaylandBufferManagerGpu::DestroyBuffer,
-                                  base::Unretained(this), widget, buffer_id));
+                                  base::Unretained(this), buffer_id));
     return;
   }
 
   base::OnceClosure task =
       base::BindOnce(&WaylandBufferManagerGpu::DestroyBufferTask,
-                     base::Unretained(this), widget, buffer_id);
+                     base::Unretained(this), buffer_id);
   RunOrQueueTask(std::move(task));
 }
 
@@ -515,12 +514,11 @@
   remote_host_->CommitOverlays(widget, std::move(overlays));
 }
 
-void WaylandBufferManagerGpu::DestroyBufferTask(gfx::AcceleratedWidget widget,
-                                                uint32_t buffer_id) {
+void WaylandBufferManagerGpu::DestroyBufferTask(uint32_t buffer_id) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(gpu_sequence_checker_);
   DCHECK(remote_host_);
 
-  remote_host_->DestroyBuffer(widget, buffer_id);
+  remote_host_->DestroyBuffer(buffer_id);
 }
 
 }  // namespace ui
diff --git a/ui/ozone/platform/wayland/gpu/wayland_buffer_manager_gpu.h b/ui/ozone/platform/wayland/gpu/wayland_buffer_manager_gpu.h
index 69dd6538..c9115f5 100644
--- a/ui/ozone/platform/wayland/gpu/wayland_buffer_manager_gpu.h
+++ b/ui/ozone/platform/wayland/gpu/wayland_buffer_manager_gpu.h
@@ -133,7 +133,7 @@
       std::vector<ozone::mojom::WaylandOverlayConfigPtr> overlays);
 
   // Asks Wayland to destroy a wl_buffer.
-  void DestroyBuffer(gfx::AcceleratedWidget widget, uint32_t buffer_id);
+  void DestroyBuffer(uint32_t buffer_id);
 
 #if defined(WAYLAND_GBM)
   // Returns a gbm_device based on a DRM render node.
@@ -218,7 +218,7 @@
   void CommitOverlaysTask(
       gfx::AcceleratedWidget widget,
       std::vector<ozone::mojom::WaylandOverlayConfigPtr> overlays);
-  void DestroyBufferTask(gfx::AcceleratedWidget widget, uint32_t buffer_id);
+  void DestroyBufferTask(uint32_t buffer_id);
 
 #if defined(WAYLAND_GBM)
   // Finds drm render node, opens it and stores the handle into
diff --git a/ui/ozone/platform/wayland/gpu/wayland_canvas_surface.cc b/ui/ozone/platform/wayland/gpu/wayland_canvas_surface.cc
index a88a6161..e5610e60 100644
--- a/ui/ozone/platform/wayland/gpu/wayland_canvas_surface.cc
+++ b/ui/ozone/platform/wayland/gpu/wayland_canvas_surface.cc
@@ -45,7 +45,7 @@
   SharedMemoryBuffer(const SharedMemoryBuffer&) = delete;
   SharedMemoryBuffer& operator=(const SharedMemoryBuffer&) = delete;
 
-  ~SharedMemoryBuffer() { buffer_manager_->DestroyBuffer(widget_, buffer_id_); }
+  ~SharedMemoryBuffer() { buffer_manager_->DestroyBuffer(buffer_id_); }
 
   // Returns SkSurface, which the client can use to write to this buffer.
   sk_sp<SkSurface> sk_surface() const { return sk_surface_; }
diff --git a/ui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc b/ui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc
index bdd4376..fbb23a2 100644
--- a/ui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc
+++ b/ui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc
@@ -285,11 +285,7 @@
   window->CommitOverlays(overlays);
 }
 
-void WaylandBufferManagerHost::DestroyBuffer(
-    [[maybe_unused]] gfx::AcceleratedWidget widget,
-    uint32_t buffer_id) {
-  // TODO(fangzhoug): Remove |widget| from the argument list of the mojo
-  // interface.
+void WaylandBufferManagerHost::DestroyBuffer(uint32_t buffer_id) {
   DCHECK(base::CurrentUIThread::IsSet());
 
   TRACE_EVENT1("wayland", "WaylandBufferManagerHost::DestroyBuffer",
diff --git a/ui/ozone/platform/wayland/host/wayland_buffer_manager_host.h b/ui/ozone/platform/wayland/host/wayland_buffer_manager_host.h
index e086267..e15eea2 100644
--- a/ui/ozone/platform/wayland/host/wayland_buffer_manager_host.h
+++ b/ui/ozone/platform/wayland/host/wayland_buffer_manager_host.h
@@ -103,8 +103,7 @@
                               uint32_t buffer_id) override;
 
   // Called by the GPU to destroy the imported wl_buffer with a |buffer_id|.
-  void DestroyBuffer(gfx::AcceleratedWidget widget,
-                     uint32_t buffer_id) override;
+  void DestroyBuffer(uint32_t buffer_id) override;
   // Called by the GPU and asks to configure the surface/subsurfaces and attach
   // wl_buffers to WaylandWindow with the specified |widget|. Calls OnSubmission
   // and OnPresentation on successful swap and pixels presented.
diff --git a/ui/ozone/platform/wayland/host/wayland_window_drag_controller.cc b/ui/ozone/platform/wayland/host/wayland_window_drag_controller.cc
index e993f39..294802d 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_drag_controller.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_drag_controller.cc
@@ -14,6 +14,7 @@
 
 #include "base/callback.h"
 #include "base/check.h"
+#include "base/containers/contains.h"
 #include "base/logging.h"
 #include "base/memory/weak_ptr.h"
 #include "base/notreached.h"
@@ -232,12 +233,10 @@
   // TODO(crbug.com/1102946): Exo does not support custom mime types. In this
   // case, |data_offer_| will hold an empty mime_types list and, at this point,
   // it's safe just to skip the offer checks and requests here.
-  if (data_offer_->mime_types().empty())
+  if (!base::Contains(data_offer_->mime_types(), kMimeTypeChromiumWindow)) {
+    DVLOG(1) << "OnEnter. No valid mime type found.";
     return;
-
-  // Ensure this is a valid "window drag" offer.
-  DCHECK_EQ(data_offer_->mime_types().size(), 1u);
-  DCHECK_EQ(data_offer_->mime_types().front(), kMimeTypeChromiumWindow);
+  }
 
   // Accept the offer and set the dnd action.
   data_offer_->SetDndActions(kDndActionWindowDrag);
diff --git a/ui/ozone/platform/wayland/mojom/wayland_buffer_manager.mojom b/ui/ozone/platform/wayland/mojom/wayland_buffer_manager.mojom
index 3646107..559efeef 100644
--- a/ui/ozone/platform/wayland/mojom/wayland_buffer_manager.mojom
+++ b/ui/ozone/platform/wayland/mojom/wayland_buffer_manager.mojom
@@ -73,14 +73,11 @@
 
   // These two methods are independent from the type of rendering.
   //
-  // Destroys a wl_buffer created by WaylandConnection based on the |buffer_id|
-  // for the WaylandWindow, which has the following |widget|. The |buffer_id|
-  // is the unique id of the buffer objects being destroyed on the browser
-  // process side. If the buffer with |buffer_id| has never been assigned to an
-  // AcceleratedWidget, it can be destroyed by passing a null widget
-  // with a correct buffer id. Providing wrong pair of the |widget| and the
-  // |buffer_id| will result in the termination of the GPU process.
-  DestroyBuffer(gfx.mojom.AcceleratedWidget widget, uint32 buffer_id);
+  // Destroys a wl_buffer created by WaylandConnection based on the |buffer_id|.
+  // The |buffer_id| is the unique id of the buffer objects being destroyed on
+  // the browser process side. Providing wrong |buffer_id| will result in the
+  // termination of the GPU process.
+  DestroyBuffer(uint32 buffer_id);
 
   // Send overlay configurations for a frame to a WaylandWindow with the
   // following |widget|.
diff --git a/ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc b/ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc
index cd9a52c..0a0aa914 100644
--- a/ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc
+++ b/ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc
@@ -199,12 +199,10 @@
     Sync();
   }
 
-  void DestroyBufferAndSetTerminateExpectation(gfx::AcceleratedWidget widget,
-                                               uint32_t buffer_id,
-                                               bool fail) {
+  void DestroyBufferAndSetTerminateExpectation(uint32_t buffer_id, bool fail) {
     SetTerminateCallbackExpectationAndDestroyChannel(&callback_, fail);
 
-    buffer_manager_gpu_->DestroyBuffer(widget, buffer_id);
+    buffer_manager_gpu_->DestroyBuffer(buffer_id);
 
     Sync();
   }
@@ -256,8 +254,7 @@
 
   CreateDmabufBasedBufferAndSetTerminateExpectation(false /*fail*/,
                                                     kDmabufBufferId);
-  DestroyBufferAndSetTerminateExpectation(gfx::kNullAcceleratedWidget,
-                                          kDmabufBufferId, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kDmabufBufferId, false /*fail*/);
 }
 
 TEST_P(WaylandBufferManagerTest, VerifyModifiers) {
@@ -307,8 +304,7 @@
   EXPECT_EQ(params_vector[0]->modifier_lo_, kFormatModiferLinear & UINT32_MAX);
 
   // Clean up.
-  DestroyBufferAndSetTerminateExpectation(gfx::kNullAcceleratedWidget,
-                                          kDmabufBufferId, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kDmabufBufferId, false /*fail*/);
 }
 
 TEST_P(WaylandBufferManagerTest, CreateShmBasedBuffers) {
@@ -316,8 +312,7 @@
 
   CreateShmBasedBufferAndSetTerminateExpecation(false /*fail*/, kShmBufferId);
 
-  DestroyBufferAndSetTerminateExpectation(gfx::kNullAcceleratedWidget,
-                                          kShmBufferId, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kShmBufferId, false /*fail*/);
 }
 
 TEST_P(WaylandBufferManagerTest, ValidateDataFromGpu) {
@@ -388,11 +383,10 @@
   // ... impossible to destroy non-existing buffer.
   {
     // Either it is attached...
-    DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, true /*fail*/);
+    DestroyBufferAndSetTerminateExpectation(kBufferId1, true /*fail*/);
 
     // Or not attached.
-    DestroyBufferAndSetTerminateExpectation(gfx::kNullAcceleratedWidget,
-                                            kBufferId1, true /*fail*/);
+    DestroyBufferAndSetTerminateExpectation(kBufferId1, true /*fail*/);
   }
 
   // Can destroy the buffer without specifying the widget.
@@ -404,8 +398,7 @@
     buffer_manager_gpu_->CommitBuffer(widget, kBufferId1, window_->GetBounds(),
                                       kDefaultScale, window_->GetBounds());
 
-    DestroyBufferAndSetTerminateExpectation(gfx::kNullAcceleratedWidget,
-                                            kBufferId1, false /*fail*/);
+    DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
   }
 
   // Still can destroy the buffer even if it has not been attached to any
@@ -414,7 +407,7 @@
     EXPECT_CALL(*server_.zwp_linux_dmabuf_v1(), CreateParams(_, _, _)).Times(1);
     CreateDmabufBasedBufferAndSetTerminateExpectation(false /*fail*/,
                                                       kBufferId1);
-    DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
+    DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
   }
 
   // ... impossible to destroy buffers twice.
@@ -430,23 +423,20 @@
     CreateDmabufBasedBufferAndSetTerminateExpectation(false /*fail*/,
                                                       kBufferId2);
 
-    DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
+    DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
     // Can't destroy the buffer with non-existing id (the manager cleared the
     // state after the previous failure).
-    DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, true /*fail*/);
+    DestroyBufferAndSetTerminateExpectation(kBufferId1, true /*fail*/);
 
     // Non-attached buffer must have been also destroyed (we can't destroy it
     // twice) if there was a failure.
-    DestroyBufferAndSetTerminateExpectation(gfx::kNullAcceleratedWidget,
-                                            kBufferId2, true /*fail*/);
+    DestroyBufferAndSetTerminateExpectation(kBufferId2, true /*fail*/);
 
     // Create and destroy non-attached buffer twice.
     CreateDmabufBasedBufferAndSetTerminateExpectation(false /*fail*/,
                                                       kBufferId2);
-    DestroyBufferAndSetTerminateExpectation(gfx::kNullAcceleratedWidget,
-                                            kBufferId2, false /*fail*/);
-    DestroyBufferAndSetTerminateExpectation(gfx::kNullAcceleratedWidget,
-                                            kBufferId2, true /*fail*/);
+    DestroyBufferAndSetTerminateExpectation(kBufferId2, false /*fail*/);
+    DestroyBufferAndSetTerminateExpectation(kBufferId2, true /*fail*/);
   }
 }
 
@@ -517,8 +507,7 @@
                                                false /* fail */);
 
   // Destroying the buffer causes all wl_buffer objects to be destroyed.
-  DestroyBufferAndSetTerminateExpectation(window_->GetWidget(), 1u,
-                                          false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(1u, false /*fail*/);
   SetTerminateCallbackExpectationAndDestroyChannel(&callback_, true /*fail*/);
   buffer_manager_gpu_->CommitBuffer(window_->GetWidget(), 1u,
                                     window_->GetBounds(), kDefaultScale,
@@ -681,8 +670,8 @@
 
   Sync();
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId2, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId2, false /*fail*/);
 }
 
 TEST_P(WaylandBufferManagerTest,
@@ -753,7 +742,7 @@
   EXPECT_CALL(mock_surface_gpu,
               OnSubmission(kBufferId2, gfx::SwapResult::SWAP_ACK, _))
       .Times(1);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, /*fail=*/false);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, /*fail=*/false);
   mock_surface->DestroyPrevAttachedBuffer();
   mock_surface->SendFrameCallback();
   Sync();
@@ -793,13 +782,13 @@
               ::testing::Eq(gfx::PresentationFeedback::Flags::kFailure))))
       .Times(1);
   EXPECT_CALL(mock_surface_gpu, OnPresentation(kBufferId3, _)).Times(1);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId2, /*fail=*/false);
+  DestroyBufferAndSetTerminateExpectation(kBufferId2, /*fail=*/false);
   mock_surface->DestroyPrevAttachedBuffer();
   mock_surface->SendFrameCallback();
   mock_wp_presentation->SendPresentationCallback();
   Sync();
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId3, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId3, false /*fail*/);
 }
 
 // This test ensures that a discarded presentation feedback sent prior receiving
@@ -945,9 +934,9 @@
 
   Sync();
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId2, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId3, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId2, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId3, false /*fail*/);
 }
 
 TEST_P(WaylandBufferManagerTest, TestCommitBufferConditions) {
@@ -1022,10 +1011,8 @@
 
   Sync();
 
-  DestroyBufferAndSetTerminateExpectation(widget, kDmabufBufferId,
-                                          false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kDmabufBufferId2,
-                                          false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kDmabufBufferId, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kDmabufBufferId2, false /*fail*/);
 }
 
 // Tests the surface does not have buffers attached until it's configured at
@@ -1086,8 +1073,7 @@
 
     window_->SetPointerFocus(false);
     temp_window.reset();
-    DestroyBufferAndSetTerminateExpectation(widget, kDmabufBufferId,
-                                            false /*fail*/);
+    DestroyBufferAndSetTerminateExpectation(kDmabufBufferId, false /*fail*/);
 
     Sync();
   }
@@ -1204,9 +1190,9 @@
 
   Sync();
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId2, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId3, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId2, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId3, false /*fail*/);
 }
 
 TEST_P(WaylandBufferManagerTest, DestroyBufferForDestroyedWindow) {
@@ -1226,7 +1212,7 @@
   Sync();
 
   temp_window.reset();
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId, false /*fail*/);
 }
 
 TEST_P(WaylandBufferManagerTest, DestroyedWindowNoSubmissionSingleBuffer) {
@@ -1258,7 +1244,7 @@
 
   Sync();
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId, false /*fail*/);
 }
 
 TEST_P(WaylandBufferManagerTest, DestroyedWindowNoSubmissionMultipleBuffers) {
@@ -1334,8 +1320,8 @@
 
   Sync();
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId2, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId2, false /*fail*/);
 }
 
 // Tests that OnSubmission and OnPresentation are properly triggered if a buffer
@@ -1389,7 +1375,7 @@
   testing::Mock::VerifyAndClearExpectations(&mock_surface_gpu);
 
   // Destroying buffer2 should do nothing yet.
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId2, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId2, false /*fail*/);
   Sync();
 
   testing::Mock::VerifyAndClearExpectations(&mock_surface_gpu);
@@ -1399,7 +1385,7 @@
               OnSubmission(kBufferId2, gfx::SwapResult::SWAP_ACK, _))
       .Times(2);
   EXPECT_CALL(mock_surface_gpu, OnPresentation(kBufferId2, _)).Times(2);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
   Sync();
 
   testing::Mock::VerifyAndClearExpectations(&mock_surface_gpu);
@@ -1466,8 +1452,8 @@
 
   testing::Mock::VerifyAndClearExpectations(&mock_surface_gpu);
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId2, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId2, false /*fail*/);
 }
 
 // Tests that OnSubmission and OnPresentation callbacks are properly called
@@ -1548,9 +1534,9 @@
 
   testing::Mock::VerifyAndClearExpectations(&mock_surface_gpu);
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId2, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId3, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId2, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId3, false /*fail*/);
 }
 
 // This test verifies that submitting the buffer more than once results in
@@ -1711,8 +1697,8 @@
 
   testing::Mock::VerifyAndClearExpectations(&mock_surface_gpu);
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId2, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId2, false /*fail*/);
 }
 
 // Tests that submitting a single buffer only receives an OnSubmission. This is
@@ -1744,7 +1730,7 @@
                                     bounds);
   Sync();
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
 }
 
 // Tests that when CommitOverlays(), root_surface can only be committed once all
@@ -1900,9 +1886,9 @@
 
   Sync();
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId2, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId3, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId2, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId3, false /*fail*/);
 }
 
 // Tests that destroying a channel doesn't result in resetting surface state
@@ -1994,7 +1980,7 @@
                                     bounds);
   Sync();
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
 }
 
 // Tests that destroying a channel results in attaching null buffers to the root
@@ -2385,8 +2371,8 @@
     testing::Mock::VerifyAndClearExpectations(&mock_surface_gpu);
   }
 
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId1, false /*fail*/);
-  DestroyBufferAndSetTerminateExpectation(widget, kBufferId2, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId1, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kBufferId2, false /*fail*/);
 }
 
 TEST_P(WaylandBufferManagerTest, ExecutesTasksAfterInitialization) {
@@ -2404,8 +2390,7 @@
   buffer_manager_gpu_->CommitBuffer(window_->GetWidget(), kDmabufBufferId,
                                     window_->GetBounds(), kDefaultScale,
                                     window_->GetBounds());
-  DestroyBufferAndSetTerminateExpectation(gfx::kNullAcceleratedWidget,
-                                          kDmabufBufferId, false /*fail*/);
+  DestroyBufferAndSetTerminateExpectation(kDmabufBufferId, false /*fail*/);
 
   base::RunLoop().RunUntilIdle();
 
@@ -2514,10 +2499,8 @@
     mock_surface_of_subsurface->SendFrameCallback();
     mock_surface->SendFrameCallback();
 
-    DestroyBufferAndSetTerminateExpectation(gfx::kNullAcceleratedWidget,
-                                            kBufferId1, false);
-    DestroyBufferAndSetTerminateExpectation(gfx::kNullAcceleratedWidget,
-                                            kBufferId2, false);
+    DestroyBufferAndSetTerminateExpectation(kBufferId1, false);
+    DestroyBufferAndSetTerminateExpectation(kBufferId2, false);
   }
 };