diff --git a/AUTHORS b/AUTHORS
index e714607..97f5e95c 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -709,6 +709,7 @@
 Marc des Garets <marc.desgarets@googlemail.com>
 Marcin Wiacek <marcin@mwiacek.com>
 Marco Rodrigues <gothicx@gmail.com>
+Marcos Caceres <marcos@marcosc.com>
 Mariam Ali <alimariam@noogler.google.com>
 Mario Pistrich <m.pistrich@gmail.com>
 Mario Sanchez Prada <mario.prada@samsung.com>
diff --git a/DEPS b/DEPS
index fb1d3bb..38186db 100644
--- a/DEPS
+++ b/DEPS
@@ -235,7 +235,7 @@
   # 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': '87a0078b890928828c08790bb2c846724234edcb',
+  'skia_revision': '7e54e3083fba9b0f07536c155e91eab6dcc84f91',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
@@ -243,7 +243,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
-  'angle_revision': 'bcc8970347f1a51b650e4a79f5e4ddec11047e79',
+  'angle_revision': '43be4d9cd8e461e9c26fb28bb8f2f26b65aea75c',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -282,7 +282,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling freetype
   # and whatever else without interference from each other.
-  'freetype_revision': 'e294a95ca85f4d1aa2cd1a6e00e572acd7f03871',
+  'freetype_revision': 'ede96b239b90bf9c9d9a01f06005ae09fb4fa19b',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling freetype
   # and whatever else without interference from each other.
@@ -310,7 +310,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': '0404c7edc03c2ded9787ba7d00f821414ce4285a',
+  'devtools_frontend_revision': '7008714d9a82c52dddf836fabf22dd17469c7328',
   # 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.
@@ -350,7 +350,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': 'a4c8c8d5bffb66900d5bc76b143ce956f4fa6a13',
+  'dawn_revision': 'd174cef8fa6b677efd4d6ba71babebea41e5282b',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -1637,7 +1637,7 @@
     Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + 'e42f534de7afea5942ecae5993d9211e258918aa',
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + '339965f93cdaaee297fe0764f1765de0ae1bd9f3',
+    Var('webrtc_git') + '/src.git' + '@' + '3cef26662aec5f992eb08491f19d1179ce685d5a',
 
   'src/third_party/libgifcodec':
      Var('skia_git') + '/libgifcodec' + '@'+  Var('libgifcodec_revision'),
@@ -1695,7 +1695,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@646a59e1f51fa8237c7104a6c7805d3d7581d021',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@26afe693b62d30de8c9836b9c2e90760b5589ca7',
     'condition': 'checkout_src_internal',
   },
 
diff --git a/android_webview/BUILD.gn b/android_webview/BUILD.gn
index c49937a..9789e3f 100644
--- a/android_webview/BUILD.gn
+++ b/android_webview/BUILD.gn
@@ -39,7 +39,7 @@
   system_webview_apk_or_module_tmpl(target_name) {
     forward_variables_from(invoker, "*")
     deps = upstream_only_webview_deps
-    min_sdk_version = 21
+    min_sdk_version = default_min_sdk_version
     if (android_64bit_target_cpu && defined(include_32_bit_webview) &&
         !include_32_bit_webview) {
       android_manifest = system_webview_64_android_manifest
@@ -108,7 +108,7 @@
     system_webview_bundle("system_webview_bundle") {
       base_module_target = ":system_webview_base_bundle_module"
       bundle_name = "SystemWebView"
-      min_sdk_version = 21
+      min_sdk_version = default_min_sdk_version
       compress_shared_libraries = true
     }
 
@@ -145,7 +145,7 @@
     system_webview_bundle("system_webview_64_bundle") {
       base_module_target = ":system_webview_64_base_bundle_module"
       bundle_name = "SystemWebView64"
-      min_sdk_version = 21
+      min_sdk_version = default_min_sdk_version
       compress_shared_libraries = true
       include_32_bit_webview = false
     }
@@ -172,7 +172,7 @@
         base_module_target = ":system_webview_32_base_bundle_module"
         bundle_name = "SystemWebView32"
         include_64_bit_webview = false
-        min_sdk_version = 21
+        min_sdk_version = default_min_sdk_version
         compress_shared_libraries = true
       }
     }
diff --git a/android_webview/test/BUILD.gn b/android_webview/test/BUILD.gn
index 5849f86..6e20978 100644
--- a/android_webview/test/BUILD.gn
+++ b/android_webview/test/BUILD.gn
@@ -211,7 +211,7 @@
   apk_name = "WebViewInstrumentationTest"
   apk_under_test = ":webview_instrumentation_apk"
   android_manifest = "../javatests/AndroidManifest.xml"
-  min_sdk_version = 21
+  min_sdk_version = default_min_sdk_version
 
   deps = [
     ":webview_instrumentation_test_mock_services_java",
diff --git a/ash/app_list/model/search/search_result.h b/ash/app_list/model/search/search_result.h
index 8104b5b9..6504a99 100644
--- a/ash/app_list/model/search/search_result.h
+++ b/ash/app_list/model/search/search_result.h
@@ -110,6 +110,9 @@
   Category category() const { return metadata_->category; }
   void set_category(Category category) { metadata_->category = category; }
 
+  bool best_match() const { return metadata_->best_match; }
+  void set_best_match(bool best_match) { metadata_->best_match = best_match; }
+
   DisplayType display_type() const { return metadata_->display_type; }
   void set_display_type(DisplayType display_type) {
     metadata_->display_type = display_type;
diff --git a/ash/capture_mode/capture_mode_api.cc b/ash/capture_mode/capture_mode_api.cc
index 8a4f34f..b16ba24 100644
--- a/ash/capture_mode/capture_mode_api.cc
+++ b/ash/capture_mode/capture_mode_api.cc
@@ -12,8 +12,4 @@
   CaptureModeController::Get()->CaptureScreenshotsOfAllDisplays();
 }
 
-bool IsCaptureModeSessionActive() {
-  return CaptureModeController::Get()->IsActive();
-}
-
 }  // namespace ash
diff --git a/ash/capture_mode/capture_mode_session.h b/ash/capture_mode/capture_mode_session.h
index 684c237..0044d0b 100644
--- a/ash/capture_mode/capture_mode_session.h
+++ b/ash/capture_mode/capture_mode_session.h
@@ -172,6 +172,7 @@
   friend class CaptureModeAdvancedSettingsTestApi;
   friend class CaptureModeSessionFocusCycler;
   friend class CaptureModeSessionTestApi;
+  friend class CaptureModeTestApi;
   class CursorSetter;
   class ScopedA11yOverrideWindowSetter;
 
diff --git a/ash/capture_mode/capture_mode_test_api.cc b/ash/capture_mode/capture_mode_test_api.cc
index 0e36e77..cffe594 100644
--- a/ash/capture_mode/capture_mode_test_api.cc
+++ b/ash/capture_mode/capture_mode_test_api.cc
@@ -6,10 +6,12 @@
 
 #include "ash/capture_mode/capture_mode_controller.h"
 #include "ash/capture_mode/capture_mode_metrics.h"
+#include "ash/capture_mode/capture_mode_session.h"
 #include "ash/capture_mode/capture_mode_types.h"
 #include "ash/capture_mode/video_recording_watcher.h"
 #include "base/auto_reset.h"
 #include "base/check.h"
+#include "base/run_loop.h"
 
 namespace ash {
 
@@ -92,6 +94,32 @@
       .get();
 }
 
+void CaptureModeTestApi::SimulateOpeningFolderSelectionDialog() {
+  DCHECK(controller_->IsActive());
+  auto* session = controller_->capture_mode_session();
+  DCHECK(!session->capture_mode_settings_widget_);
+  session->SetSettingsMenuShown(true);
+  DCHECK(session->capture_mode_settings_widget_);
+  session->OpenFolderSelectionDialog();
+
+  // In browser tests, the dialog creation is asynchronous, so we'll need to
+  // wait for it.
+  if (GetFolderSelectionDialogWindow())
+    return;
+
+  base::RunLoop loop;
+  session->folder_selection_dialog_controller_
+      ->on_dialog_window_added_callback_for_test_ = loop.QuitClosure();
+  loop.Run();
+}
+
+aura::Window* CaptureModeTestApi::GetFolderSelectionDialogWindow() {
+  DCHECK(controller_->IsActive());
+  auto* session = controller_->capture_mode_session();
+  auto* dialog_controller = session->folder_selection_dialog_controller_.get();
+  return dialog_controller ? dialog_controller->dialog_window() : nullptr;
+}
+
 void CaptureModeTestApi::SetType(bool for_video) {
   controller_->SetType(for_video ? CaptureModeType::kVideo
                                  : CaptureModeType::kImage);
diff --git a/ash/capture_mode/folder_selection_dialog_controller.cc b/ash/capture_mode/folder_selection_dialog_controller.cc
index 2a02716..8c34520 100644
--- a/ash/capture_mode/folder_selection_dialog_controller.cc
+++ b/ash/capture_mode/folder_selection_dialog_controller.cc
@@ -59,13 +59,14 @@
   DCHECK(root);
   DCHECK(root->IsRootWindow());
 
-  window_observation_.Observe(wm::TransientWindowManager::GetOrCreate(
-      dialog_background_dimmer_.window()));
+  auto* owner = dialog_background_dimmer_.window();
+  owner->SetId(kShellWindowId_CaptureModeFolderSelectionDialogOwner);
+  window_observation_.Observe(wm::TransientWindowManager::GetOrCreate(owner));
 
   dialog_background_dimmer_.SetDimColor(
       AshColorProvider::Get()->GetShieldLayerColor(
           AshColorProvider::ShieldLayerType::kShield40));
-  dialog_background_dimmer_.window()->Show();
+  owner->Show();
 
   select_folder_dialog_->SelectFile(
       ui::SelectFileDialog::SELECT_FOLDER,
@@ -74,7 +75,7 @@
       /*file_types=*/nullptr,
       /*file_type_index=*/0,
       /*default_extension=*/base::FilePath::StringType(),
-      /*owning_window=*/dialog_background_dimmer_.window(),
+      /*owning_window=*/owner,
       /*params=*/nullptr);
 }
 
@@ -104,7 +105,6 @@
   DCHECK(!dialog_window_);
 
   dialog_window_ = transient;
-  dialog_window_->SetId(kShellWindowId_CaptureModeFolderSelectionDialog);
 
   // The dialog should never resize, minimize or maximize.
   auto* widget = views::Widget::GetWidgetForNativeWindow(dialog_window_);
@@ -114,6 +114,9 @@
   widget_delegate->SetCanResize(false);
   widget_delegate->SetCanMinimize(false);
   widget_delegate->SetCanMaximize(false);
+
+  if (on_dialog_window_added_callback_for_test_)
+    std::move(on_dialog_window_added_callback_for_test_).Run();
 }
 
 void FolderSelectionDialogController::OnTransientChildRemoved(
diff --git a/ash/capture_mode/folder_selection_dialog_controller.h b/ash/capture_mode/folder_selection_dialog_controller.h
index 5b7bd83..9aef395 100644
--- a/ash/capture_mode/folder_selection_dialog_controller.h
+++ b/ash/capture_mode/folder_selection_dialog_controller.h
@@ -6,6 +6,7 @@
 #define ASH_CAPTURE_MODE_FOLDER_SELECTION_DIALOG_CONTROLLER_H_
 
 #include "ash/wm/window_dimmer.h"
+#include "base/callback_forward.h"
 #include "base/files/file_path.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/scoped_observation.h"
@@ -67,6 +68,8 @@
                                aura::Window* transient) override;
 
  private:
+  friend class CaptureModeTestApi;
+
   Delegate* const delegate_;
 
   // Dims everything behind the dialog (including the capture bar, the settings
@@ -81,6 +84,9 @@
   // |select_folder_dialog_| as a transient child of the dimming window.
   aura::Window* dialog_window_ = nullptr;
 
+  // An optional callback that will be invoked when |dialog_window_| gets added.
+  base::OnceClosure on_dialog_window_added_callback_for_test_;
+
   // We observe the transient window manager of the dimming window to know when
   // the dialog window is added or removed.
   base::ScopedObservation<wm::TransientWindowManager,
diff --git a/ash/public/cpp/app_list/app_list_types.h b/ash/public/cpp/app_list/app_list_types.h
index 8c9afd5..c9996e5 100644
--- a/ash/public/cpp/app_list/app_list_types.h
+++ b/ash/public/cpp/app_list/app_list_types.h
@@ -188,19 +188,21 @@
 // to be displayed in the search box should be associated with one category. It
 // is an error for results displayed in the search box to have a kUnknown
 // category, but results displayed in other views - eg. the Continue section -
-// may use kUnknown. These values are not yet stable, and should not yet be used
-// for metrics.
+// may use kUnknown.
+//
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
 enum class AppListSearchResultCategory {
   kUnknown = 0,
-  kBestMatch = 1,
-  kApp = 2,
+  kApps = 1,
+  kAppShortcuts = 2,
   kWeb = 3,
   kFiles = 4,
-  kAssistant = 5,
-  kSettings = 6,
-  kHelp = 7,
-  kPlayStore = 8,
-  kMaxValue = kPlayStore,
+  kSettings = 5,
+  kHelp = 6,
+  kPlayStore = 7,
+  kSearchAndAssistant = 8,
+  kMaxValue = kSearchAndAssistant,
 };
 
 // Which UI container(s) the result should be displayed in.
@@ -369,6 +371,10 @@
   // section.
   AppListSearchResultCategory category = AppListSearchResultCategory::kUnknown;
 
+  // Whether this result is a top match and should be shown in the Top Matches
+  // section instead of its category.
+  bool best_match = false;
+
   // The type of this result.
   AppListSearchResultType result_type = AppListSearchResultType::kUnknown;
 
diff --git a/ash/public/cpp/capture_mode/capture_mode_api.h b/ash/public/cpp/capture_mode/capture_mode_api.h
index 0f41aef..2d399c8 100644
--- a/ash/public/cpp/capture_mode/capture_mode_api.h
+++ b/ash/public/cpp/capture_mode/capture_mode_api.h
@@ -14,9 +14,6 @@
 // Note: this won't start a capture mode session.
 void ASH_EXPORT CaptureScreenshotsOfAllDisplays();
 
-// Returns true if a capture mode session is currently active.
-bool ASH_EXPORT IsCaptureModeSessionActive();
-
 }  // namespace ash
 
 #endif  // ASH_PUBLIC_CPP_CAPTURE_MODE_CAPTURE_MODE_API_H_
diff --git a/ash/public/cpp/capture_mode/capture_mode_test_api.h b/ash/public/cpp/capture_mode/capture_mode_test_api.h
index 782dd967..8a4f603 100644
--- a/ash/public/cpp/capture_mode/capture_mode_test_api.h
+++ b/ash/public/cpp/capture_mode/capture_mode_test_api.h
@@ -9,6 +9,10 @@
 #include "base/callback_forward.h"
 #include "base/files/file_path.h"
 
+namespace aura {
+class Window;
+}  // namespace aura
+
 namespace gfx {
 class Rect;
 }  // namespace gfx
@@ -72,6 +76,14 @@
   // Can only be called while recording is in progress for a Projector session.
   RecordingOverlayController* GetRecordingOverlayController();
 
+  // Simulates the flow taken by users to open the folder selection dialog from
+  // the settings menu, and waits until this dialog gets added.
+  void SimulateOpeningFolderSelectionDialog();
+
+  // Returns a pointer to the folder selection dialog window or nullptr if no
+  // such window exists.
+  aura::Window* GetFolderSelectionDialogWindow();
+
  private:
   // Sets the capture mode type to a video capture if |for_video| is true, or
   // image capture otherwise.
diff --git a/ash/public/cpp/shell_window_ids.h b/ash/public/cpp/shell_window_ids.h
index 99bbe640..e37a610 100644
--- a/ash/public/cpp/shell_window_ids.h
+++ b/ash/public/cpp/shell_window_ids.h
@@ -202,11 +202,12 @@
   // multiple displays connected.
   kShellWindowId_DisplayIdentificationHighlightWindow,
 
-  // The window hosting the folder selection menu for capture mode, which is not
-  // parented to a top-level container, yet it needs to be uniquely identified
-  // so that we can allow a WindowState to be created for it, since it can be
-  // dragged around.
-  kShellWindowId_CaptureModeFolderSelectionDialog,
+  // The window specified as the owner of the folder selection menu for capture
+  // mode, which will be a transient window parent of the about to be created
+  // dialog window. This is needed in order to prevent
+  // |SelectFileDialogExtension| from favoring to parent the dialog to a browser
+  // window (if one exists).
+  kShellWindowId_CaptureModeFolderSelectionDialogOwner,
 };
 
 // A list of system modal container IDs. The order of the list is important that
diff --git a/ash/system/network/OWNERS b/ash/system/network/OWNERS
index 7632e21..cb2f47e 100644
--- a/ash/system/network/OWNERS
+++ b/ash/system/network/OWNERS
@@ -1,4 +1,5 @@
 azeemarshad@chromium.org
+gordonseto@google.com
 jonmann@chromium.org
 khorimoto@chromium.org
 stevenjb@chromium.org
diff --git a/ash/system/status_area_widget_delegate.cc b/ash/system/status_area_widget_delegate.cc
index 3130cbf..459eaee 100644
--- a/ash/system/status_area_widget_delegate.cc
+++ b/ash/system/status_area_widget_delegate.cc
@@ -178,6 +178,7 @@
 }
 
 void StatusAreaWidgetDelegate::CalculateTargetBounds() {
+  // Prevents creating new layout manager when adding the tray buttons.
   if (is_adding_tray_buttons_)
     return;
   // Use a grid layout so that the trays can be centered in each cell, and
@@ -239,6 +240,10 @@
 }
 
 void StatusAreaWidgetDelegate::ChildPreferredSizeChanged(View* child) {
+  // Prevents resizing and layout row and column change when adding the tray
+  // buttons.
+  if (is_adding_tray_buttons_)
+    return;
   const gfx::Size current_size = size();
   const gfx::Size new_size = GetPreferredSize();
   if (new_size == current_size)
diff --git a/ash/system/time/calendar_month_view.h b/ash/system/time/calendar_month_view.h
index f07cfd0..e21ca1a8 100644
--- a/ash/system/time/calendar_month_view.h
+++ b/ash/system/time/calendar_month_view.h
@@ -73,7 +73,7 @@
       int column_set_id,
       bool is_in_current_month);
 
-  // Owned by `UnifiedCalendarViewController`.
+  // Owned by `CalendarView`.
   CalendarViewController* const calendar_view_controller_;
 
   // If today's cell is in this view.
diff --git a/ash/system/time/calendar_view.cc b/ash/system/time/calendar_view.cc
index a70c1b6b..e2be34c 100644
--- a/ash/system/time/calendar_view.cc
+++ b/ash/system/time/calendar_view.cc
@@ -185,11 +185,10 @@
 };
 
 CalendarView::CalendarView(DetailedViewDelegate* delegate,
-                           UnifiedSystemTrayController* controller,
-                           CalendarViewController* calendar_view_controller)
+                           UnifiedSystemTrayController* controller)
     : TrayDetailedView(delegate),
       controller_(controller),
-      calendar_view_controller_(calendar_view_controller) {
+      calendar_view_controller_(std::make_unique<CalendarViewController>()) {
   CreateTitleRow(IDS_ASH_CALENDAR_TITLE);
 
   // Add the header.
@@ -259,7 +258,8 @@
   SetMonthViews();
 
   scoped_scroll_view_observer_.Observe(scroll_view_);
-  scoped_calendar_view_controller_observer_.Observe(calendar_view_controller_);
+  scoped_calendar_view_controller_observer_.Observe(
+      calendar_view_controller_.get());
   scoped_view_observer_.AddObservation(scroll_view_);
   scoped_view_observer_.AddObservation(content_view_);
 }
@@ -443,8 +443,8 @@
 }
 
 views::View* CalendarView::AddLabelWithId(LabelType type, bool add_at_front) {
-  auto label =
-      std::make_unique<MonthYearHeaderView>(type, calendar_view_controller_);
+  auto label = std::make_unique<MonthYearHeaderView>(
+      type, calendar_view_controller_.get());
   if (add_at_front)
     return content_view_->AddChildViewAt(std::move(label), 0);
   return content_view_->AddChildView(std::move(label));
@@ -452,8 +452,8 @@
 
 CalendarMonthView* CalendarView::AddMonth(base::Time month_first_date,
                                           bool add_at_front) {
-  auto month = std::make_unique<CalendarMonthView>(month_first_date,
-                                                   calendar_view_controller_);
+  auto month = std::make_unique<CalendarMonthView>(
+      month_first_date, calendar_view_controller_.get());
   month->SetBorder(views::CreateEmptyBorder(kMonthVerticalPadding, 0,
                                             kMonthVerticalPadding, 0));
   if (add_at_front) {
diff --git a/ash/system/time/calendar_view.h b/ash/system/time/calendar_view.h
index a4744c6..60bf891 100644
--- a/ash/system/time/calendar_view.h
+++ b/ash/system/time/calendar_view.h
@@ -33,8 +33,7 @@
   METADATA_HEADER(CalendarView);
 
   CalendarView(DetailedViewDelegate* delegate,
-               UnifiedSystemTrayController* controller,
-               CalendarViewController* calendar_view_controller);
+               UnifiedSystemTrayController* controller);
   CalendarView(const CalendarView& other) = delete;
   CalendarView& operator=(const CalendarView& other) = delete;
   ~CalendarView() override;
@@ -58,6 +57,10 @@
   views::Button* CreateInfoButton(views::Button::PressedCallback callback,
                                   int info_accessible_name_id) override;
 
+  CalendarViewController* calendar_view_controller() {
+    return calendar_view_controller_.get();
+  }
+
  private:
   // The header of each month view which shows the month's name. If the year of
   // this month is not the same as the current month, the year is also shown in
@@ -117,8 +120,7 @@
   // Unowned.
   UnifiedSystemTrayController* controller_;
 
-  // Owned by `UnifiedCalendarViewController`.
-  CalendarViewController* const calendar_view_controller_;
+  std::unique_ptr<CalendarViewController> calendar_view_controller_;
 
   // The content of the `scroll_view_`, which carries months and month labels.
   // Owned by `CalendarView`.
diff --git a/ash/system/time/calendar_view_unittest.cc b/ash/system/time/calendar_view_unittest.cc
index 6509a18..5889a83 100644
--- a/ash/system/time/calendar_view_unittest.cc
+++ b/ash/system/time/calendar_view_unittest.cc
@@ -25,7 +25,6 @@
   void SetUp() override {
     AshTestBase::SetUp();
 
-    controller_ = std::make_unique<CalendarViewController>();
     delegate_ =
         std::make_unique<DetailedViewDelegate>(/*tray_controller=*/nullptr);
     tray_model_ = std::make_unique<UnifiedSystemTrayModel>(/*shelf=*/nullptr);
@@ -35,7 +34,6 @@
 
   void TearDown() override {
     calendar_view_.reset();
-    controller_.reset();
     delegate_.reset();
     tray_controller_.reset();
     tray_model_.reset();
@@ -43,11 +41,10 @@
     AshTestBase::TearDown();
   }
 
-  void CreateCalendarView(base::Time date) {
+  void CreateCalendarView() {
     calendar_view_.reset();
-    controller_->UpdateMonth(date);
-    calendar_view_ = std::make_unique<CalendarView>(
-        delegate_.get(), tray_controller_.get(), controller_.get());
+    calendar_view_ =
+        std::make_unique<CalendarView>(delegate_.get(), tray_controller_.get());
     // TODO(https://crbug.com/1236276): remove calling `Layout()` once we can
     // pop up the view from the tray. (https://crbug.com/1254491) And add tests
     // for focusing behaviors.
@@ -121,7 +118,14 @@
 TEST_F(CalendarViewTest, Init) {
   base::Time date;
   ASSERT_TRUE(base::Time::FromString("24 Aug 2021 10:00 GMT", &date));
-  CreateCalendarView(date);
+
+  // Set time override.
+  SetFakeNow(date);
+  base::subtle::ScopedTimeClockOverrides time_override(
+      &CalendarViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
+      /*thread_ticks_override=*/nullptr);
+
+  CreateCalendarView();
 
   EXPECT_EQ(u"July", GetPreviousLabelText());
   EXPECT_EQ(u"August", GetCurrentLabelText());
@@ -138,11 +142,20 @@
   EXPECT_EQ(
       u"29",
       static_cast<views::LabelButton*>(next_month()->children()[0])->GetText());
+}
 
-  // Test corner cases of the `CalendarView`.
+// Test the init view of the `CalendarView` starting with December.
+TEST_F(CalendarViewTest, InitDec) {
   base::Time dec_date;
   ASSERT_TRUE(base::Time::FromString("24 Dec 2021 10:00 GMT", &dec_date));
-  CreateCalendarView(dec_date);
+
+  // Set time override.
+  SetFakeNow(dec_date);
+  base::subtle::ScopedTimeClockOverrides time_override(
+      &CalendarViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
+      /*thread_ticks_override=*/nullptr);
+
+  CreateCalendarView();
 
   EXPECT_EQ(u"November", GetPreviousLabelText());
   EXPECT_EQ(u"December", GetCurrentLabelText());
@@ -165,7 +178,13 @@
   base::Time date;
   ASSERT_TRUE(base::Time::FromString("24 Oct 2021 10:00 GMT", &date));
 
-  CreateCalendarView(date);
+  // Set time override.
+  SetFakeNow(date);
+  base::subtle::ScopedTimeClockOverrides time_override(
+      &CalendarViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
+      /*thread_ticks_override=*/nullptr);
+
+  CreateCalendarView();
 
   EXPECT_EQ(u"September", GetPreviousLabelText());
   EXPECT_EQ(u"October", GetCurrentLabelText());
@@ -212,7 +231,7 @@
       &CalendarViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
       /*thread_ticks_override=*/nullptr);
 
-  CreateCalendarView(date);
+  CreateCalendarView();
 
   EXPECT_EQ(u"September", GetPreviousLabelText());
   EXPECT_EQ(u"October", GetCurrentLabelText());
diff --git a/ash/system/time/unified_calendar_view_controller.cc b/ash/system/time/unified_calendar_view_controller.cc
index ae7360ff..aa464f5e 100644
--- a/ash/system/time/unified_calendar_view_controller.cc
+++ b/ash/system/time/unified_calendar_view_controller.cc
@@ -16,23 +16,24 @@
     UnifiedSystemTrayController* tray_controller)
     : detailed_view_delegate_(
           std::make_unique<DetailedViewDelegate>(tray_controller)),
-      calendar_view_controller_(std::make_unique<CalendarViewController>()),
       tray_controller_(tray_controller) {}
 
-UnifiedCalendarViewController::~UnifiedCalendarViewController() {}
+UnifiedCalendarViewController::~UnifiedCalendarViewController() = default;
 
 views::View* UnifiedCalendarViewController::CreateView() {
   DCHECK(!view_);
-  view_ = new CalendarView(detailed_view_delegate_.get(), tray_controller_,
-                           calendar_view_controller_.get());
+  view_ = new CalendarView(detailed_view_delegate_.get(), tray_controller_);
   return view_;
 }
 
 std::u16string UnifiedCalendarViewController::GetAccessibleName() const {
+  // Shows `Now()` as the initial time if calendar view is not created yet.
+  base::Time current_time =
+      view_ ? view_->calendar_view_controller()->current_date()
+            : base::Time::Now();
   return l10n_util::GetStringFUTF16(
       IDS_ASH_CALENDAR_BUBBLE_ACCESSIBLE_DESCRIPTION,
-      base::TimeFormatWithPattern(calendar_view_controller_->current_date(),
-                                  "MMMM yyyy"));
+      base::TimeFormatWithPattern(current_time, "MMMM yyyy"));
 }
 
 }  // namespace ash
diff --git a/ash/system/time/unified_calendar_view_controller.h b/ash/system/time/unified_calendar_view_controller.h
index d40d8ff..1355ac3 100644
--- a/ash/system/time/unified_calendar_view_controller.h
+++ b/ash/system/time/unified_calendar_view_controller.h
@@ -13,7 +13,6 @@
 class DetailedViewDelegate;
 class UnifiedSystemTrayController;
 class CalendarView;
-class CalendarViewController;
 
 // Controller of `CalendarView` in UnifiedSystemTray.
 class UnifiedCalendarViewController : public DetailedViewController {
@@ -32,7 +31,6 @@
 
  private:
   const std::unique_ptr<DetailedViewDelegate> detailed_view_delegate_;
-  std::unique_ptr<CalendarViewController> const calendar_view_controller_;
 
   // Unowned, the object that instantiated us.
   UnifiedSystemTrayController* const tray_controller_;
diff --git a/ash/webui/diagnostics_ui/backend/network_health_provider.cc b/ash/webui/diagnostics_ui/backend/network_health_provider.cc
index fddecaa..e4ade11 100644
--- a/ash/webui/diagnostics_ui/backend/network_health_provider.cc
+++ b/ash/webui/diagnostics_ui/backend/network_health_provider.cc
@@ -475,7 +475,6 @@
 
 void NetworkHealthProvider::OnDeviceStateListReceived(
     std::vector<network_mojom::DeviceStatePropertiesPtr> devices) {
-  bool list_changed = false;
   base::flat_set<std::string> networks_seen;
 
   // Iterate all devices. If the device is already known, then update it's
@@ -506,7 +505,6 @@
     if (!matched) {
       std::string observer_guid = AddNewNetwork(device);
       networks_seen.insert(std::move(observer_guid));
-      list_changed = true;
     }
   }
 
@@ -515,7 +513,6 @@
     const std::string& observer_guid = it->first;
     if (!base::Contains(networks_seen, observer_guid)) {
       it = networks_.erase(it);
-      list_changed = true;
       continue;
     }
 
diff --git a/ash/webui/shortcut_customization_ui/resources/BUILD.gn b/ash/webui/shortcut_customization_ui/resources/BUILD.gn
index a7108a9..57d6e67 100644
--- a/ash/webui/shortcut_customization_ui/resources/BUILD.gn
+++ b/ash/webui/shortcut_customization_ui/resources/BUILD.gn
@@ -76,8 +76,10 @@
 
 js_library("accelerator_edit_view") {
   deps = [
+    ":accelerator_lookup_manager",
     ":accelerator_view",
     ":icons",
+    ":mojo_interface_provider",
     "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
     "//ui/webui/resources/cr_elements/cr_button:cr_button.m",
   ]
diff --git a/ash/webui/shortcut_customization_ui/resources/accelerator_edit_view.js b/ash/webui/shortcut_customization_ui/resources/accelerator_edit_view.js
index b01c5b4..9f8392ed2 100644
--- a/ash/webui/shortcut_customization_ui/resources/accelerator_edit_view.js
+++ b/ash/webui/shortcut_customization_ui/resources/accelerator_edit_view.js
@@ -11,8 +11,10 @@
 
 import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
+import {AcceleratorLookupManager} from './accelerator_lookup_manager.js';
 import {ViewState} from './accelerator_view.js'
-import {AcceleratorInfo, AcceleratorKeys, AcceleratorSource, AcceleratorState, AcceleratorType} from './shortcut_types.js';
+import {getShortcutProvider} from './mojo_interface_provider.js'
+import {AcceleratorConfigResult, AcceleratorInfo, AcceleratorKeys, AcceleratorSource, AcceleratorState, AcceleratorType, ShortcutProviderInterface} from './shortcut_types.js';
 
 /**
  * @fileoverview
@@ -91,6 +93,17 @@
     }
   }
 
+  /** @override */
+  constructor() {
+    super();
+
+    /** @private {!ShortcutProviderInterface} */
+    this.shortcutProvider_ = getShortcutProvider();
+
+    /** @private {!AcceleratorLookupManager} */
+    this.lookupManager_ = AcceleratorLookupManager.getInstance();
+  }
+
   /** @protected */
   onStatusMessageChanged_() {
     if (this.statusMessage === '') {
@@ -107,7 +120,21 @@
 
   /** @protected */
   onDeleteButtonClicked_() {
-    // TODO(jimmyxgong): Implement this function
+    this.shortcutProvider_
+        .removeAccelerator(
+            this.source, this.action, this.acceleratorInfo.accelerator)
+        .then((result) => {
+          if (result === AcceleratorConfigResult.kSuccess) {
+            this.lookupManager_.removeAccelerator(
+                this.source, this.action, this.acceleratorInfo.accelerator);
+
+            this.dispatchEvent(new CustomEvent('request-update-accelerator', {
+              bubbles: true,
+              composed: true,
+              detail: {source: this.source, action: this.action}
+            }));
+          }
+        });
   }
 
   /** @protected  */
diff --git a/ash/wm/window_state.cc b/ash/wm/window_state.cc
index 5bc2abf..df3cefd 100644
--- a/ash/wm/window_state.cc
+++ b/ash/wm/window_state.cc
@@ -971,12 +971,8 @@
   // WindowState is only for windows in top level container, unless they are
   // temporarily hidden when launched by window restore. The will be reparented
   // to a top level container soon, and need a WindowState.
-  // The window of capture mode folder selection dialog is also special. It's
-  // not parented to a top level container, but it needs a window state since it
-  // can be dragged around.
   if (!IsToplevelContainer(window->parent()) &&
-      !IsTemporarilyHiddenForFullrestore(window) &&
-      window->GetId() != kShellWindowId_CaptureModeFolderSelectionDialog) {
+      !IsTemporarilyHiddenForFullrestore(window)) {
     return nullptr;
   }
 
diff --git a/build/fuchsia/device_target.py b/build/fuchsia/device_target.py
index c9d5ab2f..92643bf7 100644
--- a/build/fuchsia/device_target.py
+++ b/build/fuchsia/device_target.py
@@ -254,3 +254,12 @@
     self.RunCommandPiped('dm reboot')
     time.sleep(_REBOOT_SLEEP_PERIOD)
     self.Start()
+
+  def Stop(self):
+    try:
+      super(DeviceTarget, self).Stop()
+    finally:
+      # End multiplexed ssh connection, ensure that ssh logging stops before
+      # tests/scripts return.
+      if self.IsStarted():
+        self.RunCommand(['-O', 'exit'])
diff --git a/cc/layers/picture_layer_unittest.cc b/cc/layers/picture_layer_unittest.cc
index d1f8dca..473269e 100644
--- a/cc/layers/picture_layer_unittest.cc
+++ b/cc/layers/picture_layer_unittest.cc
@@ -53,6 +53,7 @@
 
   EXPECT_EQ(0, host->SourceFrameNumber());
   host->WillCommit(nullptr);
+  EXPECT_EQ(1, host->SourceFrameNumber());
   host->CommitComplete();
   EXPECT_EQ(1, host->SourceFrameNumber());
 
diff --git a/cc/trees/layer_tree_host.cc b/cc/trees/layer_tree_host.cc
index f7e4f32..57ef165 100644
--- a/cc/trees/layer_tree_host.cc
+++ b/cc/trees/layer_tree_host.cc
@@ -511,6 +511,7 @@
   commit_completion_event_ = std::move(completion);
   swap_promise_manager_.WillCommit();
   client_->WillCommit();
+  source_frame_number_++;
 }
 
 void LayerTreeHost::WaitForCommitCompletion() {
@@ -533,7 +534,6 @@
   // This DCHECK ensures that WaitForCommitCompletion() will not block.
   DCHECK(!in_commit());
   WaitForCommitCompletion();
-  source_frame_number_++;
   client_->DidCommit(impl_commit_start_time_, impl_commit_finish_time_);
   impl_commit_start_time_ = impl_commit_finish_time_ = base::TimeTicks();
   if (did_complete_scale_animation_) {
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index c096695..20e37bb 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -112,7 +112,7 @@
   output = chrome_public_android_manifest
   variables = chrome_public_jinja_variables
   variables += [
-    "min_sdk_version=21",
+    "min_sdk_version=$default_min_sdk_version",
     "target_sdk_version=$android_sdk_version",
   ]
 }
@@ -2806,7 +2806,7 @@
   variables = default_chrome_public_jinja_variables
   variables += [
     "manifest_package=$chrome_public_test_manifest_package",
-    "min_sdk_version=21",
+    "min_sdk_version=$default_min_sdk_version",
     "target_sdk_version=$android_sdk_version",
   ]
 }
@@ -2818,7 +2818,7 @@
   variables = default_chrome_public_jinja_variables
   variables += [
     "manifest_package=$chrome_public_test_manifest_package",
-    "min_sdk_version=21",
+    "min_sdk_version=$default_min_sdk_version",
     "target_sdk_version=$android_sdk_version",
   ]
 }
@@ -2829,7 +2829,7 @@
   output = chrome_public_test_vr_apk_manifest
   variables = chrome_public_jinja_variables
   variables += [
-    "min_sdk_version=21",
+    "min_sdk_version=$default_min_sdk_version",
     "target_sdk_version=$android_sdk_version",
   ]
 }
@@ -3367,7 +3367,7 @@
   }
   is_monochrome_or_trichrome = false
   manifest_package = chrome_public_manifest_package
-  min_sdk_version = 21
+  min_sdk_version = default_min_sdk_version
   module_descs = chrome_modern_module_descs
   version_code = chrome_modern_version_code
 
diff --git a/chrome/android/aapt2.config b/chrome/android/aapt2.config
index 6cfe45e..ea85a3e 100644
--- a/chrome/android/aapt2.config
+++ b/chrome/android/aapt2.config
@@ -1,2 +1,3 @@
+drawable/ic_launcher#no_collapse
 drawable/ic_launcher_round#no_collapse
 drawable/shortcut_incognito#no_collapse
diff --git a/chrome/android/chrome_public_apk_tmpl.gni b/chrome/android/chrome_public_apk_tmpl.gni
index 8f483b1..f25c7aa 100644
--- a/chrome/android/chrome_public_apk_tmpl.gni
+++ b/chrome/android/chrome_public_apk_tmpl.gni
@@ -139,7 +139,7 @@
     } else if (_is_monochrome) {
       min_sdk_version = 24
     } else {
-      min_sdk_version = 21
+      min_sdk_version = default_min_sdk_version
     }
 
     resource_exclusion_regex = common_resource_exclusion_regex
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkFeatures.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkFeatures.java
index 542dde8..bf7c448 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkFeatures.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkFeatures.java
@@ -18,9 +18,30 @@
  * {@code false}</li>
  * <li>{@code bookmark_visuals_enabled}: boolean; refresh the visual looks of the bookmarks
  * manager. Default: {@code false}</li>
+ * <li>{@code bookmarks_refresh_min_version}: boolean; see {@link #VERSION}.</li>
  * </ul>
  */
 public class BookmarkFeatures {
+    /**
+     * An indicator of the code version specific to bookmarks and the reading list. It allows
+     * feature flags to be individually enabled without changing the {@code min_version} for the
+     * whole study.
+     *
+     * <p>Feature flag params using the {@code VERSION} value:
+     * <ul>
+     * <li>{@code bookmarks_improved_save_flow_min_version} - {@link
+     * ChromeFeatureList#BOOKMARKS_IMPROVED_SAVE_FLOW}
+     * <li>{@code bookmarks_refresh_min_version} - {@link ChromeFeatureList#BOOKMARKS_REFRESH}
+     * <li>{@code read_later_min_version} - {@link ChromeFeatureList#READ_LATER}
+     * </ul>
+     *
+     * <p>These parameters allow to control for cases where a significant bug fix or change of param
+     * semantics was merged into release, and prior versions of the code are not to be enabled. In
+     * such case the patch increments the {@code VERSION}, while experiment sets {@code
+     * *_min_version} to set a lower bound on existing implementation with the fix.
+     */
+    static final int VERSION = 0;
+
     private static final boolean BOOKMARK_VISUALS_ENABLED_DEFAULT = false;
     private static final boolean IMPROVED_SAVE_FLOW_AUTODISMISS_ENABLED_DEFAULT = true;
     // This is the same as the default dismiss time for snackbars.
@@ -32,7 +53,11 @@
 
     public static boolean isImprovedSaveFlowEnabled() {
         return FeatureList.isInitialized()
-                && ChromeFeatureList.isEnabled(ChromeFeatureList.BOOKMARKS_IMPROVED_SAVE_FLOW);
+                && ChromeFeatureList.isEnabled(ChromeFeatureList.BOOKMARKS_IMPROVED_SAVE_FLOW)
+                && ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
+                           ChromeFeatureList.BOOKMARKS_IMPROVED_SAVE_FLOW,
+                           "bookmarks_improved_save_flow_min_version", 0)
+                <= VERSION;
     }
 
     public static boolean isImprovedSaveFlowAutodismissEnabled() {
@@ -53,7 +78,10 @@
 
     public static boolean isBookmarksRefreshEnabled() {
         return FeatureList.isInitialized()
-                && ChromeFeatureList.isEnabled(ChromeFeatureList.BOOKMARKS_REFRESH);
+                && ChromeFeatureList.isEnabled(ChromeFeatureList.BOOKMARKS_REFRESH)
+                && ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
+                           ChromeFeatureList.BOOKMARKS_REFRESH, "bookmarks_refresh_min_version", 0)
+                <= VERSION;
     }
 
     public static boolean isBookmarksVisualRefreshEnabled() {
@@ -74,4 +102,4 @@
                 && ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
                         ChromeFeatureList.BOOKMARKS_REFRESH, "edit_bookmark_in_app_menu", false);
     }
-}
\ No newline at end of file
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/ReadingListFeatures.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/ReadingListFeatures.java
index d871f213..c72c0b9 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/ReadingListFeatures.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/ReadingListFeatures.java
@@ -24,16 +24,22 @@
  * discards the "last bookmark location". Default: {@link #DEFAULT_SESSION_LENGTH_SECONDS}</li>
  * <li>{@code use_root_bookmark_as_default}: boolean; use the root folder rather than "Mobile
  * bookmarks" as the default bookmark folder. Default: {@code false}</li>
+ * <li>{@code read_later_min_version}: boolean; see {@link BookmarkFeatures#VERSION}.</li>
  * </ul>
  */
 public class ReadingListFeatures {
+    /** @see BookmarkFeatures#VERSION */
+    static final int VERSION = BookmarkFeatures.VERSION;
     private static final int DEFAULT_SESSION_LENGTH_SECONDS = (int) TimeUnit.HOURS.toSeconds(1);
 
     private ReadingListFeatures() {}
 
     public static boolean isReadingListEnabled() {
         return FeatureList.isInitialized()
-                && ChromeFeatureList.isEnabled(ChromeFeatureList.READ_LATER);
+                && ChromeFeatureList.isEnabled(ChromeFeatureList.READ_LATER)
+                && ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
+                           ChromeFeatureList.READ_LATER, "read_later_min_version", 0)
+                <= VERSION;
     }
 
     /** Returns whether the root folder should be used as the default location. */
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/explore_sites/ExploreSitesCategoryCardView.java b/chrome/android/java/src/org/chromium/chrome/browser/explore_sites/ExploreSitesCategoryCardView.java
index 95915ff..365fe3b 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/explore_sites/ExploreSitesCategoryCardView.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/explore_sites/ExploreSitesCategoryCardView.java
@@ -141,7 +141,7 @@
 
         @Override
         public boolean isItemSupported(@ContextMenuManager.ContextMenuItemId int menuItemId) {
-            return menuItemId != ContextMenuManager.ContextMenuItemId.LEARN_MORE;
+            return true;
         }
 
         @Override
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/firstrun/FirstRunFlowSequencer.java b/chrome/android/java/src/org/chromium/chrome/browser/firstrun/FirstRunFlowSequencer.java
index d6c5b9a..4d8aa3a 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/firstrun/FirstRunFlowSequencer.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/firstrun/FirstRunFlowSequencer.java
@@ -60,22 +60,22 @@
     public static class FirstRunFlowSequencerDelegate {
         /** @return true if the sync consent promo page should be shown. */
         boolean shouldShowSyncConsentPage(Activity activity, List<Account> accounts) {
-            // We show the sync consent page if sync is allowed, and not signed in, and
-            // - "skip the first use hints" is not set, or
-            // - "skip the first use hints" is set, but there is at least one account.
             final IdentityManager identityManager =
                     IdentityServicesProvider.get().getIdentityManager(
                             Profile.getLastUsedRegularProfile());
-            final boolean isSignedInWithSync = identityManager.hasPrimaryAccount(ConsentLevel.SYNC);
-            final boolean shouldShowSyncConsentPreMICe = isSyncAllowed() && !isSignedInWithSync
-                    && (!shouldSkipFirstUseHints(activity) || !accounts.isEmpty());
-
+            if (identityManager.hasPrimaryAccount(ConsentLevel.SYNC) || !isSyncAllowed()) {
+                // No need to show the sync consent page if users already consented to sync or
+                // if sync is not allowed.
+                return false;
+            }
             if (FREMobileIdentityConsistencyFieldTrial.isEnabled()) {
-                final boolean isSignedInWithoutSync =
-                        identityManager.hasPrimaryAccount(ConsentLevel.SIGNIN);
-                return shouldShowSyncConsentPreMICe && isSignedInWithoutSync;
+                // Show the sync consent page only to the signed-in users.
+                return identityManager.hasPrimaryAccount(ConsentLevel.SIGNIN);
             } else {
-                return shouldShowSyncConsentPreMICe;
+                // We show the sync consent page if sync is allowed, and not signed in, and
+                // - "skip the first use hints" is not set, or
+                // - "skip the first use hints" is set, but there is at least one account.
+                return !shouldSkipFirstUseHints(activity) || !accounts.isEmpty();
             }
         }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/native_page/ContextMenuManager.java b/chrome/android/java/src/org/chromium/chrome/browser/native_page/ContextMenuManager.java
index 0c05176..ad1f07d 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/native_page/ContextMenuManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/native_page/ContextMenuManager.java
@@ -36,8 +36,7 @@
     @IntDef({ContextMenuItemId.SEARCH, ContextMenuItemId.OPEN_IN_NEW_TAB,
             ContextMenuItemId.OPEN_IN_NEW_TAB_IN_GROUP, ContextMenuItemId.OPEN_IN_INCOGNITO_TAB,
             ContextMenuItemId.OPEN_IN_NEW_WINDOW, ContextMenuItemId.SAVE_FOR_OFFLINE,
-            ContextMenuItemId.ADD_TO_MY_APPS, ContextMenuItemId.REMOVE,
-            ContextMenuItemId.LEARN_MORE})
+            ContextMenuItemId.ADD_TO_MY_APPS, ContextMenuItemId.REMOVE})
     @Retention(RetentionPolicy.SOURCE)
     public @interface ContextMenuItemId {
         // The order of the items will be based on the value of their ID. So if new items are added,
@@ -51,9 +50,8 @@
         int SAVE_FOR_OFFLINE = 5;
         int ADD_TO_MY_APPS = 6;
         int REMOVE = 7;
-        int LEARN_MORE = 8;
 
-        int NUM_ENTRIES = 9;
+        int NUM_ENTRIES = 8;
     }
 
     private final NativePageNavigationDelegate mNavigationDelegate;
@@ -273,8 +271,6 @@
             }
             case ContextMenuItemId.REMOVE:
                 return true;
-            case ContextMenuItemId.LEARN_MORE:
-                return true;
             case ContextMenuItemId.ADD_TO_MY_APPS:
                 return false;
             default:
@@ -305,8 +301,6 @@
                 return R.string.contextmenu_save_link;
             case ContextMenuItemId.REMOVE:
                 return R.string.remove;
-            case ContextMenuItemId.LEARN_MORE:
-                return R.string.learn_more;
         }
         assert false;
         return 0;
@@ -344,10 +338,6 @@
                 delegate.removeItem();
                 RecordUserAction.record(mUserActionPrefix + ".ContextMenu.RemoveItem");
                 return true;
-            case ContextMenuItemId.LEARN_MORE:
-                mNavigationDelegate.navigateToHelpPage();
-                RecordUserAction.record(mUserActionPrefix + ".ContextMenu.LearnMore");
-                return true;
             default:
                 return false;
         }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsConfig.java b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsConfig.java
index 9c77891..c106812 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsConfig.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsConfig.java
@@ -12,7 +12,6 @@
 import org.chromium.base.ApiCompatibilityUtils;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
-import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
 import org.chromium.components.browser_ui.widget.displaystyle.UiConfig;
 
 import java.lang.annotation.Retention;
@@ -45,16 +44,6 @@
     private SuggestionsConfig() {}
 
     /**
-     * @return Whether scrolling to the bottom of suggestions triggers a load.
-     */
-    public static boolean scrollToLoad() {
-        // The scroll to load feature does not work well for users who require accessibility mode.
-        if (ChromeAccessibilityUtil.get().isAccessibilityEnabled()) return false;
-
-        return ChromeFeatureList.isEnabled(ChromeFeatureList.CONTENT_SUGGESTIONS_SCROLL_TO_LOAD);
-    }
-
-    /**
      * @param resources The resources to fetch the color from.
      * @return The background color for the suggestions sheet content.
      */
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileGroup.java b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileGroup.java
index 035d34f..88901d6 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileGroup.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileGroup.java
@@ -25,7 +25,6 @@
 import org.chromium.chrome.browser.offlinepages.OfflinePageItem;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.suggestions.SiteSuggestion;
-import org.chromium.chrome.browser.suggestions.SuggestionsConfig;
 import org.chromium.chrome.browser.suggestions.SuggestionsMetrics;
 import org.chromium.chrome.browser.suggestions.SuggestionsOfflineModelObserver;
 import org.chromium.chrome.browser.suggestions.SuggestionsUiDelegate;
@@ -598,8 +597,6 @@
                 case ContextMenuItemId.REMOVE:
                     return mSuggestion.sectionType == TileSectionType.PERSONALIZED
                             && mSuggestion.source != TileSource.EXPLORE;
-                case ContextMenuItemId.LEARN_MORE:
-                    return SuggestionsConfig.scrollToLoad();
                 case ContextMenuItemId.OPEN_IN_INCOGNITO_TAB:
                     return mSuggestion.source != TileSource.EXPLORE;
                 default:
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkIntentDataProviderFactory.java b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkIntentDataProviderFactory.java
index 8fdee79..ae77de1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkIntentDataProviderFactory.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkIntentDataProviderFactory.java
@@ -47,6 +47,7 @@
 import org.chromium.components.webapps.ShortcutSource;
 import org.chromium.components.webapps.WebApkDistributor;
 import org.chromium.device.mojom.ScreenOrientationLockType;
+import org.chromium.ui.util.ColorUtils;
 import org.chromium.webapk.lib.common.WebApkCommonUtils;
 import org.chromium.webapk.lib.common.WebApkMetaDataUtils;
 import org.chromium.webapk.lib.common.splash.SplashLayout;
@@ -262,11 +263,10 @@
                 IntentUtils.safeGetString(bundle, WebApkMetaDataKeys.DISPLAY_MODE));
         int orientation = orientationFromString(
                 IntentUtils.safeGetString(bundle, WebApkMetaDataKeys.ORIENTATION));
-        long themeColor = WebApkMetaDataUtils.getLongFromMetaData(bundle,
-                WebApkMetaDataKeys.THEME_COLOR, WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING);
-        long backgroundColor =
-                WebApkMetaDataUtils.getLongFromMetaData(bundle, WebApkMetaDataKeys.BACKGROUND_COLOR,
-                        WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING);
+        long themeColor = WebApkMetaDataUtils.getLongFromMetaData(
+                bundle, WebApkMetaDataKeys.THEME_COLOR, ColorUtils.INVALID_COLOR);
+        long backgroundColor = WebApkMetaDataUtils.getLongFromMetaData(
+                bundle, WebApkMetaDataKeys.BACKGROUND_COLOR, ColorUtils.INVALID_COLOR);
 
         // Fetch the default background color from the WebAPK's resources. Fetching the default
         // background color from the WebAPK is important for consistency when:
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappDataStorage.java b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappDataStorage.java
index 0ad0e93..3ee3a7a 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappDataStorage.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappDataStorage.java
@@ -28,6 +28,7 @@
 import org.chromium.components.webapk.lib.common.WebApkConstants;
 import org.chromium.components.webapps.ShortcutSource;
 import org.chromium.device.mojom.ScreenOrientationLockType;
+import org.chromium.ui.util.ColorUtils;
 
 import java.io.File;
 
@@ -226,10 +227,8 @@
                 mPreferences.getString(KEY_ICON, null), version,
                 mPreferences.getInt(KEY_DISPLAY_MODE, DisplayMode.STANDALONE),
                 mPreferences.getInt(KEY_ORIENTATION, ScreenOrientationLockType.DEFAULT),
-                mPreferences.getLong(
-                        KEY_THEME_COLOR, WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING),
-                mPreferences.getLong(
-                        KEY_BACKGROUND_COLOR, WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING),
+                mPreferences.getLong(KEY_THEME_COLOR, ColorUtils.INVALID_COLOR),
+                mPreferences.getLong(KEY_BACKGROUND_COLOR, ColorUtils.INVALID_COLOR),
                 mPreferences.getBoolean(KEY_IS_ICON_GENERATED, false),
                 mPreferences.getBoolean(KEY_IS_ICON_ADAPTIVE, false));
     }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappIntentDataProviderFactory.java b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappIntentDataProviderFactory.java
index d9ac55d..bfcf55f 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappIntentDataProviderFactory.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappIntentDataProviderFactory.java
@@ -19,6 +19,7 @@
 import org.chromium.chrome.browser.browserservices.intents.WebappIntentUtils;
 import org.chromium.components.webapps.ShortcutSource;
 import org.chromium.device.mojom.ScreenOrientationLockType;
+import org.chromium.ui.util.ColorUtils;
 import org.chromium.webapk.lib.common.splash.SplashLayout;
 
 /**
@@ -66,8 +67,8 @@
             return null;
         }
 
-        long themeColor = IntentUtils.safeGetLongExtra(intent, WebappConstants.EXTRA_THEME_COLOR,
-                WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING);
+        long themeColor = IntentUtils.safeGetLongExtra(
+                intent, WebappConstants.EXTRA_THEME_COLOR, ColorUtils.INVALID_COLOR);
         boolean hasValidToolbarColor = WebappIntentUtils.isLongColorValid(themeColor);
         int toolbarColor = hasValidToolbarColor ? (int) themeColor
                                                 : WebappIntentDataProvider.getDefaultToolbarColor();
@@ -85,9 +86,8 @@
         int orientation = IntentUtils.safeGetIntExtra(
                 intent, WebappConstants.EXTRA_ORIENTATION, ScreenOrientationLockType.DEFAULT);
         int source = sourceFromIntent(intent);
-        Integer backgroundColor = WebappIntentUtils.colorFromLongColor(
-                IntentUtils.safeGetLongExtra(intent, WebappConstants.EXTRA_BACKGROUND_COLOR,
-                        WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING));
+        Integer backgroundColor = WebappIntentUtils.colorFromLongColor(IntentUtils.safeGetLongExtra(
+                intent, WebappConstants.EXTRA_BACKGROUND_COLOR, ColorUtils.INVALID_COLOR));
         boolean isIconGenerated = IntentUtils.safeGetBooleanExtra(
                 intent, WebappConstants.EXTRA_IS_ICON_GENERATED, false);
         boolean isIconAdaptive = IntentUtils.safeGetBooleanExtra(
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkPersonalizedSigninPromoDismissTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkPersonalizedSigninPromoDismissTest.java
index 96fdb7ec..f9a9e40 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkPersonalizedSigninPromoDismissTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkPersonalizedSigninPromoDismissTest.java
@@ -21,6 +21,7 @@
 import androidx.test.filters.MediumTest;
 
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -32,6 +33,7 @@
 import org.chromium.chrome.browser.ChromeTabbedActivity;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
+import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.signin.ui.SigninPromoController;
@@ -82,6 +84,8 @@
                 SigninPromoController.getPromoShowCountPreferenceName(
                         SigninAccessPoint.BOOKMARK_MANAGER));
         SigninPromoController.setPrefSigninPromoDeclinedBookmarksForTests(false);
+        SharedPreferencesManager.getInstance().removeKey(
+                ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT);
     }
 
     @Test
@@ -111,6 +115,9 @@
     @Test
     @MediumTest
     public void testPromoImpressionCountIncrementAfterDisplayingSigninPromo() {
+        Assert.assertEquals(0,
+                SharedPreferencesManager.getInstance().readInt(
+                        ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT));
         assertEquals(0,
                 SharedPreferencesManager.getInstance().readInt(
                         SigninPromoController.getPromoShowCountPreferenceName(
@@ -121,6 +128,9 @@
                 SharedPreferencesManager.getInstance().readInt(
                         SigninPromoController.getPromoShowCountPreferenceName(
                                 SigninAccessPoint.BOOKMARK_MANAGER)));
+        Assert.assertEquals(1,
+                SharedPreferencesManager.getInstance().readInt(
+                        ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT));
     }
 
     private void closeBookmarkManager() {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/RecentTabsPageTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/RecentTabsPageTest.java
index 6cd5b31..b4f4f1b 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/RecentTabsPageTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/RecentTabsPageTest.java
@@ -23,6 +23,8 @@
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.Feature;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
+import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
+import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
@@ -74,6 +76,8 @@
     public void tearDown() {
         leaveRecentTabsPage();
         RecentTabsManager.setRecentlyClosedTabManagerForTests(null);
+        SharedPreferencesManager.getInstance().removeKey(
+                ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT);
     }
 
     @Test
@@ -98,19 +102,31 @@
     @LargeTest
     @Feature("RenderTest")
     public void testPersonalizedSigninPromoInRecentTabsPage() throws Exception {
+        Assert.assertEquals(0,
+                SharedPreferencesManager.getInstance().readInt(
+                        ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT));
         mAccountManagerTestRule.addAccountWithNameAndAvatar(
                 AccountManagerTestRule.TEST_ACCOUNT_EMAIL);
         mPage = loadRecentTabsPage();
         mRenderTestRule.render(mPage.getView(), "personalized_signin_promo_recent_tabs_page");
+        Assert.assertEquals(1,
+                SharedPreferencesManager.getInstance().readInt(
+                        ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT));
     }
 
     @Test
     @LargeTest
     @Feature("RenderTest")
     public void testPersonalizedSyncPromoInRecentTabsPage() throws Exception {
+        Assert.assertEquals(0,
+                SharedPreferencesManager.getInstance().readInt(
+                        ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT));
         mAccountManagerTestRule.addTestAccountThenSignin();
         mPage = loadRecentTabsPage();
         mRenderTestRule.render(mPage.getView(), "personalized_sync_promo_recent_tabs_page");
+        Assert.assertEquals(1,
+                SharedPreferencesManager.getInstance().readInt(
+                        ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT));
     }
 
     /**
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/settings/MainSettingsFragmentTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/settings/MainSettingsFragmentTest.java
index a5ea548..924cf0e 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/settings/MainSettingsFragmentTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/settings/MainSettingsFragmentTest.java
@@ -64,6 +64,7 @@
 import org.chromium.chrome.browser.password_check.PasswordCheck;
 import org.chromium.chrome.browser.password_check.PasswordCheckFactory;
 import org.chromium.chrome.browser.password_manager.settings.PasswordSettings;
+import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
 import org.chromium.chrome.browser.privacy.settings.PrivacySettings;
 import org.chromium.chrome.browser.safety_check.SafetyCheckSettingsFragment;
@@ -157,6 +158,8 @@
         }
         SharedPreferencesManager.getInstance().removeKey(
                 SigninPromoController.getPromoShowCountPreferenceName(SigninAccessPoint.SETTINGS));
+        SharedPreferencesManager.getInstance().removeKey(
+                ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT);
     }
 
     @Test
@@ -401,12 +404,21 @@
     @Test
     @MediumTest
     public void testSyncPromoShownIsNotOverCounted() {
+        int promoShowCount = SharedPreferencesManager.getInstance().readInt(
+                SigninPromoController.getPromoShowCountPreferenceName(SigninAccessPoint.SETTINGS));
+        Assert.assertEquals(0, promoShowCount);
+        Assert.assertEquals(0,
+                SharedPreferencesManager.getInstance().readInt(
+                        ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT));
         launchSettingsActivity();
         onViewWaiting(allOf(withId(R.id.signin_promo_view_container), isDisplayed()));
 
-        int promoShowCount = SharedPreferencesManager.getInstance().readInt(
+        promoShowCount = SharedPreferencesManager.getInstance().readInt(
                 SigninPromoController.getPromoShowCountPreferenceName(SigninAccessPoint.SETTINGS));
-        Assert.assertEquals("Promo shown count should only be increased by 1", 1, promoShowCount);
+        Assert.assertEquals(1, promoShowCount);
+        Assert.assertEquals(1,
+                SharedPreferencesManager.getInstance().readInt(
+                        ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT));
     }
 
     private void launchSettingsActivity() {
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebApkUpdateManagerUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebApkUpdateManagerUnitTest.java
index 291efd9..99694a1 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebApkUpdateManagerUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebApkUpdateManagerUnitTest.java
@@ -59,6 +59,7 @@
 import org.chromium.components.webapk.lib.common.WebApkMetaDataKeys;
 import org.chromium.components.webapps.WebApkDistributor;
 import org.chromium.device.mojom.ScreenOrientationLockType;
+import org.chromium.ui.util.ColorUtils;
 import org.chromium.webapk.lib.common.WebApkConstants;
 import org.chromium.webapk.lib.common.splash.SplashLayout;
 import org.chromium.webapk.test.WebApkTestHelper;
@@ -1136,11 +1137,11 @@
         assertNotEquals(oldDefaultBackgroundColor, splashLayoutDefaultBackgroundColor);
 
         ManifestData androidManifestData = defaultManifestData();
-        androidManifestData.backgroundColor = WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING;
+        androidManifestData.backgroundColor = ColorUtils.INVALID_COLOR;
         androidManifestData.defaultBackgroundColor = oldDefaultBackgroundColor;
 
         ManifestData fetchedManifestData = defaultManifestData();
-        fetchedManifestData.backgroundColor = WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING;
+        fetchedManifestData.backgroundColor = ColorUtils.INVALID_COLOR;
         fetchedManifestData.defaultBackgroundColor = splashLayoutDefaultBackgroundColor;
 
         assertFalse(checkUpdateNeededForFetchedManifest(androidManifestData, fetchedManifestData));
diff --git a/chrome/browser/android/browserservices/intents/BUILD.gn b/chrome/browser/android/browserservices/intents/BUILD.gn
index e5b2416..1aa6553 100644
--- a/chrome/browser/android/browserservices/intents/BUILD.gn
+++ b/chrome/browser/android/browserservices/intents/BUILD.gn
@@ -59,5 +59,6 @@
     "//third_party/blink/public/mojom:mojom_platform_java",
     "//third_party/hamcrest:hamcrest_library_java",
     "//third_party/junit",
+    "//ui/android:ui_no_recycler_view_java",
   ]
 }
diff --git a/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebApkInfoTest.java b/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebApkInfoTest.java
index 565ae424..b393d6d 100644
--- a/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebApkInfoTest.java
+++ b/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebApkInfoTest.java
@@ -34,6 +34,7 @@
 import org.chromium.components.webapps.ShortcutSource;
 import org.chromium.components.webapps.WebApkDistributor;
 import org.chromium.device.mojom.ScreenOrientationLockType;
+import org.chromium.ui.util.ColorUtils;
 import org.chromium.webapk.lib.common.WebApkConstants;
 import org.chromium.webapk.lib.common.splash.SplashLayout;
 import org.chromium.webapk.test.WebApkTestHelper;
@@ -696,8 +697,7 @@
     public void testBackgroundColorFallbackToDefaultNoCustomDefault() {
         Bundle bundle = new Bundle();
         bundle.putString(WebApkMetaDataKeys.START_URL, START_URL);
-        bundle.putString(WebApkMetaDataKeys.BACKGROUND_COLOR,
-                WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING + "L");
+        bundle.putString(WebApkMetaDataKeys.BACKGROUND_COLOR, ColorUtils.INVALID_COLOR + "L");
         WebApkTestHelper.registerWebApkWithMetaData(WEBAPK_PACKAGE_NAME, bundle, null);
 
         Intent intent = new Intent();
@@ -720,8 +720,7 @@
 
         Bundle bundle = new Bundle();
         bundle.putString(WebApkMetaDataKeys.START_URL, START_URL);
-        bundle.putString(WebApkMetaDataKeys.BACKGROUND_COLOR,
-                WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING + "L");
+        bundle.putString(WebApkMetaDataKeys.BACKGROUND_COLOR, ColorUtils.INVALID_COLOR + "L");
         bundle.putInt(
                 WebApkMetaDataKeys.DEFAULT_BACKGROUND_COLOR_ID, defaultBackgroundColorResourceId);
         WebApkTestHelper.registerWebApkWithMetaData(WEBAPK_PACKAGE_NAME, bundle, null);
diff --git a/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappConstants.java b/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappConstants.java
index c40243be..ec262e020 100644
--- a/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappConstants.java
+++ b/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappConstants.java
@@ -5,7 +5,6 @@
 package org.chromium.chrome.browser.browserservices.intents;
 
 import org.chromium.content_public.common.ScreenOrientationConstants;
-import org.chromium.ui.util.ColorUtils;
 
 /**
  * This class contains constants related to adding shortcuts to the Android Home
@@ -46,8 +45,5 @@
     // be correctly populated into the WebappRegistry/WebappDataStorage.
     public static final int WEBAPP_SHORTCUT_VERSION = 3;
 
-    // This value is equal to kInvalidOrMissingColor in the C++ blink::Manifest struct.
-    public static final long MANIFEST_COLOR_INVALID_OR_MISSING = ColorUtils.INVALID_COLOR;
-
     private WebappConstants() {}
 }
diff --git a/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappInfo.java b/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappInfo.java
index 31e837c..be926a80 100644
--- a/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappInfo.java
+++ b/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappInfo.java
@@ -15,6 +15,7 @@
 import org.chromium.components.webapps.ShortcutSource;
 import org.chromium.components.webapps.WebApkDistributor;
 import org.chromium.device.mojom.ScreenOrientationLockType;
+import org.chromium.ui.util.ColorUtils;
 
 import java.util.List;
 import java.util.Map;
@@ -95,11 +96,11 @@
 
     /**
      * Returns the toolbar color if it is valid, and
-     * WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING otherwise.
+     * ColorUtils.INVALID_COLOR otherwise.
      */
     public long toolbarColor() {
         return hasValidToolbarColor() ? mProvider.getColorProvider().getToolbarColor()
-                                      : WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING;
+                                      : ColorUtils.INVALID_COLOR;
     }
 
     /**
@@ -112,7 +113,7 @@
     /**
      * Background color is actually a 32 bit unsigned integer which encodes a color
      * in ARGB format. Return value is a long because we also need to encode the
-     * error state of WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING.
+     * error state of ColorUtils.INVALID_COLOR.
      */
     public long backgroundColor() {
         return WebappIntentUtils.colorFromIntegerColor(getWebappExtras().backgroundColor);
diff --git a/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappInfoTest.java b/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappInfoTest.java
index 171231e2..b98c5b2 100644
--- a/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappInfoTest.java
+++ b/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappInfoTest.java
@@ -19,6 +19,7 @@
 import org.chromium.chrome.test.util.browser.webapps.WebappTestHelper;
 import org.chromium.components.webapps.ShortcutSource;
 import org.chromium.device.mojom.ScreenOrientationLockType;
+import org.chromium.ui.util.ColorUtils;
 
 /**
  * Tests the WebappInfo class's ability to parse various URLs.
@@ -166,9 +167,8 @@
         intent.putExtra(WebappConstants.EXTRA_NAME, name);
         intent.putExtra(WebappConstants.EXTRA_SHORT_NAME, shortName);
         WebappInfo info = createWebappInfo(intent);
-        Assert.assertEquals(WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING, info.toolbarColor());
-        Assert.assertEquals(
-                WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING, info.backgroundColor());
+        Assert.assertEquals(ColorUtils.INVALID_COLOR, info.toolbarColor());
+        Assert.assertEquals(ColorUtils.INVALID_COLOR, info.backgroundColor());
     }
 
     @Test
diff --git a/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappIntentUtils.java b/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappIntentUtils.java
index 82b893a..783cf26 100644
--- a/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappIntentUtils.java
+++ b/chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappIntentUtils.java
@@ -9,6 +9,7 @@
 import android.provider.Browser;
 
 import org.chromium.base.IntentUtils;
+import org.chromium.ui.util.ColorUtils;
 import org.chromium.webapk.lib.common.WebApkConstants;
 
 import java.util.HashSet;
@@ -47,22 +48,22 @@
     /**
      * Converts color from signed Integer where an unspecified color is represented as null to
      * to unsigned long where an unspecified color is represented as
-     * {@link WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING}.
+     * {@link ColorUtils.INVALID_COLOR}.
      */
     public static long colorFromIntegerColor(Integer color) {
         if (color != null) {
             return color.intValue();
         }
-        return WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING;
+        return ColorUtils.INVALID_COLOR;
     }
 
     public static boolean isLongColorValid(long longColor) {
-        return (longColor != WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING);
+        return (longColor != ColorUtils.INVALID_COLOR);
     }
 
     /**
      * Converts color from unsigned long where an unspecified color is represented as
-     * {@link WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING} to a signed Integer where an
+     * {@link ColorUtils.INVALID_COLOR} to a signed Integer where an
      * unspecified color is represented as null.
      */
     public static Integer colorFromLongColor(long longColor) {
diff --git a/chrome/browser/android/shortcut_helper.cc b/chrome/browser/android/shortcut_helper.cc
index fe1d7a1b..afb14278 100644
--- a/chrome/browser/android/shortcut_helper.cc
+++ b/chrome/browser/android/shortcut_helper.cc
@@ -18,7 +18,7 @@
 #include "components/webapps/browser/android/shortcut_info.h"
 #include "content/public/browser/manifest_icon_downloader.h"
 #include "content/public/browser/web_contents.h"
-#include "ui/android/color_helpers.h"
+#include "ui/android/color_utils_android.h"
 #include "ui/gfx/android/java_bitmap.h"
 #include "url/gurl.h"
 
diff --git a/chrome/browser/android/webapk/webapk_handler_delegate.cc b/chrome/browser/android/webapk/webapk_handler_delegate.cc
index 26beb17..eefbce9 100644
--- a/chrome/browser/android/webapk/webapk_handler_delegate.cc
+++ b/chrome/browser/android/webapk/webapk_handler_delegate.cc
@@ -10,7 +10,7 @@
 #include "chrome/android/chrome_jni_headers/WebApkHandlerDelegate_jni.h"
 #include "third_party/blink/public/mojom/manifest/display_mode.mojom.h"
 #include "third_party/skia/include/core/SkColor.h"
-#include "ui/android/color_helpers.h"
+#include "ui/android/color_utils_android.h"
 
 using base::android::JavaParamRef;
 
diff --git a/chrome/browser/android/webapk/webapk_installer.cc b/chrome/browser/android/webapk/webapk_installer.cc
index 96372e36..bca7697 100644
--- a/chrome/browser/android/webapk/webapk_installer.cc
+++ b/chrome/browser/android/webapk/webapk_installer.cc
@@ -53,7 +53,7 @@
 #include "services/network/public/cpp/simple_url_loader.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
-#include "ui/android/color_helpers.h"
+#include "ui/android/color_utils_android.h"
 #include "ui/gfx/android/java_bitmap.h"
 #include "ui/gfx/codec/png_codec.h"
 #include "url/origin.h"
diff --git a/chrome/browser/android/webapk/webapk_update_data_fetcher.cc b/chrome/browser/android/webapk/webapk_update_data_fetcher.cc
index d4dcd7e..5135c17 100644
--- a/chrome/browser/android/webapk/webapk_update_data_fetcher.cc
+++ b/chrome/browser/android/webapk/webapk_update_data_fetcher.cc
@@ -26,7 +26,7 @@
 #include "third_party/blink/public/common/manifest/manifest_util.h"
 #include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
 #include "third_party/smhasher/src/MurmurHash2.h"
-#include "ui/android/color_helpers.h"
+#include "ui/android/color_utils_android.h"
 #include "ui/gfx/android/java_bitmap.h"
 #include "ui/gfx/codec/png_codec.h"
 #include "url/gurl.h"
diff --git a/chrome/browser/android/webapk/webapk_update_manager.cc b/chrome/browser/android/webapk/webapk_update_manager.cc
index ede51fc..cef42d0 100644
--- a/chrome/browser/android/webapk/webapk_update_manager.cc
+++ b/chrome/browser/android/webapk/webapk_update_manager.cc
@@ -26,7 +26,7 @@
 #include "content/public/browser/browser_thread.h"
 #include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
 #include "third_party/skia/include/core/SkBitmap.h"
-#include "ui/android/color_helpers.h"
+#include "ui/android/color_utils_android.h"
 #include "ui/gfx/android/java_bitmap.h"
 #include "url/gurl.h"
 
diff --git a/chrome/browser/apps/app_service/DEPS b/chrome/browser/apps/app_service/DEPS
index e7cb331..9de6438 100644
--- a/chrome/browser/apps/app_service/DEPS
+++ b/chrome/browser/apps/app_service/DEPS
@@ -1,5 +1,5 @@
 include_rules = [
-  "+components/services/app_service/app_service_impl.h",
+  "+components/services/app_service/app_service_mojom_impl.h",
   "+components/services/app_service/public",
   "+components/app_restore",
   "+components/webapk",
diff --git a/chrome/browser/apps/app_service/app_service_proxy_base.cc b/chrome/browser/apps/app_service/app_service_proxy_base.cc
index 09c6a4a..38549c0 100644
--- a/chrome/browser/apps/app_service/app_service_proxy_base.cc
+++ b/chrome/browser/apps/app_service/app_service_proxy_base.cc
@@ -21,7 +21,7 @@
 #include "chrome/browser/ash/file_manager/app_id.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/web_applications/web_app_id_constants.h"
-#include "components/services/app_service/app_service_impl.h"
+#include "components/services/app_service/app_service_mojom_impl.h"
 #include "components/services/app_service/public/cpp/intent_constants.h"
 #include "components/services/app_service/public/cpp/intent_filter_util.h"
 #include "components/services/app_service/public/cpp/intent_util.h"
@@ -130,9 +130,10 @@
 
   browser_app_launcher_ = std::make_unique<apps::BrowserAppLauncher>(profile_);
 
-  app_service_impl_ =
-      std::make_unique<apps::AppServiceImpl>(profile_->GetPath());
-  app_service_impl_->BindReceiver(app_service_.BindNewPipeAndPassReceiver());
+  app_service_mojom_impl_ =
+      std::make_unique<apps::AppServiceMojomImpl>(profile_->GetPath());
+  app_service_mojom_impl_->BindReceiver(
+      app_service_.BindNewPipeAndPassReceiver());
 
   if (app_service_.is_connected()) {
     // The AppServiceProxy is a subscriber: something that wants to be able to
diff --git a/chrome/browser/apps/app_service/app_service_proxy_base.h b/chrome/browser/apps/app_service/app_service_proxy_base.h
index 6d19bac..74b4afa 100644
--- a/chrome/browser/apps/app_service/app_service_proxy_base.h
+++ b/chrome/browser/apps/app_service/app_service_proxy_base.h
@@ -31,7 +31,7 @@
 
 namespace apps {
 
-class AppServiceImpl;
+class AppServiceMojomImpl;
 
 struct IntentLaunchInfo {
   IntentLaunchInfo();
@@ -337,7 +337,7 @@
 
   // This proxy privately owns its instance of the App Service. This should not
   // be exposed except through the Mojo interface connected to |app_service_|.
-  std::unique_ptr<apps::AppServiceImpl> app_service_impl_;
+  std::unique_ptr<apps::AppServiceMojomImpl> app_service_mojom_impl_;
 
   mojo::Remote<apps::mojom::AppService> app_service_;
   apps::AppRegistryCache app_registry_cache_;
diff --git a/chrome/browser/apps/app_service/app_service_proxy_chromeos.cc b/chrome/browser/apps/app_service/app_service_proxy_chromeos.cc
index 7a301ac..e34c165 100644
--- a/chrome/browser/apps/app_service/app_service_proxy_chromeos.cc
+++ b/chrome/browser/apps/app_service/app_service_proxy_chromeos.cc
@@ -31,7 +31,7 @@
 #include "components/app_restore/features.h"
 #include "components/app_restore/full_restore_save_handler.h"
 #include "components/app_restore/full_restore_utils.h"
-#include "components/services/app_service/app_service_impl.h"
+#include "components/services/app_service/app_service_mojom_impl.h"
 #include "components/services/app_service/public/cpp/app_capability_access_cache_wrapper.h"
 #include "components/services/app_service/public/cpp/app_registry_cache_wrapper.h"
 #include "components/services/app_service/public/cpp/types_util.h"
@@ -254,7 +254,7 @@
 }
 
 void AppServiceProxyChromeOs::FlushMojoCallsForTesting() {
-  app_service_impl_->FlushMojoCallsForTesting();
+  app_service_mojom_impl_->FlushMojoCallsForTesting();
   if (built_in_chrome_os_apps_) {
     built_in_chrome_os_apps_->FlushMojoCallsForTesting();
   }
diff --git a/chrome/browser/apps/app_service/app_service_proxy_desktop.cc b/chrome/browser/apps/app_service/app_service_proxy_desktop.cc
index 6a2aaf1..9837b5b 100644
--- a/chrome/browser/apps/app_service/app_service_proxy_desktop.cc
+++ b/chrome/browser/apps/app_service/app_service_proxy_desktop.cc
@@ -7,7 +7,7 @@
 #include "chrome/browser/apps/app_service/publishers/extension_apps.h"
 #include "chrome/browser/web_applications/app_service/web_apps.h"
 #include "chrome/browser/web_applications/web_app_provider.h"
-#include "components/services/app_service/app_service_impl.h"
+#include "components/services/app_service/app_service_mojom_impl.h"
 
 namespace apps {
 
@@ -52,7 +52,7 @@
 }
 
 void AppServiceProxy::FlushMojoCallsForTesting() {
-  app_service_impl_->FlushMojoCallsForTesting();
+  app_service_mojom_impl_->FlushMojoCallsForTesting();
   receivers_.FlushForTesting();
 }
 
diff --git a/chrome/browser/apps/app_service/app_service_proxy_lacros.cc b/chrome/browser/apps/app_service/app_service_proxy_lacros.cc
index 3038a0b..7e2f4f8a 100644
--- a/chrome/browser/apps/app_service/app_service_proxy_lacros.cc
+++ b/chrome/browser/apps/app_service/app_service_proxy_lacros.cc
@@ -26,7 +26,7 @@
 #include "chrome/browser/web_applications/app_service/web_apps_publisher_host.h"
 #include "chrome/browser/web_applications/web_app_provider.h"
 #include "chromeos/lacros/lacros_service.h"
-#include "components/services/app_service/app_service_impl.h"
+#include "components/services/app_service/app_service_mojom_impl.h"
 #include "components/services/app_service/public/cpp/intent_filter_util.h"
 #include "components/services/app_service/public/cpp/intent_util.h"
 #include "components/services/app_service/public/cpp/types_util.h"
diff --git a/chrome/browser/apps/app_service/browser_app_instance_tracker_browsertest.cc b/chrome/browser/apps/app_service/browser_app_instance_tracker_browsertest.cc
index 185a7266..613fa3d 100644
--- a/chrome/browser/apps/app_service/browser_app_instance_tracker_browsertest.cc
+++ b/chrome/browser/apps/app_service/browser_app_instance_tracker_browsertest.cc
@@ -1016,14 +1016,12 @@
 IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, ActivateTabInstance) {
   // Setup: a browser with 2 tabs (app A and a non-app tab).
   Browser* browser = nullptr;
-  aura::Window* window = nullptr;
 
   content::WebContents* web_contents_a;
   content::WebContents* web_contents_c;
 
   // Open app A in a tab.
   browser = CreateBrowser();
-  window = browser->window()->GetNativeWindow();
   web_contents_a = InsertForegroundTab(browser, "https://a.example.org");
 
   // Open a second tab with no app.
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 6e2f857..6fd7dac 100644
--- a/chrome/browser/apps/app_service/webapk/webapk_install_task.cc
+++ b/chrome/browser/apps/app_service/webapk/webapk_install_task.cc
@@ -11,6 +11,7 @@
 #include "base/command_line.h"
 #include "base/location.h"
 #include "base/logging.h"
+#include "base/metrics/histogram_functions.h"
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/task/task_traits.h"
@@ -457,12 +458,19 @@
     std::unique_ptr<std::string> response_body) {
   timer_.Stop();
 
-  int response_code = -1;
-  if (url_loader_->ResponseInfo() && url_loader_->ResponseInfo()->headers)
-    response_code = url_loader_->ResponseInfo()->headers->response_code();
+  int response_or_error_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();
+  }
+  base::UmaHistogramSparse(kWebApkMinterErrorCodeHistogram,
+                           response_or_error_code);
 
-  if (!response_body || response_code != net::HTTP_OK) {
-    LOG(WARNING) << "WebAPK server returned response code " << response_code;
+  if (!response_body || response_or_error_code != net::HTTP_OK) {
+    LOG(WARNING) << "WebAPK server request returned error "
+                 << response_or_error_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 7e62c02..ce0a554 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
@@ -224,6 +224,8 @@
                                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) {
@@ -290,6 +292,8 @@
   ASSERT_EQ(apps::webapk_prefs::GetWebApkAppIds(profile()).size(), 0);
   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_metrics.cc b/chrome/browser/apps/app_service/webapk/webapk_metrics.cc
index 92b9e9e..40bc57e 100644
--- a/chrome/browser/apps/app_service/webapk/webapk_metrics.cc
+++ b/chrome/browser/apps/app_service/webapk/webapk_metrics.cc
@@ -15,6 +15,8 @@
     "ChromeOS.WebAPK.Install.ArcInstallResult";
 const char kWebApkArcUpdateResultHistogram[] =
     "ChromeOS.WebAPK.Update.ArcInstallResult";
+const char kWebApkMinterErrorCodeHistogram[] =
+    "ChromeOS.WebAPK.MinterResponseOrErrorCode";
 
 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 c5e0e64..c511b4d 100644
--- a/chrome/browser/apps/app_service/webapk/webapk_metrics.h
+++ b/chrome/browser/apps/app_service/webapk/webapk_metrics.h
@@ -37,6 +37,7 @@
 extern const char kWebApkUpdateResultHistogram[];
 extern const char kWebApkArcInstallResultHistogram[];
 extern const char kWebApkArcUpdateResultHistogram[];
+extern const char kWebApkMinterErrorCodeHistogram[];
 
 // Records the overall result of installing/updating a WebAPK to UMA.
 void RecordWebApkInstallResult(bool is_update, WebApkInstallStatus result);
diff --git a/chrome/browser/apps/guest_view/web_view_browsertest.cc b/chrome/browser/apps/guest_view/web_view_browsertest.cc
index 1648c25..b47600f 100644
--- a/chrome/browser/apps/guest_view/web_view_browsertest.cc
+++ b/chrome/browser/apps/guest_view/web_view_browsertest.cc
@@ -1840,6 +1840,14 @@
       content::CollectAllRenderFrameHosts(embedder_main_frame),
       testing::UnorderedElementsAre(embedder_main_frame, other_guest_main_frame,
                                     unattached_guest_main_frame));
+
+  // In either case, GetParentOrOuterDocument does not escape GuestViews.
+  EXPECT_EQ(nullptr, other_guest_main_frame->GetParentOrOuterDocument());
+  EXPECT_EQ(nullptr, unattached_guest_main_frame->GetParentOrOuterDocument());
+  EXPECT_EQ(other_guest_main_frame,
+            other_guest_main_frame->GetOutermostMainFrame());
+  EXPECT_EQ(unattached_guest_main_frame,
+            unattached_guest_main_frame->GetOutermostMainFrame());
 }
 
 IN_PROC_BROWSER_TEST_F(WebViewTest, Shim_TestContentLoadEvent) {
diff --git a/chrome/browser/ash/drive/drivefs_native_message_host.cc b/chrome/browser/ash/drive/drivefs_native_message_host.cc
index 5d353db..9afa036 100644
--- a/chrome/browser/ash/drive/drivefs_native_message_host.cc
+++ b/chrome/browser/ash/drive/drivefs_native_message_host.cc
@@ -21,6 +21,7 @@
 #include "extensions/browser/extension_registry.h"
 #include "extensions/common/api/messaging/messaging_endpoint.h"
 #include "extensions/common/api/messaging/port_id.h"
+#include "extensions/common/api/messaging/serialization_format.h"
 #include "extensions/common/permissions/permissions_data.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
@@ -206,7 +207,8 @@
   }
 
   const extensions::PortId port_id(base::UnguessableToken::Create(),
-                                   /* port_number= */ 1, /* is_opener= */ true);
+                                   /* port_number= */ 1, /* is_opener= */ true,
+                                   extensions::SerializationFormat::kJson);
   extensions::MessageService* const message_service =
       extensions::MessageService::Get(profile);
   auto native_message_host = CreateDriveFsInitiatedNativeMessageHost(
diff --git a/chrome/browser/ash/guest_os/vm_sk_forwarding_native_message_host.cc b/chrome/browser/ash/guest_os/vm_sk_forwarding_native_message_host.cc
index 9aad636..f225807 100644
--- a/chrome/browser/ash/guest_os/vm_sk_forwarding_native_message_host.cc
+++ b/chrome/browser/ash/guest_os/vm_sk_forwarding_native_message_host.cc
@@ -24,6 +24,7 @@
 #include "extensions/browser/api/messaging/native_message_host.h"
 #include "extensions/browser/extension_registry.h"
 #include "extensions/common/api/messaging/messaging_endpoint.h"
+#include "extensions/common/api/messaging/serialization_format.h"
 #include "extensions/common/extension.h"
 #include "url/gurl.h"
 
@@ -118,7 +119,8 @@
     const std::string& json_message,
     base::OnceCallback<void(const std::string& response)> response_callback) {
   const extensions::PortId port_id(base::UnguessableToken::Create(),
-                                   1 /* port_number */, true /* is_opener */);
+                                   1 /* port_number */, true /* is_opener */,
+                                   extensions::SerializationFormat::kJson);
 
   extensions::MessageService* const message_service =
       extensions::MessageService::Get(profile);
diff --git a/chrome/browser/ash/input_method/suggestions_service_client.cc b/chrome/browser/ash/input_method/suggestions_service_client.cc
index e88ecd22..08fea08a 100644
--- a/chrome/browser/ash/input_method/suggestions_service_client.cc
+++ b/chrome/browser/ash/input_method/suggestions_service_client.cc
@@ -85,20 +85,18 @@
       ->GetMachineLearningService()
       .LoadTextSuggester(
           text_suggester_.BindNewPipeAndPassReceiver(), std::move(spec),
-          base::BindOnce(
-              [](base::WeakPtr<SuggestionsServiceClient> weak_this,
-                 chromeos::machine_learning::mojom::LoadModelResult result) {
-                if (weak_this) {
-                  weak_this->text_suggester_loaded_ =
-                      result ==
-                      chromeos::machine_learning::mojom::LoadModelResult::OK;
-                }
-              },
-              weak_ptr_factory_.GetWeakPtr()));
+          base::BindOnce(&SuggestionsServiceClient::OnTextSuggesterLoaded,
+                         weak_ptr_factory_.GetWeakPtr()));
 }
 
 SuggestionsServiceClient::~SuggestionsServiceClient() = default;
 
+void SuggestionsServiceClient::OnTextSuggesterLoaded(
+    chromeos::machine_learning::mojom::LoadModelResult result) {
+  text_suggester_loaded_ =
+      result == chromeos::machine_learning::mojom::LoadModelResult::OK;
+}
+
 void SuggestionsServiceClient::RequestSuggestions(
     const std::string& preceding_text,
     const ime::TextSuggestionMode& suggestion_mode,
diff --git a/chrome/browser/ash/input_method/suggestions_service_client.h b/chrome/browser/ash/input_method/suggestions_service_client.h
index 0bf05b6c..43eeb69 100644
--- a/chrome/browser/ash/input_method/suggestions_service_client.h
+++ b/chrome/browser/ash/input_method/suggestions_service_client.h
@@ -12,6 +12,7 @@
 #include "base/time/time.h"
 #include "chrome/browser/ash/input_method/suggestions_source.h"
 #include "chromeos/services/ime/public/cpp/suggestions.h"
+#include "chromeos/services/machine_learning/public/mojom/machine_learning_service.mojom.h"
 #include "chromeos/services/machine_learning/public/mojom/text_suggester.mojom.h"
 #include "mojo/public/cpp/bindings/remote.h"
 
@@ -33,6 +34,11 @@
   bool IsAvailable() override;
 
  private:
+  // Called once the text suggester model has been loaded and is (or is not)
+  // available for use.
+  void OnTextSuggesterLoaded(
+      chromeos::machine_learning::mojom::LoadModelResult result);
+
   // Called when results are returned from the suggestions service
   void OnSuggestionsReturned(
       base::TimeTicks time_request_was_made,
diff --git a/chrome/browser/ash/login/users/avatar/user_image_sync_observer.cc b/chrome/browser/ash/login/users/avatar/user_image_sync_observer.cc
index d78edec..28a8767 100644
--- a/chrome/browser/ash/login/users/avatar/user_image_sync_observer.cc
+++ b/chrome/browser/ash/login/users/avatar/user_image_sync_observer.cc
@@ -89,12 +89,10 @@
 
 void UserImageSyncObserver::OnInitialSync() {
   int synced_index;
-  bool local_image_updated = false;
   if (!GetSyncedImageIndex(&synced_index) || local_image_changed_) {
     UpdateSyncedImageFromLocal();
   } else if (IsIndexSupported(synced_index)) {
     UpdateLocalImageFromSynced();
-    local_image_updated = true;
   }
 }
 
diff --git a/chrome/browser/ash/ownership/owner_settings_service_ash.cc b/chrome/browser/ash/ownership/owner_settings_service_ash.cc
index 91d9de6..9035e6b 100644
--- a/chrome/browser/ash/ownership/owner_settings_service_ash.cc
+++ b/chrome/browser/ash/ownership/owner_settings_service_ash.cc
@@ -688,6 +688,8 @@
     //   kVariationsRestrictParameter
     //   kDeviceDisabled
     //   kDeviceDisabledMessage
+    //   ReportDeviceNetworkTelemetryCollectionRateMs
+    //   ReportDeviceNetworkTelemetryEventCheckingRateMs
 
     LOG(FATAL) << "Device setting " << path << " is read-only.";
   }
diff --git a/chrome/browser/ash/policy/core/device_policy_decoder.cc b/chrome/browser/ash/policy/core/device_policy_decoder.cc
index cc71403..a5266db 100644
--- a/chrome/browser/ash/policy/core/device_policy_decoder.cc
+++ b/chrome/browser/ash/policy/core/device_policy_decoder.cc
@@ -694,6 +694,16 @@
   }
 }
 
+void DecodeIntegerReportingPolicy(PolicyMap* policies,
+                                  const std::string& policy_path,
+                                  google::protobuf::int64 int_value) {
+  std::unique_ptr<base::Value> value = DecodeIntegerValue(int_value);
+  if (value) {
+    policies->Set(policy_path, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_MACHINE,
+                  POLICY_SOURCE_CLOUD, std::move(*value), nullptr);
+  }
+}
+
 void DecodeReportingPolicies(const em::ChromeDeviceSettingsProto& policy,
                              PolicyMap* policies) {
   if (policy.has_device_reporting()) {
@@ -794,13 +804,8 @@
                     base::Value(container.report_board_status()), nullptr);
     }
     if (container.has_device_status_frequency()) {
-      std::unique_ptr<base::Value> value(
-          DecodeIntegerValue(container.device_status_frequency()));
-      if (value) {
-        policies->Set(key::kReportUploadFrequency, POLICY_LEVEL_MANDATORY,
-                      POLICY_SCOPE_MACHINE, POLICY_SOURCE_CLOUD,
-                      std::move(*value), nullptr);
-      }
+      DecodeIntegerReportingPolicy(policies, key::kReportUploadFrequency,
+                                   container.device_status_frequency());
     }
     if (container.has_report_cpu_info()) {
       policies->Set(key::kReportDeviceCpuInfo, POLICY_LEVEL_MANDATORY,
@@ -862,6 +867,16 @@
                     POLICY_SCOPE_MACHINE, POLICY_SOURCE_CLOUD,
                     base::Value(container.report_login_logout()), nullptr);
     }
+    if (container.has_report_network_telemetry_collection_rate_ms()) {
+      DecodeIntegerReportingPolicy(
+          policies, key::kReportDeviceNetworkTelemetryCollectionRateMs,
+          container.report_network_telemetry_collection_rate_ms());
+    }
+    if (container.has_report_network_telemetry_event_checking_rate_ms()) {
+      DecodeIntegerReportingPolicy(
+          policies, key::kReportDeviceNetworkTelemetryEventCheckingRateMs,
+          container.report_network_telemetry_event_checking_rate_ms());
+    }
   }
 
   if (policy.has_device_heartbeat_settings()) {
@@ -873,13 +888,8 @@
                     base::Value(container.heartbeat_enabled()), nullptr);
     }
     if (container.has_heartbeat_frequency()) {
-      std::unique_ptr<base::Value> value(
-          DecodeIntegerValue(container.heartbeat_frequency()));
-      if (value) {
-        policies->Set(key::kHeartbeatFrequency, POLICY_LEVEL_MANDATORY,
-                      POLICY_SCOPE_MACHINE, POLICY_SOURCE_CLOUD,
-                      std::move(*value), nullptr);
-      }
+      DecodeIntegerReportingPolicy(policies, key::kHeartbeatFrequency,
+                                   container.heartbeat_frequency());
     }
   }
 
diff --git a/chrome/browser/ash/policy/core/device_policy_decoder_unittest.cc b/chrome/browser/ash/policy/core/device_policy_decoder_unittest.cc
index f8b74ad..227b4865 100644
--- a/chrome/browser/ash/policy/core/device_policy_decoder_unittest.cc
+++ b/chrome/browser/ash/policy/core/device_policy_decoder_unittest.cc
@@ -67,6 +67,13 @@
  protected:
   std::unique_ptr<base::Value> GetWallpaperDict() const;
   std::unique_ptr<base::Value> GetBluetoothServiceAllowedList() const;
+  void DecodeDevicePolicyTestHelper(
+      const em::ChromeDeviceSettingsProto& device_policy,
+      const std::string& policy_path,
+      base::Value expected_value) const;
+  void DecodeUnsetDevicePolicyTestHelper(
+      const em::ChromeDeviceSettingsProto& device_policy,
+      const std::string& policy_path) const;
 };
 
 std::unique_ptr<base::Value> DevicePolicyDecoderTest::GetWallpaperDict() const {
@@ -87,6 +94,36 @@
   return list;
 }
 
+void DevicePolicyDecoderTest::DecodeDevicePolicyTestHelper(
+    const em::ChromeDeviceSettingsProto& device_policy,
+    const std::string& policy_path,
+    base::Value expected_value) const {
+  PolicyBundle bundle;
+  PolicyMap& policies = bundle.Get(PolicyNamespace(POLICY_DOMAIN_CHROME, ""));
+
+  base::WeakPtr<ExternalDataManager> external_data_manager;
+
+  DecodeDevicePolicy(device_policy, external_data_manager, &policies);
+
+  const base::Value* actual_value = policies.GetValue(policy_path);
+  ASSERT_NE(actual_value, nullptr);
+  EXPECT_EQ(*actual_value, expected_value);
+}
+
+void DevicePolicyDecoderTest::DecodeUnsetDevicePolicyTestHelper(
+    const em::ChromeDeviceSettingsProto& device_policy,
+    const std::string& policy_path) const {
+  PolicyBundle bundle;
+  PolicyMap& policies = bundle.Get(PolicyNamespace(POLICY_DOMAIN_CHROME, ""));
+
+  base::WeakPtr<ExternalDataManager> external_data_manager;
+
+  DecodeDevicePolicy(device_policy, external_data_manager, &policies);
+
+  const base::Value* actual_value = policies.GetValue(policy_path);
+  EXPECT_EQ(actual_value, nullptr);
+}
+
 TEST_F(DevicePolicyDecoderTest, DecodeJsonStringAndNormalizeJSONParseError) {
   std::string error;
   absl::optional<base::Value> decoded_json = DecodeJsonStringAndNormalize(
@@ -173,130 +210,136 @@
 }
 
 TEST_F(DevicePolicyDecoderTest, ReportDeviceLoginLogout) {
-  PolicyBundle bundle;
-  PolicyMap& policies = bundle.Get(PolicyNamespace(POLICY_DOMAIN_CHROME, ""));
-
-  base::WeakPtr<ExternalDataManager> external_data_manager;
-
   em::ChromeDeviceSettingsProto device_policy;
-  device_policy.mutable_device_reporting()->set_report_login_logout(true);
 
-  DecodeDevicePolicy(device_policy, external_data_manager, &policies);
+  DecodeUnsetDevicePolicyTestHelper(device_policy,
+                                    key::kReportDeviceLoginLogout);
 
-  const base::Value* report_device_login_logout_value =
-      policies.GetValue(key::kReportDeviceLoginLogout);
-  ASSERT_NE(report_device_login_logout_value, nullptr);
-  ASSERT_TRUE(report_device_login_logout_value->is_bool());
-  EXPECT_TRUE(report_device_login_logout_value->GetBool());
+  base::Value report_login_logout_value(true);
+  device_policy.mutable_device_reporting()->set_report_login_logout(
+      report_login_logout_value.GetBool());
+
+  DecodeDevicePolicyTestHelper(device_policy, key::kReportDeviceLoginLogout,
+                               std::move(report_login_logout_value));
+}
+
+TEST_F(DevicePolicyDecoderTest, ReportDeviceNetworkTelemetryCollectionRateMs) {
+  em::ChromeDeviceSettingsProto device_policy;
+
+  DecodeUnsetDevicePolicyTestHelper(
+      device_policy, key::kReportDeviceNetworkTelemetryCollectionRateMs);
+
+  base::Value collection_rate_ms_value(120000);
+  device_policy.mutable_device_reporting()
+      ->set_report_network_telemetry_collection_rate_ms(
+          collection_rate_ms_value.GetInt());
+
+  DecodeDevicePolicyTestHelper(
+      device_policy, key::kReportDeviceNetworkTelemetryCollectionRateMs,
+      std::move(collection_rate_ms_value));
+}
+
+TEST_F(DevicePolicyDecoderTest,
+       ReportDeviceNetworkTelemetryEventCheckingRateMs) {
+  em::ChromeDeviceSettingsProto device_policy;
+
+  DecodeUnsetDevicePolicyTestHelper(
+      device_policy, key::kReportDeviceNetworkTelemetryEventCheckingRateMs);
+
+  base::Value event_checking_rate_ms_value(80000);
+  device_policy.mutable_device_reporting()
+      ->set_report_network_telemetry_event_checking_rate_ms(
+          event_checking_rate_ms_value.GetInt());
+
+  DecodeDevicePolicyTestHelper(
+      device_policy, key::kReportDeviceNetworkTelemetryEventCheckingRateMs,
+      std::move(event_checking_rate_ms_value));
 }
 
 TEST_F(DevicePolicyDecoderTest, EnableDeviceGranularReporting) {
-  PolicyBundle bundle;
-  PolicyMap& policies = bundle.Get(PolicyNamespace(POLICY_DOMAIN_CHROME, ""));
-
-  base::WeakPtr<ExternalDataManager> external_data_manager;
-
   em::ChromeDeviceSettingsProto device_policy;
-  device_policy.mutable_device_reporting()->set_enable_granular_reporting(true);
 
-  DecodeDevicePolicy(device_policy, external_data_manager, &policies);
+  DecodeUnsetDevicePolicyTestHelper(device_policy,
+                                    key::kEnableDeviceGranularReporting);
 
-  const base::Value* enable_granular_reporting_value =
-      policies.GetValue(key::kEnableDeviceGranularReporting);
-  ASSERT_NE(enable_granular_reporting_value, nullptr);
-  ASSERT_TRUE(enable_granular_reporting_value->is_bool());
-  EXPECT_TRUE(enable_granular_reporting_value->GetBool());
+  base::Value enable_granular_reporting_value(true);
+  device_policy.mutable_device_reporting()->set_enable_granular_reporting(
+      enable_granular_reporting_value.GetBool());
+
+  DecodeDevicePolicyTestHelper(device_policy,
+                               key::kEnableDeviceGranularReporting,
+                               std::move(enable_granular_reporting_value));
 }
 
 TEST_F(DevicePolicyDecoderTest, ReportDeviceAudioStatus) {
-  PolicyBundle bundle;
-  PolicyMap& policies = bundle.Get(PolicyNamespace(POLICY_DOMAIN_CHROME, ""));
-
-  base::WeakPtr<ExternalDataManager> external_data_manager;
-
   em::ChromeDeviceSettingsProto device_policy;
-  device_policy.mutable_device_reporting()->set_report_audio_status(true);
 
-  DecodeDevicePolicy(device_policy, external_data_manager, &policies);
+  DecodeUnsetDevicePolicyTestHelper(device_policy,
+                                    key::kReportDeviceAudioStatus);
 
-  const base::Value* report_device_audio_status_value =
-      policies.GetValue(key::kReportDeviceAudioStatus);
-  ASSERT_NE(report_device_audio_status_value, nullptr);
-  ASSERT_TRUE(report_device_audio_status_value->is_bool());
-  EXPECT_TRUE(report_device_audio_status_value->GetBool());
+  base::Value report_audio_status_value(true);
+  device_policy.mutable_device_reporting()->set_report_audio_status(
+      report_audio_status_value.GetBool());
+
+  DecodeDevicePolicyTestHelper(device_policy, key::kReportDeviceAudioStatus,
+                               std::move(report_audio_status_value));
 }
 
 TEST_F(DevicePolicyDecoderTest, ReportDeviceSecurityStatus) {
-  PolicyBundle bundle;
-  PolicyMap& policies = bundle.Get(PolicyNamespace(POLICY_DOMAIN_CHROME, ""));
-
-  base::WeakPtr<ExternalDataManager> external_data_manager;
-
   em::ChromeDeviceSettingsProto device_policy;
-  device_policy.mutable_device_reporting()->set_report_security_status(true);
 
-  DecodeDevicePolicy(device_policy, external_data_manager, &policies);
+  DecodeUnsetDevicePolicyTestHelper(device_policy,
+                                    key::kReportDeviceSecurityStatus);
 
-  const base::Value* report_device_security_status_value =
-      policies.GetValue(key::kReportDeviceSecurityStatus);
-  ASSERT_NE(report_device_security_status_value, nullptr);
-  ASSERT_TRUE(report_device_security_status_value->is_bool());
-  EXPECT_TRUE(report_device_security_status_value->GetBool());
+  base::Value report_security_status_value(true);
+  device_policy.mutable_device_reporting()->set_report_security_status(
+      report_security_status_value.GetBool());
+
+  DecodeDevicePolicyTestHelper(device_policy, key::kReportDeviceSecurityStatus,
+                               std::move(report_security_status_value));
 }
 
 TEST_F(DevicePolicyDecoderTest, ReportDeviceNetworkConfiguration) {
-  PolicyBundle bundle;
-  PolicyMap& policies = bundle.Get(PolicyNamespace(POLICY_DOMAIN_CHROME, ""));
-
-  base::WeakPtr<ExternalDataManager> external_data_manager;
-
   em::ChromeDeviceSettingsProto device_policy;
+
+  DecodeUnsetDevicePolicyTestHelper(device_policy,
+                                    key::kReportDeviceNetworkConfiguration);
+
+  base::Value report_network_configuration_value(true);
   device_policy.mutable_device_reporting()->set_report_network_configuration(
-      true);
+      report_network_configuration_value.GetBool());
 
-  DecodeDevicePolicy(device_policy, external_data_manager, &policies);
-
-  const base::Value* report_device_network_configuration_value =
-      policies.GetValue(key::kReportDeviceNetworkConfiguration);
-  ASSERT_NE(report_device_network_configuration_value, nullptr);
-  ASSERT_TRUE(report_device_network_configuration_value->is_bool());
-  EXPECT_TRUE(report_device_network_configuration_value->GetBool());
+  DecodeDevicePolicyTestHelper(device_policy,
+                               key::kReportDeviceNetworkConfiguration,
+                               std::move(report_network_configuration_value));
 }
 
 TEST_F(DevicePolicyDecoderTest, ReportDeviceNetworkStatus) {
-  PolicyBundle bundle;
-  PolicyMap& policies = bundle.Get(PolicyNamespace(POLICY_DOMAIN_CHROME, ""));
-
-  base::WeakPtr<ExternalDataManager> external_data_manager;
-
   em::ChromeDeviceSettingsProto device_policy;
-  device_policy.mutable_device_reporting()->set_report_network_status(true);
 
-  DecodeDevicePolicy(device_policy, external_data_manager, &policies);
+  DecodeUnsetDevicePolicyTestHelper(device_policy,
+                                    key::kReportDeviceNetworkStatus);
 
-  const base::Value* report_device_network_status_value =
-      policies.GetValue(key::kReportDeviceNetworkStatus);
-  ASSERT_NE(report_device_network_status_value, nullptr);
-  ASSERT_TRUE(report_device_network_status_value->is_bool());
-  EXPECT_TRUE(report_device_network_status_value->GetBool());
+  base::Value report_network_status_value(true);
+  device_policy.mutable_device_reporting()->set_report_network_status(
+      report_network_status_value.GetBool());
+
+  DecodeDevicePolicyTestHelper(device_policy, key::kReportDeviceNetworkStatus,
+                               std::move(report_network_status_value));
 }
 
 TEST_F(DevicePolicyDecoderTest, kReportDeviceOsUpdateStatus) {
-  PolicyBundle bundle;
-  PolicyMap& policies = bundle.Get(PolicyNamespace(POLICY_DOMAIN_CHROME, ""));
-
-  base::WeakPtr<ExternalDataManager> external_data_manager;
-
   em::ChromeDeviceSettingsProto device_policy;
-  device_policy.mutable_device_reporting()->set_report_os_update_status(true);
 
-  DecodeDevicePolicy(device_policy, external_data_manager, &policies);
+  DecodeUnsetDevicePolicyTestHelper(device_policy,
+                                    key::kReportDeviceOsUpdateStatus);
 
-  const base::Value* report_device_os_update_status_value =
-      policies.GetValue(key::kReportDeviceOsUpdateStatus);
-  ASSERT_NE(report_device_os_update_status_value, nullptr);
-  ASSERT_TRUE(report_device_os_update_status_value->is_bool());
-  EXPECT_TRUE(report_device_os_update_status_value->GetBool());
+  base::Value report_os_update_status_value(true);
+  device_policy.mutable_device_reporting()->set_report_os_update_status(
+      report_os_update_status_value.GetBool());
+
+  DecodeDevicePolicyTestHelper(device_policy, key::kReportDeviceOsUpdateStatus,
+                               std::move(report_os_update_status_value));
 }
 
 TEST_F(DevicePolicyDecoderTest, DecodeServiceUUIDListSuccess) {
@@ -321,17 +364,19 @@
 TEST_F(DevicePolicyDecoderTest,
        DecodeLoginScreenPromptOnMultipleMatchingCertificates) {
   em::ChromeDeviceSettingsProto device_policy;
-  device_policy.mutable_login_screen_prompt_on_multiple_matching_certificates()
-      ->set_value(true);
 
-  PolicyMap policies;
-  DecodeDevicePolicy(device_policy, /*external_data_manager=*/{}, &policies);
-
-  const base::Value* policy_value = policies.GetValue(
+  DecodeUnsetDevicePolicyTestHelper(
+      device_policy,
       key::kDeviceLoginScreenPromptOnMultipleMatchingCertificates);
-  ASSERT_NE(policy_value, nullptr);
-  ASSERT_TRUE(policy_value->is_bool());
-  EXPECT_TRUE(policy_value->GetBool());
+
+  base::Value login_screen_prompt_value(true);
+  device_policy.mutable_login_screen_prompt_on_multiple_matching_certificates()
+      ->set_value(login_screen_prompt_value.GetBool());
+
+  DecodeDevicePolicyTestHelper(
+      device_policy,
+      key::kDeviceLoginScreenPromptOnMultipleMatchingCertificates,
+      std::move(login_screen_prompt_value));
 }
 
 }  // namespace policy
diff --git a/chrome/browser/ash/settings/device_settings_provider.cc b/chrome/browser/ash/settings/device_settings_provider.cc
index 3a48ae0..7748718 100644
--- a/chrome/browser/ash/settings/device_settings_provider.cc
+++ b/chrome/browser/ash/settings/device_settings_provider.cc
@@ -137,6 +137,8 @@
     kReportDeviceNetworkConfiguration,
     kReportDeviceNetworkInterfaces,
     kReportDeviceNetworkStatus,
+    kReportDeviceNetworkTelemetryCollectionRateMs,
+    kReportDeviceNetworkTelemetryEventCheckingRateMs,
     kReportDeviceSessionStatus,
     kReportDeviceSecurityStatus,
     kReportDeviceTimezoneInfo,
@@ -738,6 +740,17 @@
       new_values_cache->SetBoolean(kReportDeviceLoginLogout,
                                    reporting_policy.report_login_logout());
     }
+    if (reporting_policy.has_report_network_telemetry_collection_rate_ms()) {
+      new_values_cache->SetInteger(
+          kReportDeviceNetworkTelemetryCollectionRateMs,
+          reporting_policy.report_network_telemetry_collection_rate_ms());
+    }
+    if (reporting_policy
+            .has_report_network_telemetry_event_checking_rate_ms()) {
+      new_values_cache->SetInteger(
+          kReportDeviceNetworkTelemetryEventCheckingRateMs,
+          reporting_policy.report_network_telemetry_event_checking_rate_ms());
+    }
   }
 }
 
diff --git a/chrome/browser/ash/settings/device_settings_provider_unittest.cc b/chrome/browser/ash/settings/device_settings_provider_unittest.cc
index b23f7b1..0e8efed 100644
--- a/chrome/browser/ash/settings/device_settings_provider_unittest.cc
+++ b/chrome/browser/ash/settings/device_settings_provider_unittest.cc
@@ -116,6 +116,8 @@
     proto->set_report_app_info(enable_reporting);
     proto->set_report_print_jobs(enable_reporting);
     proto->set_report_login_logout(enable_reporting);
+    proto->set_report_network_telemetry_collection_rate_ms(frequency);
+    proto->set_report_network_telemetry_event_checking_rate_ms(frequency);
     proto->set_device_status_frequency(frequency);
     BuildAndInstallDevicePolicy();
   }
@@ -208,9 +210,17 @@
       EXPECT_EQ(expected_enable_value, *provider_->Get(setting))
           << "Value for " << setting << " does not match expected";
     }
+
+    const char* const reporting_frequency_settings[] = {
+        kReportUploadFrequency,
+        kReportDeviceNetworkTelemetryCollectionRateMs,
+        kReportDeviceNetworkTelemetryEventCheckingRateMs,
+    };
     const base::Value expected_frequency_value(expected_frequency);
-    EXPECT_EQ(expected_frequency_value,
-              *provider_->Get(kReportUploadFrequency));
+    for (auto* frequency_setting : reporting_frequency_settings) {
+      EXPECT_EQ(expected_frequency_value, *provider_->Get(frequency_setting))
+          << "Value for " << frequency_setting << " does not match expected";
+    }
   }
 
   // Helper routine to ensure log upload policy has been correctly
diff --git a/chrome/browser/ash/wilco_dtc_supportd/wilco_dtc_supportd_messaging.cc b/chrome/browser/ash/wilco_dtc_supportd/wilco_dtc_supportd_messaging.cc
index 039f4b4..54fd74c 100644
--- a/chrome/browser/ash/wilco_dtc_supportd/wilco_dtc_supportd_messaging.cc
+++ b/chrome/browser/ash/wilco_dtc_supportd/wilco_dtc_supportd_messaging.cc
@@ -29,6 +29,7 @@
 #include "extensions/browser/api/messaging/native_message_host.h"
 #include "extensions/browser/extension_registry.h"
 #include "extensions/common/api/messaging/messaging_endpoint.h"
+#include "extensions/common/api/messaging/serialization_format.h"
 #include "extensions/common/extension.h"
 #include "mojo/public/cpp/system/handle.h"
 #include "url/gurl.h"
@@ -348,7 +349,8 @@
     base::OnceCallback<void(const std::string& response)>
         send_response_callback) {
   const extensions::PortId port_id(base::UnguessableToken::Create(),
-                                   1 /* port_number */, true /* is_opener */);
+                                   1 /* port_number */, true /* is_opener */,
+                                   extensions::SerializationFormat::kJson);
   extensions::MessageService* const message_service =
       extensions::MessageService::Get(profile);
   auto native_message_host =
diff --git a/chrome/browser/chromeos/eche_app/eche_app_manager_factory.cc b/chrome/browser/chromeos/eche_app/eche_app_manager_factory.cc
index 46417b6..8fb62d0 100644
--- a/chrome/browser/chromeos/eche_app/eche_app_manager_factory.cc
+++ b/chrome/browser/chromeos/eche_app/eche_app_manager_factory.cc
@@ -29,6 +29,9 @@
 #include "chromeos/components/eche_app_ui/eche_uid_provider.h"
 #include "chromeos/components/eche_app_ui/system_info.h"
 #include "chromeos/components/phonehub/phone_hub_manager.h"
+#include "chromeos/services/secure_channel/presence_monitor_impl.h"
+#include "chromeos/services/secure_channel/public/cpp/client/presence_monitor_client_impl.h"
+#include "chromeos/services/secure_channel/public/cpp/shared/presence_monitor.h"
 #include "chromeos/strings/grit/chromeos_strings.h"
 #include "components/keyed_service/content/browser_context_dependency_manager.h"
 #include "components/pref_registry/pref_registry_syncable.h"
@@ -197,9 +200,17 @@
   if (!secure_channel_client)
     return nullptr;
 
+  auto presence_monitor =
+      std::make_unique<secure_channel::PresenceMonitorImpl>();
+  std::unique_ptr<secure_channel::PresenceMonitorClient>
+      presence_monitor_client =
+          secure_channel::PresenceMonitorClientImpl::Factory::Create(
+              std::move(presence_monitor));
+
   return new EcheAppManager(
       profile->GetPrefs(), GetSystemInfo(profile), phone_hub_manager,
       device_sync_client, multidevice_setup_client, secure_channel_client,
+      std::move(presence_monitor_client),
       base::BindRepeating(&LaunchEcheApp, profile),
       base::BindRepeating(&CloseEcheApp, profile),
       base::BindRepeating(&EcheAppManagerFactory::ShowNotification,
diff --git a/chrome/browser/chromeos/extensions/file_manager/private_api_misc.cc b/chrome/browser/chromeos/extensions/file_manager/private_api_misc.cc
index 3a7bdf74..47e4487 100644
--- a/chrome/browser/chromeos/extensions/file_manager/private_api_misc.cc
+++ b/chrome/browser/chromeos/extensions/file_manager/private_api_misc.cc
@@ -595,9 +595,17 @@
                                          DevToolsToggleAction::Inspect());
       break;
     case extensions::api::file_manager_private::INSPECTION_TYPE_BACKGROUND:
-      // Open inspector for background page.
-      extensions::devtools_util::InspectBackgroundPage(
-          extension(), Profile::FromBrowserContext(browser_context()));
+      // Open inspector for background page if extension pointer is not null.
+      // Files app SWA is not an extension and thus has no associated background
+      // page.
+      if (extension()) {
+        extensions::devtools_util::InspectBackgroundPage(
+            extension(), Profile::FromBrowserContext(browser_context()));
+      } else {
+        return RespondNow(
+            Error(base::StringPrintf("Inspection type(%d) not supported.",
+                                     static_cast<int>(params->type))));
+      }
       break;
     default:
       NOTREACHED();
diff --git a/chrome/browser/chromeos/extensions/file_manager/system_notification_manager.cc b/chrome/browser/chromeos/extensions/file_manager/system_notification_manager.cc
index 78cb4a9..cc68710 100644
--- a/chrome/browser/chromeos/extensions/file_manager/system_notification_manager.cc
+++ b/chrome/browser/chromeos/extensions/file_manager/system_notification_manager.cc
@@ -353,14 +353,12 @@
     base::Value::ListView& event_arguments) {
   std::unique_ptr<message_center::Notification> notification;
   file_manager_private::DriveConfirmDialogEvent dialog_event;
-  const char* id;
   std::u16string title =
       l10n_util::GetStringUTF16(IDS_FILE_BROWSER_DRIVE_DIRECTORY_LABEL);
   std::u16string message;
   if (file_manager_private::DriveConfirmDialogEvent::Populate(
           event_arguments[0], &dialog_event)) {
     std::vector<message_center::ButtonInfo> notification_buttons;
-    id = file_manager_private::ToString(dialog_event.type);
     scoped_refptr<message_center::NotificationDelegate> delegate =
         new message_center::HandleNotificationClickDelegate(base::BindRepeating(
             &SystemNotificationManager::HandleDriveDialogClick,
diff --git a/chrome/browser/chromeos/extensions/incoming_native_messaging_apitest.cc b/chrome/browser/chromeos/extensions/incoming_native_messaging_apitest.cc
index 5ea3807c..fa4a0f9 100644
--- a/chrome/browser/chromeos/extensions/incoming_native_messaging_apitest.cc
+++ b/chrome/browser/chromeos/extensions/incoming_native_messaging_apitest.cc
@@ -21,6 +21,7 @@
 #include "extensions/browser/api/messaging/native_message_host.h"
 #include "extensions/common/api/messaging/messaging_endpoint.h"
 #include "extensions/common/api/messaging/port_id.h"
+#include "extensions/common/api/messaging/serialization_format.h"
 #include "extensions/test/result_catcher.h"
 #include "ipc/ipc_message.h"
 #include "testing/gmock/include/gmock/gmock.h"
@@ -75,7 +76,8 @@
       std::unique_ptr<extensions::NativeMessageHost> native_message_host) {
     auto* const message_service = extensions::MessageService::Get(profile());
     const extensions::PortId port_id(base::UnguessableToken::Create(),
-                                     1 /* port_number */, true /* is_opener */);
+                                     1 /* port_number */, true /* is_opener */,
+                                     extensions::SerializationFormat::kJson);
     auto native_message_port = std::make_unique<extensions::NativeMessagePort>(
         message_service->GetChannelDelegate(), port_id,
         std::move(native_message_host));
diff --git a/chrome/browser/commerce/merchant_viewer/android/BUILD.gn b/chrome/browser/commerce/merchant_viewer/android/BUILD.gn
index 0b2a36e3..d4e3ae3 100644
--- a/chrome/browser/commerce/merchant_viewer/android/BUILD.gn
+++ b/chrome/browser/commerce/merchant_viewer/android/BUILD.gn
@@ -180,6 +180,7 @@
     "//third_party/android_deps:espresso_java",
     "//third_party/android_deps:protobuf_lite_runtime_java",
     "//third_party/android_support_test_runner:runner_java",
+    "//third_party/androidx:androidx_annotation_annotation_java",
     "//third_party/androidx:androidx_appcompat_appcompat_resources_java",
     "//third_party/androidx:androidx_test_core_java",
     "//third_party/androidx:androidx_test_runner_java",
diff --git a/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustBottomSheetCoordinator.java b/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustBottomSheetCoordinator.java
index 3b07c40..eaafad6 100644
--- a/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustBottomSheetCoordinator.java
+++ b/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustBottomSheetCoordinator.java
@@ -74,8 +74,8 @@
     }
 
     /** Displays the details tab sheet. */
-    public void requestOpenSheet(GURL url, String title) {
-        setupSheet();
+    public void requestOpenSheet(GURL url, String title, Runnable onBottomSheetDismissed) {
+        setupSheet(onBottomSheetDismissed);
         mMediator.navigateToUrl(url, title);
         mBottomSheetController.requestShowContent(mSheetContent, true);
     }
@@ -85,7 +85,7 @@
         mBottomSheetController.hideContent(mSheetContent, true);
     }
 
-    private void setupSheet() {
+    private void setupSheet(Runnable onBottomSheetDismissed) {
         if (mSheetContent != null) {
             return;
         }
@@ -106,6 +106,13 @@
             public void onSheetContentChanged(BottomSheetContent newContent) {
                 if (newContent != mSheetContent) {
                     mMetrics.recordMetricsForBottomSheetClosed(mCloseReason);
+                    if (onBottomSheetDismissed != null
+                            && (mCloseReason == StateChangeReason.NONE
+                                    || mCloseReason == StateChangeReason.SWIPE
+                                    || mCloseReason == StateChangeReason.BACK_PRESS
+                                    || mCloseReason == StateChangeReason.TAP_SCRIM)) {
+                        onBottomSheetDismissed.run();
+                    }
                     destroySheet();
                 }
             }
diff --git a/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustMessageViewModel.java b/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustMessageViewModel.java
index e3a00b5..3bd604a 100644
--- a/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustMessageViewModel.java
+++ b/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustMessageViewModel.java
@@ -41,8 +41,9 @@
         /**
          * Called when message primary action is tapped.
          * @param trustSignals The signal associated with this message.
+         * @param messageAssociatedUrl The url associated with this message context.
          */
-        void onMessagePrimaryAction(MerchantTrustSignals trustSignals);
+        void onMessagePrimaryAction(MerchantTrustSignals trustSignals, String messageAssociatedUrl);
     }
 
     public static PropertyModel create(Context context, MerchantTrustSignals trustSignals,
@@ -62,7 +63,9 @@
                 .with(MessageBannerProperties.ON_DISMISSED,
                         (reason) -> actionsHandler.onMessageDismissed(reason, messageAssociatedUrl))
                 .with(MessageBannerProperties.ON_PRIMARY_ACTION,
-                        () -> actionsHandler.onMessagePrimaryAction(trustSignals))
+                        ()
+                                -> actionsHandler.onMessagePrimaryAction(
+                                        trustSignals, messageAssociatedUrl))
                 .build();
     }
 
diff --git a/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustMetrics.java b/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustMetrics.java
index 57354de..3806ff8 100644
--- a/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustMetrics.java
+++ b/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustMetrics.java
@@ -37,6 +37,24 @@
         int MAX_VALUE = 2;
     }
 
+    /**
+     * Which ui the bottom sheet is opened from.
+     *
+     * Needs to stay in sync with MerchantTrustBottomSheetOpenedSource in enums.xml. These values
+     * are persisted to logs. Entries should not be renumbered and numeric values should never be
+     * reused.
+     */
+    @IntDef({BottomSheetOpenedSource.UNKNOWN, BottomSheetOpenedSource.FROM_MESSAGE,
+            BottomSheetOpenedSource.FROM_PAGE_INFO})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BottomSheetOpenedSource {
+        int UNKNOWN = 0;
+        int FROM_MESSAGE = 1;
+        int FROM_PAGE_INFO = 2;
+        // Always update MAX_VALUE to match the last item in the list.
+        int MAX_VALUE = 2;
+    }
+
     // Metrics for merchant trust signal message.
     private boolean mDidRecordMessagePrepared;
     private boolean mDidRecordMessageShown;
diff --git a/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustSignalsCoordinator.java b/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustSignalsCoordinator.java
index 009da682..be39af0 100644
--- a/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustSignalsCoordinator.java
+++ b/chrome/browser/commerce/merchant_viewer/android/java/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustSignalsCoordinator.java
@@ -16,6 +16,7 @@
 
 import org.chromium.base.supplier.ObservableSupplier;
 import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
+import org.chromium.chrome.browser.merchant_viewer.MerchantTrustMetrics.BottomSheetOpenedSource;
 import org.chromium.chrome.browser.merchant_viewer.MerchantTrustMetrics.MessageClearReason;
 import org.chromium.chrome.browser.merchant_viewer.proto.MerchantTrustSignalsOuterClass.MerchantTrustSignals;
 import org.chromium.chrome.browser.preferences.Pref;
@@ -233,24 +234,37 @@
     }
 
     @Override
-    public void onMessagePrimaryAction(MerchantTrustSignals trustSignals) {
+    public void onMessagePrimaryAction(
+            MerchantTrustSignals trustSignals, String messageAssociatedUrl) {
         mMetrics.recordMetricsForMessageTapped();
-        launchDetailsPage(new GURL(trustSignals.getMerchantDetailsPageUrl()));
+        launchDetailsPage(new GURL(trustSignals.getMerchantDetailsPageUrl()),
+                ()
+                        -> onBottomSheetDismissed(
+                                BottomSheetOpenedSource.FROM_MESSAGE, messageAssociatedUrl));
     }
 
     // PageInfoStoreInfoController.StoreInfoActionHandler implementation.
     @Override
     public void onStoreInfoClicked(MerchantTrustSignals trustSignals) {
-        launchDetailsPage(new GURL(trustSignals.getMerchantDetailsPageUrl()));
+        launchDetailsPage(new GURL(trustSignals.getMerchantDetailsPageUrl()),
+                () -> onBottomSheetDismissed(BottomSheetOpenedSource.FROM_PAGE_INFO, null));
         // If user has clicked the "Store info" row, send a signal to disable {@link
         // FeatureConstants.PAGE_INFO_STORE_INFO_FEATURE}.
         final Tracker tracker = TrackerFactory.getTrackerForProfile(mProfileSupplier.get());
         tracker.notifyEvent(EventConstants.PAGE_INFO_STORE_INFO_ROW_CLICKED);
     }
 
-    private void launchDetailsPage(GURL url) {
+    private void launchDetailsPage(GURL url, Runnable onBottomSheetDismissed) {
         mDetailsTabCoordinator.requestOpenSheet(url,
-                mContext.getResources().getString(R.string.merchant_viewer_preview_sheet_title));
+                mContext.getResources().getString(R.string.merchant_viewer_preview_sheet_title),
+                onBottomSheetDismissed);
+    }
+
+    private void onBottomSheetDismissed(
+            @BottomSheetOpenedSource int openSource, @Nullable String messageAssociatedUrl) {
+        if (openSource == BottomSheetOpenedSource.FROM_MESSAGE && messageAssociatedUrl != null) {
+            maybeShowStoreIcon(messageAssociatedUrl);
+        }
     }
 
     /**
diff --git a/chrome/browser/commerce/merchant_viewer/android/javatests/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustBottomSheetCoordinatorTest.java b/chrome/browser/commerce/merchant_viewer/android/javatests/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustBottomSheetCoordinatorTest.java
index 28c2ca1..19789ad 100644
--- a/chrome/browser/commerce/merchant_viewer/android/javatests/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustBottomSheetCoordinatorTest.java
+++ b/chrome/browser/commerce/merchant_viewer/android/javatests/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustBottomSheetCoordinatorTest.java
@@ -84,6 +84,9 @@
     @Mock
     private MerchantTrustBottomSheetMediator mMockMediator;
 
+    @Mock
+    private Runnable mMockOnBottomSheetDismissed;
+
     @Captor
     private ArgumentCaptor<EmptyBottomSheetObserver> mBottomSheetObserverCaptor;
 
@@ -119,8 +122,10 @@
     }
 
     private void requestOpenSheetAndVerify() {
-        TestThreadUtils.runOnUiThreadBlocking(
-                () -> { mDetailsTabCoordinator.requestOpenSheet(mMockGurl, DUMMY_SHEET_TITLE); });
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            mDetailsTabCoordinator.requestOpenSheet(
+                    mMockGurl, DUMMY_SHEET_TITLE, mMockOnBottomSheetDismissed);
+        });
         verify(mMockMediator, times(1))
                 .setupSheetWebContents(any(ThinWebView.class), any(PropertyModel.class));
         verify(mMockBottomSheetController, times(1))
@@ -148,6 +153,7 @@
                 () -> { mBottomSheetObserverCaptor.getValue().onSheetContentChanged(null); });
         verify(mMockMetrics, times(1))
                 .recordMetricsForBottomSheetClosed(eq(StateChangeReason.BACK_PRESS));
+        verify(mMockOnBottomSheetDismissed, times(1)).run();
         verify(mMockDecorView, times(1))
                 .removeOnLayoutChangeListener(any(OnLayoutChangeListener.class));
         verify(mMockBottomSheetController, times(1))
diff --git a/chrome/browser/commerce/merchant_viewer/android/javatests/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustSignalsCoordinatorTest.java b/chrome/browser/commerce/merchant_viewer/android/javatests/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustSignalsCoordinatorTest.java
index 3d5cd977..4686099 100644
--- a/chrome/browser/commerce/merchant_viewer/android/javatests/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustSignalsCoordinatorTest.java
+++ b/chrome/browser/commerce/merchant_viewer/android/javatests/src/org/chromium/chrome/browser/merchant_viewer/MerchantTrustSignalsCoordinatorTest.java
@@ -27,6 +27,8 @@
 import org.junit.Test;
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.invocation.InvocationOnMock;
@@ -133,6 +135,9 @@
     @Mock
     private Tracker mMockTracker;
 
+    @Captor
+    private ArgumentCaptor<Runnable> mOnBottomSheetDismissedCaptor;
+
     private static final String FAKE_HOST = "fake_host";
     private static final String DIFFERENT_HOST = "different_host";
     private static final String FAKE_URL = "fake_url";
@@ -487,10 +492,13 @@
     @SmallTest
     @Test
     public void testOnMessagePrimaryAction() {
-        mCoordinator.onMessagePrimaryAction(mDummyMerchantTrustSignals);
+        mCoordinator.onMessagePrimaryAction(mDummyMerchantTrustSignals, FAKE_URL);
         verify(mMockMetrics, times(1)).recordMetricsForMessageTapped();
         verify(mMockDetailsTabCoordinator, times(1))
-                .requestOpenSheet(any(GURL.class), any(String.class));
+                .requestOpenSheet(any(GURL.class), any(String.class),
+                        mOnBottomSheetDismissedCaptor.capture());
+        mOnBottomSheetDismissedCaptor.getValue().run();
+        verify(mCoordinator, times(1)).maybeShowStoreIcon(eq(FAKE_URL));
     }
 
     @SmallTest
@@ -500,9 +508,12 @@
 
         mCoordinator.onStoreInfoClicked(mDummyMerchantTrustSignals);
         verify(mMockDetailsTabCoordinator, times(1))
-                .requestOpenSheet(any(GURL.class), any(String.class));
+                .requestOpenSheet(any(GURL.class), any(String.class),
+                        mOnBottomSheetDismissedCaptor.capture());
         verify(mMockTracker, times(1))
                 .notifyEvent(eq(EventConstants.PAGE_INFO_STORE_INFO_ROW_CLICKED));
+        mOnBottomSheetDismissedCaptor.getValue().run();
+        verify(mCoordinator, times(0)).maybeShowStoreIcon(any());
 
         TrackerFactory.setTrackerForTests(null);
     }
diff --git a/chrome/browser/component_updater/cros_component_installer_chromeos.cc b/chrome/browser/component_updater/cros_component_installer_chromeos.cc
index fe0e50b..15aa188 100644
--- a/chrome/browser/component_updater/cros_component_installer_chromeos.cc
+++ b/chrome/browser/component_updater/cros_component_installer_chromeos.cc
@@ -35,7 +35,7 @@
 
 // All downloadable Chrome OS components.
 const ComponentConfig kConfigs[] = {
-    {"cros-termina", ComponentConfig::PolicyType::kEnvVersion, "960.1",
+    {"cros-termina", ComponentConfig::PolicyType::kEnvVersion, "970.1",
      "e9d960f84f628e1f42d05de4046bb5b3154b6f1f65c08412c6af57a29aecaffb"},
     {"rtanalytics-light", ComponentConfig::PolicyType::kEnvVersion, "96.0",
      "69f09d33c439c2ab55bbbe24b47ab55cb3f6c0bd1f1ef46eefea3216ec925038"},
diff --git a/chrome/browser/component_updater/recovery_improved_component_installer.cc b/chrome/browser/component_updater/recovery_improved_component_installer.cc
index 42b026e..a7ba327 100644
--- a/chrome/browser/component_updater/recovery_improved_component_installer.cc
+++ b/chrome/browser/component_updater/recovery_improved_component_installer.cc
@@ -110,8 +110,13 @@
 void RecoveryComponentActionHandler::WaitForCommand(base::Process process) {
   int exit_code = 0;
   const base::TimeDelta kMaxWaitTime = base::Seconds(600);
-  const bool succeeded =
-      process.WaitForExitWithTimeout(kMaxWaitTime, &exit_code);
+  bool succeeded = false;
+  if (!process.IsValid()) {
+    exit_code =
+        static_cast<int>(update_client::InstallError::LAUNCH_PROCESS_FAILED)
+  } else {
+    succeeded = process.WaitForExitWithTimeout(kMaxWaitTime, &exit_code);
+  }
   base::DeletePathRecursively(unpack_path_);
   main_task_runner_->PostTask(
       FROM_HERE, base::BindOnce(std::move(callback_), succeeded, exit_code, 0));
diff --git a/chrome/browser/content_creation/reactions/internal/android/BUILD.gn b/chrome/browser/content_creation/reactions/internal/android/BUILD.gn
index aac4017..b4e3a20 100644
--- a/chrome/browser/content_creation/reactions/internal/android/BUILD.gn
+++ b/chrome/browser/content_creation/reactions/internal/android/BUILD.gn
@@ -10,12 +10,14 @@
     "java/src/org/chromium/chrome/browser/content_creation/reactions/LightweightReactionsCoordinatorFactory.java",
     "java/src/org/chromium/chrome/browser/content_creation/reactions/LightweightReactionsCoordinatorImpl.java",
     "java/src/org/chromium/chrome/browser/content_creation/reactions/LightweightReactionsDialog.java",
+    "java/src/org/chromium/chrome/browser/content_creation/reactions/ReactionGifDrawable.java",
     "java/src/org/chromium/chrome/browser/content_creation/reactions/ReactionServiceFactory.java",
     "java/src/org/chromium/chrome/browser/content_creation/reactions/scene/ReactionLayout.java",
     "java/src/org/chromium/chrome/browser/content_creation/reactions/scene/SceneCoordinator.java",
     "java/src/org/chromium/chrome/browser/content_creation/reactions/scene/SceneEditorDelegate.java",
     "java/src/org/chromium/chrome/browser/content_creation/reactions/toolbar/ToolbarControlsDelegate.java",
     "java/src/org/chromium/chrome/browser/content_creation/reactions/toolbar/ToolbarCoordinator.java",
+    "java/src/org/chromium/chrome/browser/content_creation/reactions/toolbar/ToolbarReactionsDelegate.java",
   ]
 
   deps = [
@@ -33,6 +35,7 @@
     "//third_party/androidx:androidx_appcompat_appcompat_java",
     "//third_party/androidx:androidx_appcompat_appcompat_resources_java",
     "//third_party/androidx:androidx_fragment_fragment_java",
+    "//third_party/gif_player:gif_player_java",
     "//ui/android:ui_no_recycler_view_java",
   ]
 
diff --git a/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/LightweightReactionsCoordinatorImpl.java b/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/LightweightReactionsCoordinatorImpl.java
index 010e411..435e65b 100644
--- a/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/LightweightReactionsCoordinatorImpl.java
+++ b/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/LightweightReactionsCoordinatorImpl.java
@@ -59,7 +59,7 @@
      * @param view The root {@link View} of the dialog.
      */
     private void onViewCreated(View view) {
-        mToolbarCoordinator = new ToolbarCoordinator(view, this);
+        mToolbarCoordinator = new ToolbarCoordinator(view, this, mSceneCoordinator);
     }
 
     // LightweightReactionsCoordinator implementation.
diff --git a/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/ReactionGifDrawable.java b/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/ReactionGifDrawable.java
new file mode 100644
index 0000000..449b05f
--- /dev/null
+++ b/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/ReactionGifDrawable.java
@@ -0,0 +1,20 @@
+// 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.
+
+package org.chromium.chrome.browser.content_creation.reactions;
+
+import android.graphics.Bitmap;
+
+import jp.tomorrowkey.android.gifplayer.BaseGifDrawable;
+import jp.tomorrowkey.android.gifplayer.BaseGifImage;
+
+/**
+ * An extension of the BaseGifDrawable class that allows stepping through the frames of the GIF
+ * without updating the View, to facilitate decoding the GIF for exporting purposes.
+ */
+public class ReactionGifDrawable extends BaseGifDrawable {
+    public ReactionGifDrawable(BaseGifImage gifImage, Bitmap.Config bitmapConfig) {
+        super(gifImage, bitmapConfig);
+    }
+}
diff --git a/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/scene/ReactionLayout.java b/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/scene/ReactionLayout.java
index be386a2..136eefb 100644
--- a/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/scene/ReactionLayout.java
+++ b/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/scene/ReactionLayout.java
@@ -53,7 +53,7 @@
         super.onFinishInflate();
         mReaction = findViewById(R.id.reaction_view);
         setUpCopyButton();
-        mDeleteButton = findViewById(R.id.delete_button);
+        setUpDeleteButton();
         mAdjustButton = findViewById(R.id.adjust_button);
     }
 
@@ -94,4 +94,9 @@
             }
         });
     }
+
+    private void setUpDeleteButton() {
+        mDeleteButton = findViewById(R.id.delete_button);
+        mDeleteButton.setOnClickListener(view -> mSceneEditorDelegate.removeReaction(this));
+    }
 }
diff --git a/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/scene/SceneCoordinator.java b/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/scene/SceneCoordinator.java
index 73a00de..68fc88e 100644
--- a/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/scene/SceneCoordinator.java
+++ b/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/scene/SceneCoordinator.java
@@ -11,6 +11,8 @@
 import androidx.appcompat.content.res.AppCompatResources;
 
 import org.chromium.chrome.browser.content_creation.reactions.internal.R;
+import org.chromium.chrome.browser.content_creation.reactions.toolbar.ToolbarReactionsDelegate;
+import org.chromium.components.content_creation.reactions.ReactionMetadata;
 import org.chromium.ui.LayoutInflaterUtils;
 import org.chromium.ui.base.ViewUtils;
 
@@ -20,7 +22,7 @@
 /**
  * Manages the scene UI and the reactions on the scene.
  */
-public class SceneCoordinator implements SceneEditorDelegate {
+public class SceneCoordinator implements SceneEditorDelegate, ToolbarReactionsDelegate {
     private static final int DEFAULT_REACTION_SIZE_DP = 100;
     private static final int REACTION_OFFSET_DP = 45;
     private static final int MAX_REACTION_COUNT = 10;
@@ -78,7 +80,7 @@
         markActiveStatus(reactionLayout, true);
     }
 
-    // SceneEditorCallback implementation.
+    // SceneEditorDelegate implementation.
     @Override
     public boolean canAddReaction() {
         return mReactionLayouts.size() < MAX_REACTION_COUNT;
@@ -104,6 +106,7 @@
 
     @Override
     public void removeReaction(ReactionLayout reactionLayout) {
+        markActiveStatus(reactionLayout, false);
         mSceneBackground.removeView(reactionLayout);
         mReactionLayouts.remove(reactionLayout);
     }
@@ -121,4 +124,10 @@
             mActiveReaction = null;
         }
     }
+
+    // ToolbarReactionsDelegate implementation.
+    @Override
+    public void onToolbarReactionTapped(ReactionMetadata reactionData) {
+        // No-op for now, will either add a new reaction or change the currently selected one.
+    }
 }
diff --git a/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/toolbar/ToolbarCoordinator.java b/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/toolbar/ToolbarCoordinator.java
index e5ba8b4..4df517f 100644
--- a/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/toolbar/ToolbarCoordinator.java
+++ b/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/toolbar/ToolbarCoordinator.java
@@ -13,12 +13,19 @@
  * Coordinator for the Lightweight Reactions toolbar.
  */
 public class ToolbarCoordinator {
-    public ToolbarCoordinator(View parentView, ToolbarControlsDelegate delegate) {
+    private final ToolbarControlsDelegate mControlsDelegate;
+    private final ToolbarReactionsDelegate mReactionsDelegate;
+
+    public ToolbarCoordinator(View parentView, ToolbarControlsDelegate controlsDelegate,
+            ToolbarReactionsDelegate reactionsDelegate) {
+        mControlsDelegate = controlsDelegate;
+        mReactionsDelegate = reactionsDelegate;
+
         RelativeLayout toolbarLayout = parentView.findViewById(R.id.lightweight_reactions_toolbar);
 
         View closeButton = toolbarLayout.findViewById(R.id.close_button);
-        closeButton.setOnClickListener(v -> delegate.cancelButtonTapped());
+        closeButton.setOnClickListener(v -> mControlsDelegate.cancelButtonTapped());
         View doneButton = toolbarLayout.findViewById(R.id.done_button);
-        doneButton.setOnClickListener(v -> delegate.doneButtonTapped());
+        doneButton.setOnClickListener(v -> mControlsDelegate.doneButtonTapped());
     }
 }
diff --git a/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/toolbar/ToolbarReactionsDelegate.java b/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/toolbar/ToolbarReactionsDelegate.java
new file mode 100644
index 0000000..ce9f74e
--- /dev/null
+++ b/chrome/browser/content_creation/reactions/internal/android/java/src/org/chromium/chrome/browser/content_creation/reactions/toolbar/ToolbarReactionsDelegate.java
@@ -0,0 +1,17 @@
+// 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.
+
+package org.chromium.chrome.browser.content_creation.reactions.toolbar;
+
+import org.chromium.components.content_creation.reactions.ReactionMetadata;
+
+/**
+ * Interface used by the toolbar to delegate action handlers for tapping reactions.
+ */
+public interface ToolbarReactionsDelegate {
+    /**
+     * Invoked when the user taps a reaction.
+     */
+    public void onToolbarReactionTapped(ReactionMetadata reactionData);
+}
\ No newline at end of file
diff --git a/chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_api.cc b/chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_api.cc
index cf94fb6..19541a7 100644
--- a/chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_api.cc
+++ b/chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_api.cc
@@ -290,7 +290,8 @@
     return RespondNow(Error("cannot find specified tab"));
   }
 
-  content::RenderFrameHost* frame = web_contents->GetMainFrame();
+  content::RenderFrameHost* frame = RenderFrameHostForTabAndFrameId(
+      browser_context(), params->options.tab_id, params->options.frame_id);
   if (!frame) {
     return RespondNow(Error("cannot find frame"));
   }
diff --git a/chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_api_unittest.cc b/chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_api_unittest.cc
index 392b7ed8..f500bee 100644
--- a/chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_api_unittest.cc
+++ b/chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_api_unittest.cc
@@ -270,6 +270,7 @@
     base::Value::DictStorage dict;
     dict.emplace("appId", app_id);
     dict.emplace("tabId", tab_id_);
+    dict.emplace("frameId", -1);  // Ignored.
     dict.emplace("origin", app_id);
     auto args = std::make_unique<base::Value>(base::Value::Type::LIST);
     args->Append(base::Value(std::move(dict)));
@@ -304,6 +305,7 @@
     base::Value::DictStorage dict;
     dict.emplace("appId", origin);
     dict.emplace("tabId", tab_id_);
+    dict.emplace("frameId", 0 /* main frame */);
     dict.emplace("origin", origin);
     auto args = std::make_unique<base::Value>(base::Value::Type::LIST);
     args->Append(base::Value(std::move(dict)));
diff --git a/chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_browsertest.cc b/chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_browsertest.cc
index 3bcbdb9..eaa2bb5 100644
--- a/chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_browsertest.cc
+++ b/chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_browsertest.cc
@@ -2,8 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/base_switches.h"
+#include "base/strings/strcat.h"
 #include "base/strings/string_util.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/with_feature_override.h"
+#include "build/build_config.h"
 #include "chrome/browser/extensions/component_loader.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
@@ -11,10 +15,15 @@
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/ui_test_utils.h"
 #include "components/embedder_support/switches.h"
+#include "components/permissions/permission_request_manager.h"
+#include "components/permissions/test/permission_request_observer.h"
 #include "components/prefs/pref_service.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/url_loader_interceptor.h"
+#include "device/fido/features.h"
 #include "extensions/browser/pref_names.h"
 #include "extensions/common/extension_features.h"
 #include "net/dns/mock_host_resolver.h"
@@ -30,14 +39,26 @@
 namespace {
 
 constexpr char kCryptoTokenExtensionId[] = "kmendfapggjehodndflmmgagdbamhnfd";
+
+// Origin for running tests with an Origin Trial token. Hostname needs to be
+// from `net::EmbeddedTestServer::CERT_TEST_NAMES`.
 constexpr char kOriginTrialOrigin[] = "https://a.test";
 
+// Domain to serve files from. This should be different from `kOriginTrialToken`
+// domain. Needs to be from `net::EmbeddedTestServer::CERT_TEST_NAMES`.
+constexpr char kNonOriginTrialDomain[] = "b.test";
+
 class CryptotokenBrowserTest : public base::test::WithFeatureOverride,
                                public InProcessBrowserTest {
  protected:
   CryptotokenBrowserTest()
       : base::test::WithFeatureOverride(
-            extensions_features::kU2FSecurityKeyAPI) {}
+            extensions_features::kU2FSecurityKeyAPI) {
+#if defined(OS_WIN)
+    // Don't dispatch requests to the native Windows API.
+    scoped_feature_list_.InitAndDisableFeature(device::kWebAuthUseNativeWinApi);
+#endif
+  }
 
   void SetUpCommandLine(base::CommandLine* command_line) override {
     // The public key for the default privatey key used by the
@@ -73,13 +94,24 @@
 
   void TearDownOnMainThread() override { url_loader_interceptor_.reset(); }
 
+  // Returns the frame to use when attempting to connect to Cryptotoken in the
+  // methods below. Uses the main frame by default or
+  // |frame_to_use_for_connecting_| if a test overrides it.
+  content::RenderFrameHost* FrameToUseForConnecting() {
+    return frame_to_use_for_connecting_ ? frame_to_use_for_connecting_
+                                        : browser()
+                                              ->tab_strip_model()
+                                              ->GetActiveWebContents()
+                                              ->GetMainFrame();
+  }
+
   void ExpectChromeRuntimeIsUndefined() {
     const std::string script = base::StringPrintf(
         R"(let port = chrome.runtime.connect('%s',
               {});)",
         kCryptoTokenExtensionId);
-    const content::EvalJsResult result = content::EvalJs(
-        browser()->tab_strip_model()->GetActiveWebContents(), script);
+    const content::EvalJsResult result =
+        content::EvalJs(FrameToUseForConnecting(), script);
     EXPECT_THAT(
         result.error,
         testing::StartsWith("a JavaScript error:\nTypeError: Cannot read "
@@ -95,8 +127,8 @@
               });
       }))",
         kCryptoTokenExtensionId);
-    const content::EvalJsResult result = content::EvalJs(
-        browser()->tab_strip_model()->GetActiveWebContents(), script);
+    const content::EvalJsResult result =
+        content::EvalJs(FrameToUseForConnecting(), script);
     EXPECT_EQ(true, result);
   }
 
@@ -113,14 +145,89 @@
               });
       }))",
         kCryptoTokenExtensionId);
-    const content::EvalJsResult result = content::EvalJs(
-        browser()->tab_strip_model()->GetActiveWebContents(), script);
+    const content::EvalJsResult result =
+        content::EvalJs(FrameToUseForConnecting(), script);
     EXPECT_EQ("Could not establish connection. Receiving end does not exist.",
               result);
   }
 
+  // Indicates whether `ExpectSignSuccess()` should expect a U2F deprecation
+  // prompt to be shown.
+  enum class PromptExpectation {
+    kNoPrompt,
+    kShowPrompt,
+  };
+
+  void ExpectSignSuccess(const std::string& app_id,
+                         PromptExpectation prompt_expectation) {
+    content::WebContents* web_contents =
+        content::WebContents::FromRenderFrameHost(FrameToUseForConnecting());
+    if (prompt_expectation == PromptExpectation::kShowPrompt) {
+      // Automatically resolve permission prompts shown by Cryptotoken on the
+      // target frame.
+      permissions::PermissionRequestManager* request_manager =
+          permissions::PermissionRequestManager::FromWebContents(web_contents);
+      request_manager->set_auto_response_for_test(
+          permissions::PermissionRequestManager::DENY_ALL);
+    }
+
+    permissions::PermissionRequestObserver permission_request_observer(
+        web_contents);
+    const std::string script = base::StringPrintf(
+        R"(new Promise((resolve,reject) => {
+          chrome.runtime.sendMessage('%s',
+              {
+                type: 'u2f_sign_request',
+                appId: '%s',
+                challenge: 'aGVsbG8gd29ybGQ',
+                registeredKeys: [
+                  {
+                    version: 'U2F_V2',
+                    keyHandle: 'aGVsbG8gd29ybGQ',
+                    transports: ['usb'],
+                    appId: '%s',
+                  }
+                ],
+                timeoutSeconds: 3,
+                requestId: 1
+              },
+              (args) => {
+                  if (chrome.runtime.lastError !== undefined) {
+                    resolve('runtime error: ' + chrome.runtime.lastError);
+                    return;
+                  }
+                  if ('responseData' in args &&
+                      'errorCode' in args.responseData) {
+                    resolve('errorCode:'
+                            + args.responseData.errorCode
+                            + ",errorMessage:"
+                            + args.responseData.errorMessage);
+                    return;
+                  }
+                  reject(); // Requests can never succeed.
+              });
+      }))",
+        kCryptoTokenExtensionId, app_id.c_str(), app_id.c_str());
+    const content::EvalJsResult result =
+        content::EvalJs(FrameToUseForConnecting(), script);
+    if (prompt_expectation == PromptExpectation::kShowPrompt) {
+      // Denied prompt results in a DEVICE_INELIGIBLE error.
+      EXPECT_EQ("errorCode:4,errorMessage:The operation was not allowed",
+                result);
+      EXPECT_EQ(true, permission_request_observer.request_shown());
+    } else {
+      // Without a prompt, the request times out eventually. N.B. the
+      // `timeoutSeconds` parameter above needs to be high enough to allow time
+      // for the prompt to show in the kShowPrompt case, but not too high to
+      // cause test stalls and timeouts
+      EXPECT_EQ("errorCode:5,errorMessage:undefined", result);
+      EXPECT_EQ(false, permission_request_observer.request_shown());
+    }
+  }
+
   net::EmbeddedTestServer http_server_{net::EmbeddedTestServer::TYPE_HTTP};
   net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
+  content::RenderFrameHost* frame_to_use_for_connecting_ = nullptr;
 
  private:
   // content::URLLoaderInterceptor callback
@@ -145,18 +252,24 @@
         "Origin-Trial: %s\n\n",
         kOriginTrialToken);
     content::URLLoaderInterceptor::WriteResponse(
-        headers, "<html><head></head><body>OK</body></html>",
+        headers,
+        "<html><head></head><body>OK\n"
+        "<iframe id=\"otIframe\"></iframe>"
+        "</body></html>",
         params->client.get());
     return true;
   }
 
+#if defined(OS_WIN)
+  base::test::ScopedFeatureList scoped_feature_list_;
+#endif
   std::unique_ptr<content::URLLoaderInterceptor> url_loader_interceptor_;
 };
 
 IN_PROC_BROWSER_TEST_P(CryptotokenBrowserTest, Connect) {
   // CryptoToken can only be connected to if the feature flag is enabled.
   ASSERT_TRUE(ui_test_utils::NavigateToURL(
-      browser(), https_server_.GetURL("a.test", "/empty.html")));
+      browser(), https_server_.GetURL(kNonOriginTrialDomain, "/empty.html")));
   if (IsParamFeatureEnabled()) {
     ExpectConnectSuccess();
   } else {
@@ -164,6 +277,17 @@
   }
 }
 
+IN_PROC_BROWSER_TEST_P(CryptotokenBrowserTest, SignShowsPrompt) {
+  if (!IsParamFeatureEnabled()) {
+    // Can't connect with the API disabled.
+    return;
+  }
+  GURL url = https_server_.GetURL(kNonOriginTrialDomain, "/empty.html");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+  std::string app_id = url::Origin::Create(url).Serialize();
+  ExpectSignSuccess(app_id, PromptExpectation::kShowPrompt);
+}
+
 IN_PROC_BROWSER_TEST_P(CryptotokenBrowserTest, ConnectWithOriginTrial) {
   // Connection succeeds regardless of feature flag state with the origin trial.
   ASSERT_TRUE(
@@ -171,20 +295,107 @@
   ExpectConnectSuccess();
 }
 
+IN_PROC_BROWSER_TEST_P(CryptotokenBrowserTest,
+                       SignWithOriginTrialDoesNotShowPrompt) {
+  GURL url = GURL(kOriginTrialOrigin);
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+  std::string app_id = url::Origin::Create(url).Serialize();
+  ExpectSignSuccess(app_id, PromptExpectation::kNoPrompt);
+}
+
+IN_PROC_BROWSER_TEST_P(CryptotokenBrowserTest,
+                       ConnectWithOriginTrialInCrossOriginIframe) {
+  // Cross-origin iframe can connect with a trial token even if the parent frame
+  // is not enrolled in the trial.
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), https_server_.GetURL(kNonOriginTrialDomain, "/iframe.html")));
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(content::NavigateIframeToURL(web_contents, "test",
+                                           GURL(kOriginTrialOrigin)));
+  frame_to_use_for_connecting_ = content::FrameMatchingPredicate(
+      web_contents->GetPrimaryPage(),
+      base::BindRepeating(&content::FrameIsChildOfMainFrame));
+  ExpectConnectSuccess();
+}
+
+IN_PROC_BROWSER_TEST_P(
+    CryptotokenBrowserTest,
+    SignWithOriginTrialInCrossOriginIframeDoesNotShowPrompt) {
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), https_server_.GetURL(kNonOriginTrialDomain, "/iframe.html")));
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  GURL url = GURL(kOriginTrialOrigin);
+  ASSERT_TRUE(content::NavigateIframeToURL(web_contents, "test", url));
+  frame_to_use_for_connecting_ = content::FrameMatchingPredicate(
+      web_contents->GetPrimaryPage(),
+      base::BindRepeating(&content::FrameIsChildOfMainFrame));
+
+  // Also make sure no permissions are showing on the main frame either.
+  permissions::PermissionRequestManager* request_manager =
+      permissions::PermissionRequestManager::FromWebContents(web_contents);
+  request_manager->set_auto_response_for_test(
+      permissions::PermissionRequestManager::DENY_ALL);
+  permissions::PermissionRequestObserver permission_request_observer(
+      web_contents);
+
+  std::string app_id = url::Origin::Create(url).Serialize();
+  ExpectSignSuccess(app_id, PromptExpectation::kNoPrompt);
+  EXPECT_FALSE(permission_request_observer.request_shown());
+}
+
+IN_PROC_BROWSER_TEST_P(CryptotokenBrowserTest,
+                       OriginTrailDoesNotAffectChildIframes) {
+  GURL parent_url = GURL(kOriginTrialOrigin);
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), parent_url));
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  GURL child_url = https_server_.GetURL(kNonOriginTrialDomain, "/empty.html");
+  ASSERT_TRUE(
+      content::NavigateIframeToURL(web_contents, "otIframe", child_url));
+  frame_to_use_for_connecting_ = content::FrameMatchingPredicate(
+      web_contents->GetPrimaryPage(),
+      base::BindRepeating(&content::FrameIsChildOfMainFrame));
+
+  if (IsParamFeatureEnabled()) {
+    // With the feature flag enabled, the U2F API is active; but the OT that
+    // would suppress the prompt on the main frame, does not suppress the prompt
+    // on the child frame.
+    ExpectConnectSuccess();
+    std::string app_id = url::Origin::Create(child_url).Serialize();
+    ExpectSignSuccess(app_id, PromptExpectation::kShowPrompt);
+  } else {
+    // With the feature flag off, the U2F API is inactive on the child frame,
+    // even though it is enabled via OT on the parent frame.
+    ExpectConnectFailure();
+  }
+}
+
 IN_PROC_BROWSER_TEST_P(CryptotokenBrowserTest, ConnectWithEnterprisePolicy) {
   // Connection succeeds regardless of feature flag state with the enterprise
   // policy overriding deprecation changes.
   browser()->profile()->GetPrefs()->Set(
       extensions::pref_names::kU2fSecurityKeyApiEnabled, base::Value(true));
-  ASSERT_TRUE(
-      ui_test_utils::NavigateToURL(browser(), GURL(kOriginTrialOrigin)));
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), https_server_.GetURL(kNonOriginTrialDomain, "/empty.html")));
   ExpectConnectSuccess();
 }
 
+IN_PROC_BROWSER_TEST_P(CryptotokenBrowserTest,
+                       SignWithEnterprisePolicyDoesNotShowPrompt) {
+  browser()->profile()->GetPrefs()->Set(
+      extensions::pref_names::kU2fSecurityKeyApiEnabled, base::Value(true));
+  GURL url = GURL(https_server_.GetURL(kNonOriginTrialDomain, "/empty.html"));
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+  std::string app_id = url::Origin::Create(url).Serialize();
+  ExpectSignSuccess(app_id, PromptExpectation::kNoPrompt);
+}
+
 IN_PROC_BROWSER_TEST_P(CryptotokenBrowserTest, InsecureOriginCannotConnect) {
   // Connections from insecure origins always fail.
   ASSERT_TRUE(ui_test_utils::NavigateToURL(
-      browser(), http_server_.GetURL("a.test", "/empty.html")));
+      browser(), http_server_.GetURL(kNonOriginTrialDomain, "/empty.html")));
   ExpectChromeRuntimeIsUndefined();
 }
 
diff --git a/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_unittest.cc b/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_unittest.cc
index 42392bc4..45cf0bc 100644
--- a/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_unittest.cc
+++ b/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_unittest.cc
@@ -2249,10 +2249,7 @@
       disable_reason::DISABLE_BLOCKED_BY_POLICY, true);
 
   disable_extension_and_check_allocation(
-      disable_reason::DISABLE_REMOTELY_FOR_MALWARE, true);
-
-  disable_extension_and_check_allocation(
-      disable_reason::DISABLE_REMOTELY_FOR_MALWARE |
+      disable_reason::DISABLE_BLOCKED_BY_POLICY |
           disable_reason::DISABLE_USER_ACTION,
       true);
 
diff --git a/chrome/browser/extensions/api/messaging/native_message_port.cc b/chrome/browser/extensions/api/messaging/native_message_port.cc
index 2d3bd51..0ddc42a 100644
--- a/chrome/browser/extensions/api/messaging/native_message_port.cc
+++ b/chrome/browser/extensions/api/messaging/native_message_port.cc
@@ -13,6 +13,7 @@
 #include "chrome/browser/extensions/api/messaging/native_message_process_host.h"
 #include "content/public/browser/browser_thread.h"
 #include "extensions/common/api/messaging/message.h"
+#include "extensions/common/api/messaging/serialization_format.h"
 
 namespace extensions {
 
@@ -115,8 +116,11 @@
 void NativeMessagePort::PostMessageFromNativeHost(const std::string& message) {
   DCHECK(thread_checker_.CalledOnValidThread());
   if (weak_channel_delegate_) {
+    // Native messaging always uses JSON since a native host doesn't understand
+    // structured cloning serialization.
     weak_channel_delegate_->PostMessage(
-        port_id_, Message(message, false /* user_gesture */));
+        port_id_,
+        Message(message, SerializationFormat::kJson, false /* user_gesture */));
   }
 }
 
diff --git a/chrome/browser/extensions/api/messaging/native_messaging_launch_from_native.cc b/chrome/browser/extensions/api/messaging/native_messaging_launch_from_native.cc
index 1c0249b..498466b 100644
--- a/chrome/browser/extensions/api/messaging/native_messaging_launch_from_native.cc
+++ b/chrome/browser/extensions/api/messaging/native_messaging_launch_from_native.cc
@@ -29,6 +29,7 @@
 #include "extensions/browser/event_router.h"
 #include "extensions/browser/extension_registry.h"
 #include "extensions/common/api/messaging/messaging_endpoint.h"
+#include "extensions/common/api/messaging/serialization_format.h"
 #include "extensions/common/permissions/permission_set.h"
 #include "extensions/common/permissions/permissions_data.h"
 
@@ -228,7 +229,8 @@
     return;
   }
   const extensions::PortId port_id(base::UnguessableToken::Create(),
-                                   1 /* port_number */, true /* is_opener */);
+                                   1 /* port_number */, true /* is_opener */,
+                                   extensions::SerializationFormat::kJson);
   extensions::MessageService* const message_service =
       extensions::MessageService::Get(profile);
   // TODO(crbug.com/967262): Apply policy for allow_user_level.
diff --git a/chrome/browser/extensions/extension_allowlist_unittest.cc b/chrome/browser/extensions/extension_allowlist_unittest.cc
index e262f4d5..600334c 100644
--- a/chrome/browser/extensions/extension_allowlist_unittest.cc
+++ b/chrome/browser/extensions/extension_allowlist_unittest.cc
@@ -149,8 +149,10 @@
   EXPECT_EQ(BitMapBlocklistState::BLOCKLISTED_MALWARE,
             blocklist_prefs::GetSafeBrowsingExtensionBlocklistState(
                 kExtensionId1, extension_prefs()));
-  EXPECT_EQ(disable_reason::DISABLE_REMOTELY_FOR_MALWARE |
-                disable_reason::DISABLE_NOT_ALLOWLISTED,
+  EXPECT_TRUE(blocklist_prefs::HasOmahaBlocklistState(
+      kExtensionId1, BitMapBlocklistState::BLOCKLISTED_MALWARE,
+      extension_prefs()));
+  EXPECT_EQ(disable_reason::DISABLE_NOT_ALLOWLISTED,
             extension_prefs()->GetDisableReasons(kExtensionId1));
   EXPECT_TRUE(IsBlocklisted(kExtensionId1));
 
@@ -164,7 +166,10 @@
   EXPECT_EQ(BitMapBlocklistState::BLOCKLISTED_MALWARE,
             blocklist_prefs::GetSafeBrowsingExtensionBlocklistState(
                 kExtensionId1, extension_prefs()));
-  EXPECT_EQ(disable_reason::DISABLE_REMOTELY_FOR_MALWARE,
+  EXPECT_TRUE(blocklist_prefs::HasOmahaBlocklistState(
+      kExtensionId1, BitMapBlocklistState::BLOCKLISTED_MALWARE,
+      extension_prefs()));
+  EXPECT_EQ(disable_reason::DISABLE_NONE,
             extension_prefs()->GetDisableReasons(kExtensionId1));
   EXPECT_TRUE(IsBlocklisted(kExtensionId1));
 
diff --git a/chrome/browser/extensions/extension_error_ui_default_unittest.cc b/chrome/browser/extensions/extension_error_ui_default_unittest.cc
index b170410..43a7834 100644
--- a/chrome/browser/extensions/extension_error_ui_default_unittest.cc
+++ b/chrome/browser/extensions/extension_error_ui_default_unittest.cc
@@ -9,6 +9,7 @@
 #include "chrome/grit/generated_resources.h"
 #include "chrome/test/base/testing_profile.h"
 #include "content/public/test/browser_task_environment.h"
+#include "extensions/browser/blocklist_extension_prefs.h"
 #include "extensions/browser/disable_reason.h"
 #include "extensions/browser/extension_prefs.h"
 #include "extensions/browser/extension_system.h"
@@ -117,10 +118,9 @@
       extensions::ExtensionBuilder(
           "Bar", extensions::ExtensionBuilder::Type::PLATFORM_APP)
           .Build();
-  extensions::ExtensionPrefs::Get(delegate.GetContext())
-      ->AddDisableReason(
-          extension->id(),
-          extensions::disable_reason::DISABLE_REMOTELY_FOR_MALWARE);
+  extensions::blocklist_prefs::AddOmahaBlocklistState(
+      extension->id(), extensions::BitMapBlocklistState::BLOCKLISTED_MALWARE,
+      extensions::ExtensionPrefs::Get(delegate.GetContext()));
   delegate.InsertForbidden(extension);
 
   extensions::ExtensionErrorUIDefault ui(&delegate);
diff --git a/chrome/browser/extensions/extension_prefs_unittest.cc b/chrome/browser/extensions/extension_prefs_unittest.cc
index 9874822..97ae419 100644
--- a/chrome/browser/extensions/extension_prefs_unittest.cc
+++ b/chrome/browser/extensions/extension_prefs_unittest.cc
@@ -28,6 +28,8 @@
 #include "content/public/browser/notification_source.h"
 #include "content/public/test/mock_notification_observer.h"
 #include "extensions/browser/blocklist_extension_prefs.h"
+#include "extensions/browser/blocklist_state.h"
+#include "extensions/browser/disable_reason.h"
 #include "extensions/browser/extension_pref_value_map.h"
 #include "extensions/browser/extension_prefs.h"
 #include "extensions/browser/install_flag.h"
@@ -1125,6 +1127,13 @@
     // The pref is not migrated to the new pref yet.
     EXPECT_FALSE(prefs()->IsBlocklistedExtensionAcknowledged(extension_->id()));
 
+    prefs()->SetExtensionDisabled(
+        extension_->id(),
+        disable_reason::DEPRECATED_DISABLE_REMOTELY_FOR_MALWARE);
+    // The pref is not migrated to the new pref yet.
+    EXPECT_FALSE(blocklist_prefs::HasOmahaBlocklistState(
+        extension_->id(), BitMapBlocklistState::BLOCKLISTED_MALWARE, prefs()));
+
     prefs()->MigrateOldBlocklistPrefs();
   }
 
@@ -1144,6 +1153,13 @@
     // The old pref should be cleared.
     EXPECT_FALSE(is_blocklist_acknowledged_pref);
     EXPECT_TRUE(prefs()->IsBlocklistedExtensionAcknowledged(extension_->id()));
+
+    EXPECT_TRUE(blocklist_prefs::HasOmahaBlocklistState(
+        extension_->id(), BitMapBlocklistState::BLOCKLISTED_MALWARE, prefs()));
+    // The old pref should be cleared.
+    EXPECT_FALSE(prefs()->HasDisableReason(
+        extension_->id(),
+        disable_reason::DEPRECATED_DISABLE_REMOTELY_FOR_MALWARE));
   }
 
  private:
diff --git a/chrome/browser/extensions/extension_service.cc b/chrome/browser/extensions/extension_service.cc
index a2ea327..04d17c22 100644
--- a/chrome/browser/extensions/extension_service.cc
+++ b/chrome/browser/extensions/extension_service.cc
@@ -875,8 +875,9 @@
     return;
   }
 
-  if (extension_prefs_->HasDisableReason(
-          extension_id, disable_reason::DISABLE_REMOTELY_FOR_MALWARE)) {
+  if (blocklist_prefs::HasOmahaBlocklistState(
+          extension_id, BitMapBlocklistState::BLOCKLISTED_MALWARE,
+          extension_prefs_)) {
     // The extension is already disabled. No work needs to be done.
     return;
   }
@@ -888,8 +889,9 @@
   // Add the extension to the blocklisted extensions set.
   UpdateBlocklistedExtensions({extension_id},
                               registry_->blocklisted_extensions().GetIDs());
-  extension_prefs_->AddDisableReason(
-      extension_id, disable_reason::DISABLE_REMOTELY_FOR_MALWARE);
+  blocklist_prefs::AddOmahaBlocklistState(
+      extension_id, BitMapBlocklistState::BLOCKLISTED_MALWARE,
+      extension_prefs_);
   // Show an error for the newly blocklisted extension.
   error_controller_->ShowErrorIfNeeded();
 }
@@ -897,12 +899,15 @@
 void ExtensionService::MaybeEnableRemotelyDisabledExtension(
     const std::string& extension_id) {
   CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
-  int disable_reasons = extension_prefs_->GetDisableReasons(extension_id);
-  if ((disable_reasons & disable_reason::DISABLE_REMOTELY_FOR_MALWARE) == 0)
+  if (!blocklist_prefs::HasOmahaBlocklistState(
+          extension_id, BitMapBlocklistState::BLOCKLISTED_MALWARE,
+          extension_prefs_)) {
     return;
+  }
 
-  extension_prefs_->RemoveDisableReason(
-      extension_id, disable_reason::DISABLE_REMOTELY_FOR_MALWARE);
+  blocklist_prefs::RemoveOmahaBlocklistState(
+      extension_id, BitMapBlocklistState::BLOCKLISTED_MALWARE,
+      extension_prefs_);
 
   ExtensionIdSet unchanged = registry_->blocklisted_extensions().GetIDs();
   DCHECK(base::Contains(unchanged, extension_id));
@@ -2217,8 +2222,9 @@
   // to |unchanged| set.
   for (const auto& extension_id :
        registry_->blocklisted_extensions().GetIDs()) {
-    if (extension_prefs_->HasDisableReason(
-            extension_id, disable_reason::DISABLE_REMOTELY_FOR_MALWARE)) {
+    if (blocklist_prefs::HasOmahaBlocklistState(
+            extension_id, BitMapBlocklistState::BLOCKLISTED_MALWARE,
+            extension_prefs_)) {
       unchanged.insert(extension_id);
     }
   }
diff --git a/chrome/browser/extensions/extension_service_unittest.cc b/chrome/browser/extensions/extension_service_unittest.cc
index 5cbc294b..42160dc 100644
--- a/chrome/browser/extensions/extension_service_unittest.cc
+++ b/chrome/browser/extensions/extension_service_unittest.cc
@@ -3525,8 +3525,8 @@
 
   ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
   service()->PerformActionBasedOnOmahaAttributes(good0, attributes);
-  EXPECT_EQ(disable_reason::DISABLE_REMOTELY_FOR_MALWARE,
-            prefs->GetDisableReasons(good0));
+  EXPECT_TRUE(blocklist_prefs::HasOmahaBlocklistState(
+      good0, BitMapBlocklistState::BLOCKLISTED_MALWARE, prefs));
   EXPECT_TRUE(DoesIntegerPrefExist(good0, kPrefBlocklistState));
   EXPECT_FALSE(registry()->enabled_extensions().Contains(good0));
   EXPECT_TRUE(registry()->blocklisted_extensions().Contains(good0));
@@ -3535,9 +3535,9 @@
   test_blocklist.Clear(false);
   task_environment()->RunUntilIdle();
 
-  // If the extension has a DISABLE_REMOTELY_FOR_MALWARE disable reason,
-  // the extension should still not be enabled even if it's no on the
-  // SB blocklist. This disable reason needs to be removed prior to
+  // If the extension has a BLOCKLISTED_MALWARE state in the Omaha blocklist
+  // pref, the extension should still not be enabled even if it's not on the SB
+  // blocklist. This state needs to be removed prior to
   // unblocklisting/re-enabling.
   EXPECT_FALSE(registry()->enabled_extensions().Contains(good0));
   EXPECT_TRUE(registry()->blocklisted_extensions().Contains(good0));
@@ -4738,8 +4738,8 @@
 
   ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
   service()->PerformActionBasedOnOmahaAttributes(good_crx, attributes);
-  EXPECT_EQ(disable_reason::DISABLE_REMOTELY_FOR_MALWARE,
-            prefs->GetDisableReasons(good_crx));
+  EXPECT_TRUE(blocklist_prefs::HasOmahaBlocklistState(
+      good_crx, BitMapBlocklistState::BLOCKLISTED_MALWARE, prefs));
   EXPECT_TRUE(blocklist_prefs::IsExtensionBlocklisted(good_crx, prefs));
 
   attributes.SetKey("_malware", base::Value(false));
@@ -4757,19 +4757,19 @@
   InstallCRX(data_dir().AppendASCII("good.crx"), INSTALL_NEW);
   EXPECT_TRUE(registry()->enabled_extensions().GetByID(good_crx));
 
+  base::Value attributes(base::Value::Type::DICTIONARY);
+  attributes.SetKey("_malware", base::Value(true));
   ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
-  service()->DisableExtension(good_crx,
-                              disable_reason::DISABLE_REMOTELY_FOR_MALWARE |
-                                  disable_reason::DISABLE_USER_ACTION);
+  service()->DisableExtension(good_crx, disable_reason::DISABLE_USER_ACTION);
   EXPECT_TRUE(registry()->disabled_extensions().GetByID(good_crx));
-  service()->BlocklistExtensionForTest(good_crx);
+  service()->PerformActionBasedOnOmahaAttributes(good_crx, attributes);
   EXPECT_TRUE(blocklist_prefs::IsExtensionBlocklisted(good_crx, prefs));
 
-  base::Value empty_attr(base::Value::Type::DICTIONARY);
-  service()->PerformActionBasedOnOmahaAttributes(good_crx, empty_attr);
+  attributes.SetKey("_malware", base::Value(false));
+  service()->PerformActionBasedOnOmahaAttributes(good_crx, attributes);
   EXPECT_TRUE(registry()->disabled_extensions().GetByID(good_crx));
-  EXPECT_FALSE(prefs->GetDisableReasons(good_crx) &
-               disable_reason::DISABLE_REMOTELY_FOR_MALWARE);
+  EXPECT_FALSE(blocklist_prefs::HasOmahaBlocklistState(
+      good_crx, BitMapBlocklistState::BLOCKLISTED_MALWARE, prefs));
   EXPECT_FALSE(blocklist_prefs::IsExtensionBlocklisted(good_crx, prefs));
 }
 
@@ -4787,11 +4787,6 @@
   EXPECT_TRUE(blocklist_prefs::IsExtensionBlocklisted(good0, prefs));
   EXPECT_TRUE(blocklist_prefs::IsExtensionBlocklisted(good1, prefs));
 
-  // Test that a disable reason can be added to a blocklisted extension.
-  prefs->AddDisableReason(good0, disable_reason::DISABLE_REMOTELY_FOR_MALWARE);
-  EXPECT_TRUE(prefs->HasDisableReason(
-      good0, disable_reason::DISABLE_REMOTELY_FOR_MALWARE));
-
   // Test that a blocklisted extension can be disabled.
   service()->DisableExtension(good1, disable_reason::DISABLE_BLOCKED_BY_POLICY);
   EXPECT_TRUE(prefs->HasDisableReason(
diff --git a/chrome/browser/extensions/omaha_attributes_handler_unittest.cc b/chrome/browser/extensions/omaha_attributes_handler_unittest.cc
index 06e4c22..c30adcd 100644
--- a/chrome/browser/extensions/omaha_attributes_handler_unittest.cc
+++ b/chrome/browser/extensions/omaha_attributes_handler_unittest.cc
@@ -210,8 +210,9 @@
 
   ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
   EXPECT_TRUE(state_tester.ExpectBlocklisted(kTestExtensionId));
-  EXPECT_EQ(disable_reason::DISABLE_REMOTELY_FOR_MALWARE |
-                disable_reason::DISABLE_GREYLIST,
+  EXPECT_TRUE(blocklist_prefs::HasOmahaBlocklistState(
+      kTestExtensionId, BitMapBlocklistState::BLOCKLISTED_MALWARE, prefs));
+  EXPECT_EQ(disable_reason::DISABLE_GREYLIST,
             prefs->GetDisableReasons(kTestExtensionId));
 
   // Remove malware.
diff --git a/chrome/browser/extensions/preinstalled_apps_browsertest.cc b/chrome/browser/extensions/preinstalled_apps_browsertest.cc
index 313f0791..d3df4bb 100644
--- a/chrome/browser/extensions/preinstalled_apps_browsertest.cc
+++ b/chrome/browser/extensions/preinstalled_apps_browsertest.cc
@@ -22,6 +22,7 @@
 #include "chrome/browser/web_applications/preinstalled_web_app_utils.h"
 #include "chrome/browser/web_applications/test/fake_os_integration_manager.h"
 #include "chrome/browser/web_applications/test/fake_web_app_provider.h"
+#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
 #include "chrome/browser/web_applications/web_app.h"
 #include "chrome/browser/web_applications/web_app_constants.h"
 #include "chrome/browser/web_applications/web_app_helpers.h"
@@ -185,6 +186,8 @@
   // We override this to also wait for the PreinstalledWebAppManager.
   void WaitForSystemReady() override {
     PreinstalledAppsBrowserTest::WaitForSystemReady();
+    web_app::test::WaitUntilReady(
+        web_app::WebAppProvider::GetForTest(browser()->profile()));
 
     // For web app migration tests, we want to set up extension app shortcut
     // locations to test that they are preserved.
diff --git a/chrome/browser/extensions/updater/extension_updater.cc b/chrome/browser/extensions/updater/extension_updater.cc
index 55514b8..01737b3c 100644
--- a/chrome/browser/extensions/updater/extension_updater.cc
+++ b/chrome/browser/extensions/updater/extension_updater.cc
@@ -37,6 +37,7 @@
 #include "content/public/browser/notification_service.h"
 #include "content/public/browser/notification_source.h"
 #include "crypto/sha2.h"
+#include "extensions/browser/blocklist_extension_prefs.h"
 #include "extensions/browser/extension_file_task_runner.h"
 #include "extensions/browser/extension_prefs.h"
 #include "extensions/browser/extension_registry.h"
@@ -448,9 +449,11 @@
                     params.fetch_priority, &update_check_params);
     ExtensionSet remotely_disabled_extensions;
     for (auto extension : registry_->blocklisted_extensions()) {
-      if (extension_prefs_->HasDisableReason(
-              extension->id(), disable_reason::DISABLE_REMOTELY_FOR_MALWARE))
+      if (blocklist_prefs::HasOmahaBlocklistState(
+              extension->id(), BitMapBlocklistState::BLOCKLISTED_MALWARE,
+              extension_prefs_)) {
         remotely_disabled_extensions.Insert(extension);
+      }
     }
     AddToDownloader(&remotely_disabled_extensions, pending_ids, request_id,
                     params.fetch_priority, &update_check_params);
diff --git a/chrome/browser/extensions/updater/extension_updater_unittest.cc b/chrome/browser/extensions/updater/extension_updater_unittest.cc
index 96755be..fe91ec3 100644
--- a/chrome/browser/extensions/updater/extension_updater_unittest.cc
+++ b/chrome/browser/extensions/updater/extension_updater_unittest.cc
@@ -2682,8 +2682,9 @@
   blocklist_prefs::SetSafeBrowsingExtensionBlocklistState(
       remotely_blocklisted_id, BitMapBlocklistState::BLOCKLISTED_MALWARE,
       service.extension_prefs());
-  service.extension_prefs()->AddDisableReason(
-      remotely_blocklisted_id, disable_reason::DISABLE_REMOTELY_FOR_MALWARE);
+  blocklist_prefs::AddOmahaBlocklistState(
+      remotely_blocklisted_id, BitMapBlocklistState::BLOCKLISTED_MALWARE,
+      service.extension_prefs());
 
   // We expect that both enabled and remotely blocklisted extensions are
   // auto-updated.
diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc
index 6a17bf2..aa66710 100644
--- a/chrome/browser/flags/android/chrome_feature_list.cc
+++ b/chrome/browser/flags/android/chrome_feature_list.cc
@@ -199,7 +199,6 @@
     &kChromeSurveyNextAndroid,
     &kCommandLineOnNonRooted,
     &kConditionalTabStripAndroid,
-    &kContentSuggestionsScrollToLoad,
     &kContextMenuEnableLensShoppingAllowlist,
     &kContextMenuGoogleLensChip,
     &kContextMenuSearchWithGoogleLens,
@@ -501,9 +500,6 @@
 const base::Feature kCommandLineOnNonRooted{"CommandLineOnNonRooted",
                                             base::FEATURE_DISABLED_BY_DEFAULT};
 
-const base::Feature kContentSuggestionsScrollToLoad{
-    "ContentSuggestionsScrollToLoad", base::FEATURE_DISABLED_BY_DEFAULT};
-
 const base::Feature kContextMenuEnableLensShoppingAllowlist{
     "ContextMenuEnableLensShoppingAllowlist",
     base::FEATURE_DISABLED_BY_DEFAULT};
diff --git a/chrome/browser/flags/android/chrome_feature_list.h b/chrome/browser/flags/android/chrome_feature_list.h
index 96fafc2..9b30c2d 100644
--- a/chrome/browser/flags/android/chrome_feature_list.h
+++ b/chrome/browser/flags/android/chrome_feature_list.h
@@ -61,7 +61,6 @@
 extern const base::Feature kContextMenuShopWithGoogleLens;
 extern const base::Feature kContextMenuSearchAndShopWithGoogleLens;
 extern const base::Feature kContextMenuTranslateWithGoogleLens;
-extern const base::Feature kContentSuggestionsScrollToLoad;
 extern const base::Feature kContextualSearchDebug;
 extern const base::Feature kContextualSearchForceCaption;
 extern const base::Feature kContextualSearchLegacyHttpPolicy;
diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
index c44ba310..2bd063b7 100644
--- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
+++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
@@ -276,8 +276,6 @@
     public static final String COMMERCE_PRICE_TRACKING = "CommercePriceTracking";
     public static final String CONDITIONAL_TAB_STRIP_ANDROID = "ConditionalTabStripAndroid";
     public static final String CONTACTS_PICKER_SELECT_ALL = "ContactsPickerSelectAll";
-    public static final String CONTENT_SUGGESTIONS_SCROLL_TO_LOAD =
-            "ContentSuggestionsScrollToLoad";
     public static final String CONTEXT_MENU_ENABLE_LENS_SHOPPING_ALLOWLIST =
             "ContextMenuEnableLensShoppingAllowlist";
     public static final String CONTEXT_MENU_GOOGLE_LENS_CHIP = "ContextMenuGoogleLensChip";
diff --git a/chrome/browser/lacros/lacros_prefs.cc b/chrome/browser/lacros/lacros_prefs.cc
index e3b7e9f..61ca315 100644
--- a/chrome/browser/lacros/lacros_prefs.cc
+++ b/chrome/browser/lacros/lacros_prefs.cc
@@ -23,6 +23,11 @@
   // browser settings. It could reasonably move to a browser-specific
   // location with suitable #ifdefs.
   registry->RegisterBooleanPref(::prefs::kSettingsShowOSBanner, true);
+
+  // The following two prefs are used in imageWriterPrivate extension api
+  // implementation code, which are supported in Lacros.
+  registry->RegisterBooleanPref(::prefs::kExternalStorageDisabled, false);
+  registry->RegisterBooleanPref(::prefs::kExternalStorageReadOnly, false);
 }
 
 }  // namespace lacros_prefs
diff --git a/chrome/browser/media/router/discovery/dial/dial_service_unittest.cc b/chrome/browser/media/router/discovery/dial/dial_service_unittest.cc
index e889484..33af47b 100644
--- a/chrome/browser/media/router/discovery/dial/dial_service_unittest.cc
+++ b/chrome/browser/media/router/discovery/dial/dial_service_unittest.cc
@@ -17,7 +17,6 @@
 #include "net/base/ip_endpoint.h"
 #include "net/base/net_errors.h"
 #include "net/base/network_interfaces.h"
-#include "net/log/test_net_log.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -55,14 +54,13 @@
   DialServiceTest()
       : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP),
         mock_ip_(net::IPAddress::IPv4AllZeros()),
-        dial_service_(&test_net_log_) {
+        dial_service_(net::NetLog::Get()) {
     dial_service_.AddObserver(&mock_observer_);
     dial_socket_ = dial_service_.CreateDialSocket();
   }
 
  protected:
   content::BrowserTaskEnvironment task_environment_;
-  net::RecordingTestNetLog test_net_log_;
   net::IPAddress mock_ip_;
   DialServiceImpl dial_service_;
   std::unique_ptr<DialServiceImpl::DialSocket> dial_socket_;
diff --git a/chrome/browser/media/webrtc/webrtc_getdisplaymedia_browsertest.cc b/chrome/browser/media/webrtc/webrtc_getdisplaymedia_browsertest.cc
index 9588b01..8839177 100644
--- a/chrome/browser/media/webrtc/webrtc_getdisplaymedia_browsertest.cc
+++ b/chrome/browser/media/webrtc/webrtc_getdisplaymedia_browsertest.cc
@@ -5,6 +5,7 @@
 #include <string>
 
 #include "base/path_service.h"
+#include "base/strings/strcat.h"
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "build/build_config.h"
@@ -40,6 +41,8 @@
 static const char kMainHtmlFileName[] = "webrtc_getdisplaymedia_test.html";
 static const char kSameOriginRenamedTitle[] = "Renamed Same Origin Tab";
 
+enum class DisplaySurfaceType { kTab, kWindow, kScreen };
+
 enum class GetDisplayMediaVariant : int {
   kStandard = 0,
   kPreferCurrentTab = 1
@@ -61,6 +64,20 @@
 
 constexpr char kAppWindowTitle[] = "AppWindow Display Capture Test";
 
+std::string DisplaySurfaceTypeAsString(
+    DisplaySurfaceType display_surface_type) {
+  switch (display_surface_type) {
+    case DisplaySurfaceType::kTab:
+      return "browser";
+    case DisplaySurfaceType::kWindow:
+      return "window";
+    case DisplaySurfaceType::kScreen:
+      return "screen";
+  }
+  NOTREACHED();
+  return "error";
+}
+
 void RunGetDisplayMedia(content::WebContents* tab,
                         const std::string& constraints,
                         bool is_fake_ui,
@@ -594,3 +611,169 @@
       &result));
   EXPECT_EQ(result, "live");
 }
+
+class GetDisplayMediaVideoTrackBrowserTest
+    : public WebRtcTestBase,
+      public testing::WithParamInterface<
+          std::tuple<bool, bool, DisplaySurfaceType>> {
+ public:
+  GetDisplayMediaVideoTrackBrowserTest()
+      : conditional_focus_enabled_(std::get<0>(GetParam())),
+        region_capture_enabled_(std::get<1>(GetParam())),
+        display_surface_type_(std::get<2>(GetParam())) {}
+
+  ~GetDisplayMediaVideoTrackBrowserTest() override = default;
+
+  void SetUpInProcessBrowserTestFixture() override {
+    DetectErrorsInJavaScript();
+  }
+
+  void SetUpOnMainThread() override {
+    WebRtcTestBase::SetUpOnMainThread();
+
+    ASSERT_TRUE(embedded_test_server()->Start());
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+    // The picker itself shows previews which are unsupported in Lacros tests.
+    base::Value matchlist(base::Value::Type::LIST);
+    matchlist.Append("*");
+    browser()->profile()->GetPrefs()->Set(prefs::kTabCaptureAllowedByOrigins,
+                                          matchlist);
+#endif
+  }
+
+  // Unlike SetUp(), this is called from the test body. This allows skipping
+  // this test for (platform, test-case) combinations which are not supported.
+  void SetupTest() {
+    // Fire up the page.
+    tab_ = OpenTestPageInNewTab(kMainHtmlPage);
+
+    // Initiate the capture.
+    std::string result;
+    ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+        tab_->GetMainFrame(),
+        "runGetDisplayMedia({video: true, audio: true}, "
+        "\"top-level-document\");",
+        &result));
+    ASSERT_EQ(result, "capture-success");
+  }
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    WebRtcTestBase::SetUpCommandLine(command_line);
+
+    std::vector<std::string> enabled_blink_features;
+    if (conditional_focus_enabled_) {
+      enabled_blink_features.push_back("ConditionalFocus");
+    }
+    if (region_capture_enabled_) {
+      enabled_blink_features.push_back("RegionCapture");
+    }
+    if (!enabled_blink_features.empty()) {
+      command_line->AppendSwitchASCII(
+          switches::kEnableBlinkFeatures,
+          base::JoinString(enabled_blink_features, ","));
+    }
+
+    command_line->AppendSwitch(switches::kUseFakeUIForMediaStream);
+    command_line->AppendSwitchASCII(
+        switches::kUseFakeDeviceForMediaStream,
+        base::StrCat({"display-media-type=",
+                      DisplaySurfaceTypeAsString(display_surface_type_)}));
+  }
+
+  std::string GetVideoTrackType() {
+    std::string result;
+    EXPECT_TRUE(content::ExecuteScriptAndExtractString(
+        tab_->GetMainFrame(), "getVideoTrackType();", &result));
+    return result;
+  }
+
+  std::string GetVideoCloneTrackType() {
+    std::string result;
+    EXPECT_TRUE(content::ExecuteScriptAndExtractString(
+        tab_->GetMainFrame(), "getVideoCloneTrackType();", &result));
+    return result;
+  }
+
+  bool HasAudioTrack() {
+    std::string result;
+    EXPECT_TRUE(content::ExecuteScriptAndExtractString(
+        tab_->GetMainFrame(), "hasAudioTrack();", &result));
+    EXPECT_TRUE(result == "true" || result == "false");
+    return result == "true";
+  }
+
+  std::string GetAudioTrackType() {
+    std::string result;
+    EXPECT_TRUE(content::ExecuteScriptAndExtractString(
+        tab_->GetMainFrame(), "getAudioTrackType();", &result));
+    return result;
+  }
+
+  std::string ExpectedVideoTrackType() const {
+    switch (display_surface_type_) {
+      case DisplaySurfaceType::kTab:
+        return region_capture_enabled_
+                   ? "BrowserCaptureMediaStreamTrack"
+                   : conditional_focus_enabled_ ? "FocusableMediaStreamTrack"
+                                                : "MediaStreamTrack";
+      case DisplaySurfaceType::kWindow:
+        return conditional_focus_enabled_ || region_capture_enabled_
+                   ? "FocusableMediaStreamTrack"
+                   : "MediaStreamTrack";
+      case DisplaySurfaceType::kScreen:
+        return "MediaStreamTrack";
+    }
+    NOTREACHED();
+    return "Error";
+  }
+
+ protected:
+  const bool conditional_focus_enabled_;
+  const bool region_capture_enabled_;
+  const DisplaySurfaceType display_surface_type_;
+
+ private:
+  content::WebContents* tab_ = nullptr;
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    _,
+    GetDisplayMediaVideoTrackBrowserTest,
+    testing::Combine(/*conditional_focus_enabled=*/testing::Bool(),
+                     /*region_capture_enabled=*/testing::Bool(),
+                     /*display_surface_type=*/
+                     testing::Values(DisplaySurfaceType::kTab,
+                                     DisplaySurfaceType::kWindow,
+                                     DisplaySurfaceType::kScreen)),
+    [](const testing::TestParamInfo<
+        GetDisplayMediaVideoTrackBrowserTest::ParamType>& info) {
+      return base::StrCat(
+          {std::get<0>(info.param) ? "ConditionalFocus" : "",
+           std::get<1>(info.param) ? "RegionCapture" : "",
+           std::get<2>(info.param) == DisplaySurfaceType::kTab
+               ? "Tab"
+               : std::get<2>(info.param) == DisplaySurfaceType::kWindow
+                     ? "Window"
+                     : "Screen"});
+    });
+
+// Normally, each of these these would have its own test, but the number of
+// combinations and the setup time for browser-tests make this undesirable,
+// especially given the simplicity of each of these tests.
+// After both (a) Conditional Focus and (b) Region Capture ship, this can
+// simpplified to three non-parameterized tests (tab/window/screen).
+IN_PROC_BROWSER_TEST_P(GetDisplayMediaVideoTrackBrowserTest, RunCombinedTest) {
+  SetupTest();
+
+  // Test #1: The video track is of the expected type.
+  EXPECT_EQ(GetVideoTrackType(), ExpectedVideoTrackType());
+
+  // Test #2: Video clones are of the same type as the original.
+  EXPECT_EQ(GetVideoTrackType(), GetVideoCloneTrackType());
+
+  // Test #3: Audio tracks are all simply MediaStreamTrack.
+  if (HasAudioTrack()) {
+    EXPECT_EQ(GetAudioTrackType(), "MediaStreamTrack");
+  }
+}
diff --git a/chrome/browser/metrics/extensions_metrics_provider.cc b/chrome/browser/metrics/extensions_metrics_provider.cc
index 8e0fcaf..e336726 100644
--- a/chrome/browser/metrics/extensions_metrics_provider.cc
+++ b/chrome/browser/metrics/extensions_metrics_provider.cc
@@ -254,8 +254,6 @@
        ExtensionInstallProto::CUSTODIAN_APPROVAL_REQUIRED},
       {extensions::disable_reason::DISABLE_BLOCKED_BY_POLICY,
        ExtensionInstallProto::BLOCKED_BY_POLICY},
-      {extensions::disable_reason::DISABLE_REMOTELY_FOR_MALWARE,
-       ExtensionInstallProto::DISABLE_REMOTELY_FOR_MALWARE},
       {extensions::disable_reason::DISABLE_REINSTALL,
        ExtensionInstallProto::REINSTALL},
       {extensions::disable_reason::DISABLE_NOT_ALLOWLISTED,
diff --git a/chrome/browser/metrics/extensions_metrics_provider_unittest.cc b/chrome/browser/metrics/extensions_metrics_provider_unittest.cc
index 3da5a04..e8a2989 100644
--- a/chrome/browser/metrics/extensions_metrics_provider_unittest.cc
+++ b/chrome/browser/metrics/extensions_metrics_provider_unittest.cc
@@ -319,17 +319,6 @@
       EXPECT_EQ(ExtensionInstallProto::CORRUPTED,
                 install.disable_reasons().Get(1));
     }
-    // Adding additional disable reasons should result in all reasons being
-    // reported.
-    prefs()->AddDisableReason(
-        extension->id(),
-        extensions::disable_reason::DISABLE_REMOTELY_FOR_MALWARE);
-    {
-      ExtensionInstallProto install = ConstructProto(*extension);
-      ASSERT_EQ(3, install.disable_reasons_size());
-      EXPECT_EQ(ExtensionInstallProto::DISABLE_REMOTELY_FOR_MALWARE,
-                install.disable_reasons().Get(2));
-    }
   }
 
   {
diff --git a/chrome/browser/optimization_guide/prediction/prediction_model_download_manager.cc b/chrome/browser/optimization_guide/prediction/prediction_model_download_manager.cc
index aec30c4..f893e4e 100644
--- a/chrome/browser/optimization_guide/prediction/prediction_model_download_manager.cc
+++ b/chrome/browser/optimization_guide/prediction/prediction_model_download_manager.cc
@@ -403,7 +403,7 @@
 
     // ReplaceFile failed, log the error code and attempt to utilize base::Move
     // instead as the file could be on a different storage partition.
-    UMA_HISTOGRAM_ENUMERATION(
+    base::UmaHistogramExactLinear(
         "OptimizationGuide.PredictionModelDownloadManager.ReplaceFileError." +
             GetStringNameForOptimizationTarget(
                 model_info.optimization_target()),
diff --git a/chrome/browser/preferences/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceKeys.java b/chrome/browser/preferences/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceKeys.java
index 844735c..581f62d 100644
--- a/chrome/browser/preferences/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceKeys.java
+++ b/chrome/browser/preferences/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceKeys.java
@@ -862,9 +862,11 @@
     public static final String SIGNIN_ACCOUNT_RENAME_EVENT_INDEX =
             "prefs_sync_account_rename_event_index";
 
-    /** SigninPromo Show Count preference. */
-    public static final KeyPrefix SIGNIN_PROMO_SHOW_COUNT =
-            new KeyPrefix("Chrome.SigninPromo.ShowCount.*");
+    /** SyncPromo Show Count preference. */
+    public static final KeyPrefix SYNC_PROMO_SHOW_COUNT =
+            new KeyPrefix("Chrome.SyncPromo.ShowCount.*");
+    /** SyncPromo total shown count preference across all access points. */
+    public static final String SYNC_PROMO_TOTAL_SHOW_COUNT = "Chrome.SyncPromo.TotalShowCount";
 
     /**
      * Generic signin and sync promo preferences.
@@ -1127,9 +1129,10 @@
                 SHARING_LAST_SHARED_COMPONENT_NAME,
                 SHOW_START_SEGMENTATION_RESULT,
                 SIGNIN_PROMO_IMPRESSIONS_COUNT_NTP,
-                SIGNIN_PROMO_SHOW_COUNT.pattern(),
+                SYNC_PROMO_SHOW_COUNT.pattern(),
                 SIGNIN_PROMO_NTP_FIRST_SHOWN_TIME,
                 SIGNIN_PROMO_NTP_LAST_SHOWN_TIME,
+                SYNC_PROMO_TOTAL_SHOW_COUNT,
                 START_NEXT_SHOW_ON_STARTUP_DECISION_MS,
                 START_SHOW_ON_STARTUP,
                 TAP_FEED_CARDS_COUNT,
diff --git a/chrome/browser/resources/cryptotoken/enroller.js b/chrome/browser/resources/cryptotoken/enroller.js
index 2cd41ac..ed75e75 100644
--- a/chrome/browser/resources/cryptotoken/enroller.js
+++ b/chrome/browser/resources/cryptotoken/enroller.js
@@ -417,6 +417,7 @@
             {
               'appId': appId,
               'tabId': messageSender.tab.id,
+              'frameId': 0,  // ignored
               'origin': sender.origin,
             },
             resolve);
@@ -897,7 +898,12 @@
 Enroller.prototype.checkU2fApiPermission_ = function(
     appId, challenge, request, attestationMode) {
   chrome.cryptotokenPrivate.canMakeU2fApiRequest(
-      {tabId: this.sender_.tabId, origin: this.sender_.origin, appId: appId},
+      {
+        tabId: this.sender_.tabId,
+        frameId: this.sender_.frameId,
+        origin: this.sender_.origin,
+        appId: appId
+      },
       (result) => {
         if (!result) {
           this.notifyError_({
diff --git a/chrome/browser/resources/cryptotoken/signer.js b/chrome/browser/resources/cryptotoken/signer.js
index 692a8aac..5921de98 100644
--- a/chrome/browser/resources/cryptotoken/signer.js
+++ b/chrome/browser/resources/cryptotoken/signer.js
@@ -510,7 +510,12 @@
     this.doSignWebAuthnContinue_(decodedChallenge, credentialList, appid);
   } else {
     chrome.cryptotokenPrivate.canMakeU2fApiRequest(
-        {tabId: this.sender_.tabId, origin: this.sender_.origin, appId: appid},
+        {
+          tabId: this.sender_.tabId,
+          frameId: this.sender_.frameId,
+          origin: this.sender_.origin,
+          appId: appid
+        },
         (result) => {
           if (!result) {
             this.notifyError_({
diff --git a/chrome/browser/resources/settings/chromeos/os_apps_page/app_notifications_page/app_notification_row.html b/chrome/browser/resources/settings/chromeos/os_apps_page/app_notifications_page/app_notification_row.html
index c5d9f2e..4be458a 100644
--- a/chrome/browser/resources/settings/chromeos/os_apps_page/app_notifications_page/app_notification_row.html
+++ b/chrome/browser/resources/settings/chromeos/os_apps_page/app_notifications_page/app_notification_row.html
@@ -16,7 +16,7 @@
   }
 
   #appToggle {
-    padding: 0 20px 0 0;
+    margin-inline-end: 20px;
   }
 
   #appIcon {
diff --git a/chrome/browser/safe_browsing/chrome_password_protection_service.cc b/chrome/browser/safe_browsing/chrome_password_protection_service.cc
index 5e67260..89880c8 100644
--- a/chrome/browser/safe_browsing/chrome_password_protection_service.cc
+++ b/chrome/browser/safe_browsing/chrome_password_protection_service.cc
@@ -57,6 +57,7 @@
 #include "components/safe_browsing/content/browser/ui_manager.h"
 #include "components/safe_browsing/content/browser/web_ui/safe_browsing_ui.h"
 #include "components/safe_browsing/core/browser/db/database_manager.h"
+#include "components/safe_browsing/core/browser/realtime/policy_engine.h"
 #include "components/safe_browsing/core/browser/safe_browsing_metrics_collector.h"
 #include "components/safe_browsing/core/browser/sync/safe_browsing_primary_account_token_fetcher.h"
 #include "components/safe_browsing/core/browser/verdict_cache_manager.h"
@@ -1491,6 +1492,13 @@
   }
   ChromeUserPopulation::PageLoadToken token =
       cache_manager_->GetPageLoadToken(main_frame_url);
+  if (RealTimePolicyEngine::CanPerformFullURLLookup(
+          profile_->GetPrefs(), profile_->IsOffTheRecord(),
+          g_browser_process->variations_service())) {
+    base::UmaHistogramBoolean(
+        "SafeBrowsing.PageLoadToken.PasswordProtectionHasToken",
+        token.has_token_value());
+  }
   // It's possible that the token is not found because real time URL check is
   // not performed for this navigation. Create a new page load token in this
   // case.
diff --git a/chrome/browser/signin/ui/android/java/src/org/chromium/chrome/browser/signin/ui/SigninPromoController.java b/chrome/browser/signin/ui/android/java/src/org/chromium/chrome/browser/signin/ui/SigninPromoController.java
index da8d485..e4e7275 100644
--- a/chrome/browser/signin/ui/android/java/src/org/chromium/chrome/browser/signin/ui/SigninPromoController.java
+++ b/chrome/browser/signin/ui/android/java/src/org/chromium/chrome/browser/signin/ui/SigninPromoController.java
@@ -208,13 +208,11 @@
     public static String getPromoShowCountPreferenceName(@AccessPoint int accessPoint) {
         switch (accessPoint) {
             case SigninAccessPoint.BOOKMARK_MANAGER:
-                return ChromePreferenceKeys.SIGNIN_PROMO_SHOW_COUNT.createKey(BOOKMARKS);
+                return ChromePreferenceKeys.SYNC_PROMO_SHOW_COUNT.createKey(BOOKMARKS);
             case SigninAccessPoint.NTP_CONTENT_SUGGESTIONS:
-                return ChromePreferenceKeys.SIGNIN_PROMO_SHOW_COUNT.createKey(NTP);
-            case SigninAccessPoint.RECENT_TABS:
-                return ChromePreferenceKeys.SIGNIN_PROMO_SHOW_COUNT.createKey(RECENT_TABS);
+                return ChromePreferenceKeys.SYNC_PROMO_SHOW_COUNT.createKey(NTP);
             case SigninAccessPoint.SETTINGS:
-                return ChromePreferenceKeys.SIGNIN_PROMO_SHOW_COUNT.createKey(SETTINGS);
+                return ChromePreferenceKeys.SYNC_PROMO_SHOW_COUNT.createKey(SETTINGS);
             default:
                 throw new IllegalArgumentException(
                         "Unexpected value for access point: " + accessPoint);
@@ -426,8 +424,12 @@
 
     /** Increases promo show count by one. */
     public void increasePromoShowCount() {
+        if (mAccessPoint != SigninAccessPoint.RECENT_TABS) {
+            SharedPreferencesManager.getInstance().incrementInt(
+                    getPromoShowCountPreferenceName(mAccessPoint));
+        }
         SharedPreferencesManager.getInstance().incrementInt(
-                getPromoShowCountPreferenceName(mAccessPoint));
+                ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT);
 
         if (mAccessPoint == SigninAccessPoint.NTP_CONTENT_SUGGESTIONS) {
             final long currentTime = System.currentTimeMillis();
diff --git a/chrome/browser/ui/android/autofill/autofill_progress_dialog_view_android.cc b/chrome/browser/ui/android/autofill/autofill_progress_dialog_view_android.cc
index d7d48b3..4d3dcb0 100644
--- a/chrome/browser/ui/android/autofill/autofill_progress_dialog_view_android.cc
+++ b/chrome/browser/ui/android/autofill/autofill_progress_dialog_view_android.cc
@@ -39,7 +39,13 @@
   return dialog_view;
 }
 
-void AutofillProgressDialogViewAndroid::Dismiss() {
+void AutofillProgressDialogViewAndroid::Dismiss(
+    bool show_confirmation_before_closing) {
+  if (show_confirmation_before_closing) {
+    ShowConfirmation();
+    return;
+  }
+
   JNIEnv* env = base::android::AttachCurrentThread();
   if (!java_object_.is_null()) {
     Java_AutofillProgressDialogBridge_dismiss(env, java_object_);
diff --git a/chrome/browser/ui/android/autofill/autofill_progress_dialog_view_android.h b/chrome/browser/ui/android/autofill/autofill_progress_dialog_view_android.h
index f911cf39..52ec4b7 100644
--- a/chrome/browser/ui/android/autofill/autofill_progress_dialog_view_android.h
+++ b/chrome/browser/ui/android/autofill/autofill_progress_dialog_view_android.h
@@ -24,7 +24,7 @@
   ~AutofillProgressDialogViewAndroid() override;
 
   // AutofillProgressDialogView.
-  void Dismiss() override;
+  void Dismiss(bool show_confirmation_before_closing) override;
 
   // Called by the Java code when the progress dialog is dismissed.
   void OnDismissed(JNIEnv* env);
@@ -33,7 +33,7 @@
   void ShowDialog();
 
   // Show the confirmation icon and text.
-  void ShowConfirmation() override;
+  void ShowConfirmation();
 
  private:
   AutofillProgressDialogController* controller_;
diff --git a/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/AutofillProgressDialogBridge.java b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/AutofillProgressDialogBridge.java
index a33f08b..0c67c32 100644
--- a/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/AutofillProgressDialogBridge.java
+++ b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/AutofillProgressDialogBridge.java
@@ -107,6 +107,8 @@
                     .setVisibility(View.VISIBLE);
             ((TextView) mProgressDialogContentView.findViewById(R.id.message))
                     .setText(confirmationMessage);
+            // TODO(crbug.com/1243475): Dismiss the Java View after some delay if confirmation has
+            // been shown.
         }
     }
 
diff --git a/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/WebContentsDarkModeController.java b/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/WebContentsDarkModeController.java
index 0aca9035..23de5b3 100644
--- a/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/WebContentsDarkModeController.java
+++ b/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/WebContentsDarkModeController.java
@@ -206,7 +206,7 @@
      */
     private static void recordAutoDarkSettingsChangeSource(
             @AutoDarkSettingsChangeSource int source, boolean enabled) {
-        String histogram = "Android.DarkTheme.AutoDark.SettingsChangeSource."
+        String histogram = "Android.DarkTheme.AutoDarkMode.SettingsChangeSource."
                 + (enabled ? "Enabled" : "Disabled");
         RecordHistogram.recordEnumeratedHistogram(
                 histogram, source, AutoDarkSettingsChangeSource.NUM_ENTRIES);
diff --git a/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/WebContentsDarkModeControllerUnitTest.java b/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/WebContentsDarkModeControllerUnitTest.java
index 753600e..81a5436 100644
--- a/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/WebContentsDarkModeControllerUnitTest.java
+++ b/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/WebContentsDarkModeControllerUnitTest.java
@@ -286,7 +286,7 @@
 
     private void assertAutoDarkModeChangeSourceRecorded(
             @AutoDarkSettingsChangeSource int source, boolean enabled, int expectedCounts) {
-        String histogramName = "Android.DarkTheme.AutoDark.SettingsChangeSource."
+        String histogramName = "Android.DarkTheme.AutoDarkMode.SettingsChangeSource."
                 + (enabled ? "Enabled" : "Disabled");
         int actualCount = RecordHistogram.getHistogramValueCountForTesting(histogramName, source);
         Assert.assertEquals("Histogram <" + histogramName + "> for sample <" + source
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
index c42d3a9f..c09efe1 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -2279,7 +2279,7 @@
         Preview page <ph name="BEGIN_NEW">&lt;new&gt;</ph>New<ph name="END_NEW">&lt;/new&gt;</ph>
       </message>
       <message name="IDS_CONTEXTMENU_READ_LATER" desc="Context sensitive menu item for marking a link to be read later. We're also labeling it *New* to draw attention to it when first released. [CHAR_LIMIT=30]">
-        Add to Reading List <ph name="BEGIN_NEW">&lt;new&gt;</ph>New<ph name="END_NEW">&lt;/new&gt;</ph>
+        Add to reading list <ph name="BEGIN_NEW">&lt;new&gt;</ph>New<ph name="END_NEW">&lt;/new&gt;</ph>
       </message>
       <message name="IDS_CONTEXTMENU_PERFORMANCE_INFO_FAST" desc="This string is shown in the context sensitive menu for links and is shown underneath the link URL. It indicates to the user that the link's target page is fast-loading and responsive. [CHAR_LIMIT=30]">
         Fast page
@@ -3225,14 +3225,14 @@
       <message name="IDS_MENU_EDIT_BOOKMARK" desc="Menu item for editing the content of the bookmark for the current page. [CHAR_LIMIT=27]">
         Edit Bookmark
       </message>
-      <message name="IDS_MENU_ADD_TO_READING_LIST" desc="Menu item for adding the current page to the 'Reading List' to read later. [CHAR_LIMIT=27]">
-        Add to Reading List
+      <message name="IDS_MENU_ADD_TO_READING_LIST" desc="Menu item for adding the current page to the reading list. [CHAR_LIMIT=27]">
+        Add to reading list
       </message>
-      <message name="IDS_MENU_DELETE_FROM_READING_LIST" desc="Menu item for removing the current page from the 'Reading List'. [CHAR_LIMIT=27]">
-        Delete from Reading List
+      <message name="IDS_MENU_DELETE_FROM_READING_LIST" desc="Menu item for removing the current page from the reading list. [CHAR_LIMIT=27]">
+        Delete from reading list
       </message>
-      <message name="IDS_MENU_EDIT_READING_LIST" desc="Menu item for opening and editing the 'Reading List'. Shown only when the current page is already on the Reading List. [CHAR_LIMIT=27]">
-        Edit Reading List
+      <message name="IDS_MENU_EDIT_READING_LIST" desc="Menu item for opening and editing the reading list. Shown only when the current page is already on the reading list. [CHAR_LIMIT=27]">
+        Edit reading list
       </message>
       <message name="IDS_MENU_FIND_IN_PAGE" desc="Menu item allowing users to find text within the current page. [CHAR_LIMIT=27]">
         Find in page
@@ -3455,7 +3455,7 @@
         Save this page for later and get a reminder
       </message>
       <message name="IDS_READING_LIST_SAVE_PAGES_FOR_LATER" desc="The text on the reading list in product help bubble to introduce the feature to the user.">
-        Add pages to your Reading List to get a reminder
+        Add pages to your reading list for later
       </message>
       <message name="IDS_READING_LIST_FIND_IN_BOOKMARKS" desc="The text to inform the user to find the reading list in bookmarks UI.">
         Find your reading list in Bookmarks
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTEXTMENU_READ_LATER.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTEXTMENU_READ_LATER.png.sha1
index cb17f7aa..eca993fa 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTEXTMENU_READ_LATER.png.sha1
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTEXTMENU_READ_LATER.png.sha1
@@ -1 +1 @@
-e8b9ef1faf769b5ac2aff34b9d2de2e3ad67216b
\ No newline at end of file
+f37cc0fb8a8a4955e1cedcc2f2a403040cf93c4e
\ No newline at end of file
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_MENU_ADD_TO_READING_LIST.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_MENU_ADD_TO_READING_LIST.png.sha1
index 7e2996e..d335978 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_MENU_ADD_TO_READING_LIST.png.sha1
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_MENU_ADD_TO_READING_LIST.png.sha1
@@ -1 +1 @@
-ae82e7ea79689aef45d76de26fa8baf61b961b3b
\ No newline at end of file
+f957c8672dd7f7bd3d133cd8ea74458b3c269e93
\ No newline at end of file
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_MENU_DELETE_FROM_READING_LIST.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_MENU_DELETE_FROM_READING_LIST.png.sha1
index adb7c28b..31cf388 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_MENU_DELETE_FROM_READING_LIST.png.sha1
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_MENU_DELETE_FROM_READING_LIST.png.sha1
@@ -1 +1 @@
-b080c9f217e7dff4dce75ca962b404efe215572a
\ No newline at end of file
+045ee313c247d75dba791334a0983198d6631e49
\ No newline at end of file
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_MENU_EDIT_READING_LIST.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_MENU_EDIT_READING_LIST.png.sha1
index e3eba9e..a51e205 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_MENU_EDIT_READING_LIST.png.sha1
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_MENU_EDIT_READING_LIST.png.sha1
@@ -1 +1 @@
-a843887c2997190f3c846699e12ed973ea88a481
\ No newline at end of file
+f74ab9e5c668d66c8bd760f0dda525fbbba20c87
\ No newline at end of file
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_READING_LIST_SAVE_PAGES_FOR_LATER.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_READING_LIST_SAVE_PAGES_FOR_LATER.png.sha1
index 202456a..6edcd955 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_READING_LIST_SAVE_PAGES_FOR_LATER.png.sha1
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_READING_LIST_SAVE_PAGES_FOR_LATER.png.sha1
@@ -1 +1 @@
-89c19bdab51f56cc41109a57ca8c2227ceea5f5a
\ No newline at end of file
+35263b1e4ddad400502674b738a2d6cc423ced1f
\ No newline at end of file
diff --git a/chrome/browser/ui/app_list/app_list_sort_browsertest.cc b/chrome/browser/ui/app_list/app_list_sort_browsertest.cc
index 4dde6130..04dc28a0 100644
--- a/chrome/browser/ui/app_list/app_list_sort_browsertest.cc
+++ b/chrome/browser/ui/app_list/app_list_sort_browsertest.cc
@@ -7,6 +7,7 @@
 #include "ash/public/cpp/test/app_list_test_api.h"
 #include "ash/public/cpp/test/shell_test_api.h"
 #include "ash/shell.h"
+#include "base/feature_list.h"
 #include "chrome/browser/extensions/extension_browsertest.h"
 #include "chrome/browser/ui/app_list/app_list_client_impl.h"
 #include "chrome/browser/ui/app_list/app_list_model_updater.h"
@@ -47,8 +48,15 @@
     ash::AcceleratorController::Get()->PerformActionIfEnabled(
         ash::TOGGLE_APP_LIST_FULLSCREEN, {});
 
-    // Assume that there are two default apps.
-    ASSERT_EQ(2, app_list_test_api_.GetTopListItemCount());
+    const int default_app_count = app_list_test_api_.GetTopListItemCount();
+
+    if (base::FeatureList::IsEnabled(chromeos::features::kLacrosSupport)) {
+      // Assume that there are three default apps, one being the Lacros browser.
+      ASSERT_EQ(3, app_list_test_api_.GetTopListItemCount());
+    } else {
+      // Assume that there are two default apps.
+      ASSERT_EQ(2, app_list_test_api_.GetTopListItemCount());
+    }
 
     app1_id_ = LoadExtension(test_data_dir_.AppendASCII("app1"))->id();
     ASSERT_FALSE(app1_id_.empty());
@@ -58,7 +66,7 @@
     // app in this test.
     app3_id_ = LoadExtension(test_data_dir_.AppendASCII("app4"))->id();
     ASSERT_FALSE(app3_id_.empty());
-    EXPECT_EQ(5, app_list_test_api_.GetTopListItemCount());
+    EXPECT_EQ(default_app_count + 3, app_list_test_api_.GetTopListItemCount());
 
     event_generator_ = std::make_unique<ui::test::EventGenerator>(
         ash::Shell::GetPrimaryRootWindow());
diff --git a/chrome/browser/ui/app_list/search/arc/recommend_apps_fetcher_impl.cc b/chrome/browser/ui/app_list/search/arc/recommend_apps_fetcher_impl.cc
index 9d96433..ee085945 100644
--- a/chrome/browser/ui/app_list/search/arc/recommend_apps_fetcher_impl.cc
+++ b/chrome/browser/ui/app_list/search/arc/recommend_apps_fetcher_impl.cc
@@ -153,12 +153,10 @@
   // TODO(thanhdng): Add a UMA histogram here recording the time difference.
 
   std::unique_ptr<network::SimpleURLLoader> loader(std::move(app_list_loader_));
-  int response_code = 0;
   if (!loader->ResponseInfo() || !loader->ResponseInfo()->headers) {
     delegate_->OnLoadError();
     return;
   }
-  response_code = loader->ResponseInfo()->headers->response_code();
   // TODO(thanhndng): Add a UMA histogram here recording the response code.
 
   // If the recommended app list could not be downloaded, show an error message
diff --git a/chrome/browser/ui/app_list/search/chrome_search_result.cc b/chrome/browser/ui/app_list/search/chrome_search_result.cc
index 78e4ceb..fee5f00 100644
--- a/chrome/browser/ui/app_list/search/chrome_search_result.cc
+++ b/chrome/browser/ui/app_list/search/chrome_search_result.cc
@@ -72,6 +72,11 @@
   SetSearchResultMetadata();
 }
 
+void ChromeSearchResult::SetBestMatch(bool best_match) {
+  metadata_->best_match = best_match;
+  SetSearchResultMetadata();
+}
+
 void ChromeSearchResult::SetDisplayType(DisplayType display_type) {
   metadata_->display_type = display_type;
   SetSearchResultMetadata();
diff --git a/chrome/browser/ui/app_list/search/chrome_search_result.h b/chrome/browser/ui/app_list/search/chrome_search_result.h
index 0d6c1c42..d9a519b 100644
--- a/chrome/browser/ui/app_list/search/chrome_search_result.h
+++ b/chrome/browser/ui/app_list/search/chrome_search_result.h
@@ -66,6 +66,7 @@
   }
   const std::string& id() const { return metadata_->id; }
   Category category() const { return metadata_->category; }
+  bool best_match() const { return metadata_->best_match; }
   DisplayType display_type() const { return metadata_->display_type; }
   ash::AppListSearchResultType result_type() const {
     return metadata_->result_type;
@@ -100,6 +101,7 @@
   void SetRating(float rating);
   void SetFormattedPrice(const std::u16string& formatted_price);
   void SetCategory(Category category);
+  void SetBestMatch(bool best_match);
   void SetDisplayType(DisplayType display_type);
   void SetResultType(ResultType result_type);
   void SetMetricsType(MetricsType metrics_type);
diff --git a/chrome/browser/ui/app_list/search/ranking/category_item_ranker.cc b/chrome/browser/ui/app_list/search/ranking/category_item_ranker.cc
index ed42957..206c830 100644
--- a/chrome/browser/ui/app_list/search/ranking/category_item_ranker.cc
+++ b/chrome/browser/ui/app_list/search/ranking/category_item_ranker.cc
@@ -22,7 +22,9 @@
   category_scores_.clear();
 }
 
-void CategoryItemRanker::Rank(ResultsMap& results, ProviderType provider) {
+void CategoryItemRanker::Rank(ResultsMap& results,
+                              CategoriesMap& categories,
+                              ProviderType provider) {
   UpdateCategoryScore(results, provider);
   RescoreResults(results);
 }
diff --git a/chrome/browser/ui/app_list/search/ranking/category_item_ranker.h b/chrome/browser/ui/app_list/search/ranking/category_item_ranker.h
index cce9f50..19f2e1a 100644
--- a/chrome/browser/ui/app_list/search/ranking/category_item_ranker.h
+++ b/chrome/browser/ui/app_list/search/ranking/category_item_ranker.h
@@ -24,7 +24,9 @@
 
   // Ranker:
   void Start(const std::u16string& query) override;
-  void Rank(ResultsMap& results, ProviderType provider) override;
+  void Rank(ResultsMap& results,
+            CategoriesMap& categories,
+            ProviderType provider) override;
 
  private:
   // Updates the score of |provider|'s category in |category_scores_|.
diff --git a/chrome/browser/ui/app_list/search/ranking/category_usage_ranker.cc b/chrome/browser/ui/app_list/search/ranking/category_usage_ranker.cc
index 41a4637..9cf964c 100644
--- a/chrome/browser/ui/app_list/search/ranking/category_usage_ranker.cc
+++ b/chrome/browser/ui/app_list/search/ranking/category_usage_ranker.cc
@@ -50,17 +50,18 @@
   // |category_ranker_| has a small recency component to its scores, so the
   // categories will appear in reverse order to the Record calls. This must
   // record every category except unknown and best match.
-  category_ranker_->Record(CategoryToString(Category::kAssistant));
+  category_ranker_->Record(CategoryToString(Category::kSearchAndAssistant));
   category_ranker_->Record(CategoryToString(Category::kPlayStore));
-  category_ranker_->Record(CategoryToString(Category::kWeb));
-  category_ranker_->Record(CategoryToString(Category::kFiles));
-  category_ranker_->Record(CategoryToString(Category::kHelp));
   category_ranker_->Record(CategoryToString(Category::kSettings));
-  category_ranker_->Record(CategoryToString(Category::kApp));
+  category_ranker_->Record(CategoryToString(Category::kFiles));
+  category_ranker_->Record(CategoryToString(Category::kWeb));
+  category_ranker_->Record(CategoryToString(Category::kAppShortcuts));
+  category_ranker_->Record(CategoryToString(Category::kApps));
+  category_ranker_->Record(CategoryToString(Category::kHelp));
 
-  // Check we've recorded every category except unknown and best match.
+  // Check we've recorded every category except unknown.
   DCHECK_EQ(category_ranker_->Rank().size(),
-            static_cast<size_t>(Category::kMaxValue) - 1);
+            static_cast<size_t>(Category::kMaxValue));
 }
 
 CategoryUsageRanker::~CategoryUsageRanker() {}
@@ -93,7 +94,9 @@
   }
 }
 
-void CategoryUsageRanker::Rank(ResultsMap& results, ProviderType provider) {
+void CategoryUsageRanker::Rank(ResultsMap& results,
+                               CategoriesMap& categories,
+                               ProviderType provider) {
   // Update each result's score to be:
   //  kCategoryScoreFactor*(category rank) + (result relevance)
   const auto it = results.find(provider);
diff --git a/chrome/browser/ui/app_list/search/ranking/category_usage_ranker.h b/chrome/browser/ui/app_list/search/ranking/category_usage_ranker.h
index 5c0ee63..3941b0f 100644
--- a/chrome/browser/ui/app_list/search/ranking/category_usage_ranker.h
+++ b/chrome/browser/ui/app_list/search/ranking/category_usage_ranker.h
@@ -25,7 +25,9 @@
 
   // Ranker:
   void Start(const std::u16string& query) override;
-  void Rank(ResultsMap& results, ProviderType provider) override;
+  void Rank(ResultsMap& results,
+            CategoriesMap& categories,
+            ProviderType provider) override;
   void Train(const LaunchData& launch) override;
 
  private:
diff --git a/chrome/browser/ui/app_list/search/ranking/filtering_ranker.cc b/chrome/browser/ui/app_list/search/ranking/filtering_ranker.cc
index fcb1762..d13ce6e 100644
--- a/chrome/browser/ui/app_list/search/ranking/filtering_ranker.cc
+++ b/chrome/browser/ui/app_list/search/ranking/filtering_ranker.cc
@@ -24,7 +24,9 @@
   last_query_ = query;
 }
 
-void FilteringRanker::Rank(ResultsMap& results, ProviderType provider) {
+void FilteringRanker::Rank(ResultsMap& results,
+                           CategoriesMap& categories,
+                           ProviderType provider) {
   // Don't perform any filtering on zero-state.
   if (last_query_.empty())
     return;
diff --git a/chrome/browser/ui/app_list/search/ranking/filtering_ranker.h b/chrome/browser/ui/app_list/search/ranking/filtering_ranker.h
index 7018187..a2641d6 100644
--- a/chrome/browser/ui/app_list/search/ranking/filtering_ranker.h
+++ b/chrome/browser/ui/app_list/search/ranking/filtering_ranker.h
@@ -22,7 +22,9 @@
 
   // Ranker:
   void Start(const std::u16string& query) override;
-  void Rank(ResultsMap& results, ProviderType provider) override;
+  void Rank(ResultsMap& results,
+            CategoriesMap& categories,
+            ProviderType provider) override;
 
  private:
   std::u16string last_query_;
diff --git a/chrome/browser/ui/app_list/search/ranking/ranker.h b/chrome/browser/ui/app_list/search/ranking/ranker.h
index cf2df364..8714349 100644
--- a/chrome/browser/ui/app_list/search/ranking/ranker.h
+++ b/chrome/browser/ui/app_list/search/ranking/ranker.h
@@ -39,7 +39,9 @@
   //
   // The goal of a ranker should be to update scores in the Scoring structs
   // within |results|. Generally, one ranker should map to one score member.
-  virtual void Rank(ResultsMap& results, ProviderType provider) {}
+  virtual void Rank(ResultsMap& results,
+                    CategoriesMap& categories,
+                    ProviderType provider) {}
 
   // Called each time a user launches a result.
   virtual void Train(const LaunchData& launch) {}
diff --git a/chrome/browser/ui/app_list/search/ranking/ranker_delegate.cc b/chrome/browser/ui/app_list/search/ranking/ranker_delegate.cc
index 88d371f..ea236ef 100644
--- a/chrome/browser/ui/app_list/search/ranking/ranker_delegate.cc
+++ b/chrome/browser/ui/app_list/search/ranking/ranker_delegate.cc
@@ -16,9 +16,11 @@
     ranker->Start(query);
 }
 
-void RankerDelegate::Rank(ResultsMap& results, ProviderType provider) {
+void RankerDelegate::Rank(ResultsMap& results,
+                          CategoriesMap& categories,
+                          ProviderType provider) {
   for (auto& ranker : rankers_)
-    ranker->Rank(results, provider);
+    ranker->Rank(results, categories, provider);
 }
 
 void RankerDelegate::Train(const LaunchData& launch) {
diff --git a/chrome/browser/ui/app_list/search/ranking/ranker_delegate.h b/chrome/browser/ui/app_list/search/ranking/ranker_delegate.h
index 73edbe6..e233720 100644
--- a/chrome/browser/ui/app_list/search/ranking/ranker_delegate.h
+++ b/chrome/browser/ui/app_list/search/ranking/ranker_delegate.h
@@ -30,7 +30,9 @@
 
   // Ranker:
   void Start(const std::u16string& query) override;
-  void Rank(ResultsMap& results, ProviderType provider) override;
+  void Rank(ResultsMap& results,
+            CategoriesMap& categories,
+            ProviderType provider) override;
   void Train(const LaunchData& launch) override;
 
  private:
diff --git a/chrome/browser/ui/app_list/search/ranking/score_normalizing_ranker.cc b/chrome/browser/ui/app_list/search/ranking/score_normalizing_ranker.cc
index 53a489d..b864112 100644
--- a/chrome/browser/ui/app_list/search/ranking/score_normalizing_ranker.cc
+++ b/chrome/browser/ui/app_list/search/ranking/score_normalizing_ranker.cc
@@ -59,7 +59,8 @@
 
 ScoreNormalizingRanker::~ScoreNormalizingRanker() {}
 
-void ScoreNormalizingRanker::Rank(ResultsMap& results, ProviderType provider) {
-}
+void ScoreNormalizingRanker::Rank(ResultsMap& results,
+                                  CategoriesMap& categories,
+                                  ProviderType provider) {}
 
 }  // namespace app_list
diff --git a/chrome/browser/ui/app_list/search/ranking/score_normalizing_ranker.h b/chrome/browser/ui/app_list/search/ranking/score_normalizing_ranker.h
index e63d296..3c08748 100644
--- a/chrome/browser/ui/app_list/search/ranking/score_normalizing_ranker.h
+++ b/chrome/browser/ui/app_list/search/ranking/score_normalizing_ranker.h
@@ -31,7 +31,9 @@
   ScoreNormalizingRanker& operator=(const ScoreNormalizingRanker&) = delete;
 
   // Ranker:
-  void Rank(ResultsMap& results, ProviderType provider) override;
+  void Rank(ResultsMap& results,
+            CategoriesMap& categories,
+            ProviderType provider) override;
 
  private:
   // TODO(crbug.com/1247475): Score normalizers removed due to stability issues.
diff --git a/chrome/browser/ui/app_list/search/ranking/top_match_ranker.cc b/chrome/browser/ui/app_list/search/ranking/top_match_ranker.cc
index 3a138c6c..4caebd1 100644
--- a/chrome/browser/ui/app_list/search/ranking/top_match_ranker.cc
+++ b/chrome/browser/ui/app_list/search/ranking/top_match_ranker.cc
@@ -46,7 +46,9 @@
 
 TopMatchRanker::~TopMatchRanker() {}
 
-void TopMatchRanker::Rank(ResultsMap& results, ProviderType provider) {
+void TopMatchRanker::Rank(ResultsMap& results,
+                          CategoriesMap& categories,
+                          ProviderType provider) {
   const auto it = results.find(provider);
   DCHECK(it != results.end());
 
diff --git a/chrome/browser/ui/app_list/search/ranking/top_match_ranker.h b/chrome/browser/ui/app_list/search/ranking/top_match_ranker.h
index ec0ba31..83a10cc 100644
--- a/chrome/browser/ui/app_list/search/ranking/top_match_ranker.h
+++ b/chrome/browser/ui/app_list/search/ranking/top_match_ranker.h
@@ -27,7 +27,9 @@
   TopMatchRanker& operator=(const TopMatchRanker&) = delete;
 
   // Ranker:
-  void Rank(ResultsMap& results, ProviderType provider) override;
+  void Rank(ResultsMap& results,
+            CategoriesMap& categories,
+            ProviderType provider) override;
 };
 
 }  // namespace app_list
diff --git a/chrome/browser/ui/app_list/search/ranking/types.cc b/chrome/browser/ui/app_list/search/ranking/types.cc
index 82726d6..1d2850e 100644
--- a/chrome/browser/ui/app_list/search/ranking/types.cc
+++ b/chrome/browser/ui/app_list/search/ranking/types.cc
@@ -4,6 +4,8 @@
 
 #include "chrome/browser/ui/app_list/search/ranking/types.h"
 
+#include <stddef.h>
+
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/stringprintf.h"
diff --git a/chrome/browser/ui/app_list/search/ranking/types.h b/chrome/browser/ui/app_list/search/ranking/types.h
index 37733c7..94b50f6 100644
--- a/chrome/browser/ui/app_list/search/ranking/types.h
+++ b/chrome/browser/ui/app_list/search/ranking/types.h
@@ -7,7 +7,6 @@
 
 #include "ash/public/cpp/app_list/app_list_types.h"
 #include "base/files/file_path.h"
-#include "chrome/browser/ui/app_list/search/search_controller.h"
 
 namespace app_list {
 
diff --git a/chrome/browser/ui/app_list/search/ranking/util.cc b/chrome/browser/ui/app_list/search/ranking/util.cc
index 4a83421..b194949 100644
--- a/chrome/browser/ui/app_list/search/ranking/util.cc
+++ b/chrome/browser/ui/app_list/search/ranking/util.cc
@@ -27,8 +27,9 @@
     case ResultType::kInstalledApp:
     case ResultType::kInstantApp:
     case ResultType::kInternalApp:
+      return Category::kApps;
     case ResultType::kArcAppShortcut:
-      return Category::kApp;
+      return Category::kAppShortcuts;
     case ResultType::kOmnibox:
     case ResultType::kAnswerCard:
       return Category::kWeb;
@@ -39,9 +40,6 @@
     case ResultType::kFileSearch:
     case ResultType::kDriveSearch:
       return Category::kFiles;
-    case ResultType::kAssistantChip:
-    case ResultType::kAssistantText:
-      return Category::kAssistant;
     case ResultType::kOsSettings:
       return Category::kSettings;
     case ResultType::kHelpApp:
@@ -49,6 +47,9 @@
     case ResultType::kPlayStoreReinstallApp:
     case ResultType::kPlayStoreApp:
       return Category::kPlayStore;
+    case ResultType::kAssistantChip:
+    case ResultType::kAssistantText:
+      return Category::kSearchAndAssistant;
     // Never used in the search backend.
     case ResultType::kUnknown:
     // Suggested content toggle fake result type. Used only in ash, not in the
@@ -57,7 +58,7 @@
     // Deprecated.
     case ResultType::kLauncher:
       NOTREACHED();
-      return Category::kApp;
+      return Category::kApps;
   }
 }
 
@@ -65,23 +66,22 @@
   switch (category) {
     case Category::kUnknown:
       return u"(unknown) ";
-    case Category::kBestMatch:
-      // The debug string for best match is handled elsewhere.
-      return u"";
-    case Category::kApp:
+    case Category::kApps:
+      return u"(apps) ";
+    case Category::kAppShortcuts:
       return u"(apps) ";
     case Category::kWeb:
       return u"(web) ";
     case Category::kFiles:
       return u"(files) ";
-    case Category::kAssistant:
-      return u"(assistant) ";
     case Category::kSettings:
       return u"(settings) ";
     case Category::kHelp:
       return u"(help) ";
     case Category::kPlayStore:
       return u"(play store) ";
+    case Category::kSearchAndAssistant:
+      return u"(assistant) ";
   }
 }
 
diff --git a/chrome/browser/ui/app_list/search/ranking/util.h b/chrome/browser/ui/app_list/search/ranking/util.h
index e592fb5..9f43444 100644
--- a/chrome/browser/ui/app_list/search/ranking/util.h
+++ b/chrome/browser/ui/app_list/search/ranking/util.h
@@ -9,6 +9,7 @@
 
 #include "base/files/file_path.h"
 #include "chrome/browser/ui/app_list/search/ranking/types.h"
+#include "chrome/browser/ui/app_list/search/search_controller.h"
 
 class Profile;
 
@@ -21,8 +22,8 @@
 // Given a search result type, returns the category it should be placed in.
 Category ResultTypeToCategory(ResultType result_type);
 
-// TODO(crbug.com/1199206): Once the UI has support for categories this can be
-// removed.
+// TODO(crbug.com/1199206): Once the UI has support for categories the following
+// methods can be removed.
 
 // Given a category, returns a debug string of its name suitable for the interim
 // UI.
diff --git a/chrome/browser/ui/app_list/search/search_controller.h b/chrome/browser/ui/app_list/search/search_controller.h
index 09aa130..c4c40ba9 100644
--- a/chrome/browser/ui/app_list/search/search_controller.h
+++ b/chrome/browser/ui/app_list/search/search_controller.h
@@ -14,6 +14,7 @@
 
 #include "base/callback.h"
 #include "base/containers/flat_map.h"
+#include "base/logging.h"
 #include "base/macros.h"
 #include "base/observer_list_types.h"
 #include "chrome/browser/ui/app_list/search/mixer.h"
@@ -41,6 +42,7 @@
 
 using Results = std::vector<std::unique_ptr<ChromeSearchResult>>;
 using ResultsMap = base::flat_map<ProviderType, Results>;
+using CategoriesMap = base::flat_map<Category, double>;
 
 // Controller that collects query from given SearchBoxModel, dispatches it
 // to all search providers, then invokes the mixer to mix and to publish the
diff --git a/chrome/browser/ui/app_list/search/search_controller_impl_new.cc b/chrome/browser/ui/app_list/search/search_controller_impl_new.cc
index a5f670f..7eceb56 100644
--- a/chrome/browser/ui/app_list/search/search_controller_impl_new.cc
+++ b/chrome/browser/ui/app_list/search/search_controller_impl_new.cc
@@ -32,6 +32,7 @@
 #include "chrome/browser/ui/app_list/search/ranking/ranker_delegate.h"
 #include "chrome/browser/ui/app_list/search/ranking/score_normalizing_ranker.h"
 #include "chrome/browser/ui/app_list/search/ranking/top_match_ranker.h"
+#include "chrome/browser/ui/app_list/search/ranking/util.h"
 #include "chrome/browser/ui/app_list/search/search_metrics_observer.h"
 #include "chrome/browser/ui/app_list/search/search_provider.h"
 #include "chrome/browser/ui/app_list/search/search_result_ranker/chip_ranker.h"
@@ -92,6 +93,7 @@
 
   last_query_ = query;
   results_.clear();
+  categories_.clear();
   for (Observer& observer : observer_list_)
     observer.OnResultsCleared();
 
@@ -162,7 +164,7 @@
   results_[provider_type] = std::move(results);
 
   // Update ranking of all results.
-  ranker_->Rank(results_, provider_type);
+  ranker_->Rank(results_, categories_, provider_type);
 
   // Compile a single list of results and sort by their relevance.
   std::vector<ChromeSearchResult*> all_results;
@@ -194,6 +196,15 @@
               return a->display_score() > b->display_score();
             });
 
+  // Create a vector of categories in display order.
+  std::vector<std::pair<Category, double>> sorted_category_pairs(
+      categories_.begin(), categories_.end());
+  std::sort(sorted_category_pairs.begin(), sorted_category_pairs.end(),
+            [](const auto& a, const auto& b) { return a.second > b.second; });
+  std::vector<Category> sorted_categories;
+  for (const auto& pair : sorted_category_pairs)
+    sorted_categories.push_back(pair.first);
+
   if (!observer_list_.empty()) {
     std::vector<const ChromeSearchResult*> observer_results;
     for (auto* result : all_results)
@@ -202,8 +213,7 @@
       observer.OnResultsAdded(last_query_, observer_results);
   }
 
-  // TODO(crbug.com/1199206): Send category ranks.
-  model_updater_->PublishSearchResults(all_results, /*categories=*/{});
+  model_updater_->PublishSearchResults(all_results, sorted_categories);
 }
 
 ChromeSearchResult* SearchControllerImplNew::FindSearchResult(
diff --git a/chrome/browser/ui/app_list/search/search_controller_impl_new.h b/chrome/browser/ui/app_list/search/search_controller_impl_new.h
index 71506cc..66947a0 100644
--- a/chrome/browser/ui/app_list/search/search_controller_impl_new.h
+++ b/chrome/browser/ui/app_list/search/search_controller_impl_new.h
@@ -101,6 +101,9 @@
   // Storage for all search results for the current query.
   ResultsMap results_;
 
+  // Storage for category scores for the current query.
+  CategoriesMap categories_;
+
   std::unique_ptr<SearchMetricsObserver> metrics_observer_;
   using Providers = std::vector<std::unique_ptr<SearchProvider>>;
   Providers providers_;
diff --git a/chrome/browser/ui/ash/capture_mode/capture_mode_browsertest.cc b/chrome/browser/ui/ash/capture_mode/capture_mode_browsertest.cc
index baf2dd7..2d50357 100644
--- a/chrome/browser/ui/ash/capture_mode/capture_mode_browsertest.cc
+++ b/chrome/browser/ui/ash/capture_mode/capture_mode_browsertest.cc
@@ -2,14 +2,20 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "ash/constants/ash_features.h"
 #include "ash/public/cpp/capture_mode/capture_mode_test_api.h"
+#include "ash/public/cpp/shell_window_ids.h"
 #include "ash/public/cpp/test/shell_test_api.h"
+#include "base/test/scoped_feature_list.h"
+#include "chrome/browser/ash/file_manager/file_manager_test_util.h"
+#include "chrome/browser/extensions/extension_browsertest.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_window.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "content/public/test/browser_test.h"
 #include "ui/aura/window.h"
 #include "ui/events/test/event_generator.h"
+#include "ui/wm/core/window_util.h"
 
 // Testing class to test CrOS capture mode, which is a feature to take
 // screenshots and record video.
@@ -31,3 +37,41 @@
   ash::CaptureModeTestApi().StartForWindow(/*for_video=*/false);
   EXPECT_TRUE(shell_test_api.IsContextMenuShown());
 }
+
+class AdvancedSettingsCaptureModeBrowserTest
+    : public extensions::ExtensionBrowserTest {
+ public:
+  AdvancedSettingsCaptureModeBrowserTest() {
+    scoped_feature_list_.InitAndEnableFeature(
+        ash::features::kImprovedScreenCaptureSettings);
+  }
+
+  ~AdvancedSettingsCaptureModeBrowserTest() override = default;
+
+  // extensions::ExtensionBrowserTest:
+  void SetUpOnMainThread() override {
+    extensions::ExtensionBrowserTest::SetUpOnMainThread();
+    CHECK(profile());
+    file_manager::test::AddDefaultComponentExtensionsOnMainThread(profile());
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+// Tests that the capture mode folder selection dialog window gets parented
+// correctly when a browser window is available.
+IN_PROC_BROWSER_TEST_F(AdvancedSettingsCaptureModeBrowserTest,
+                       FolderSelectionDialogParentedCorrectly) {
+  ASSERT_TRUE(browser());
+  ash::CaptureModeTestApi test_api;
+  test_api.StartForFullscreen(/*for_video=*/false);
+  test_api.SimulateOpeningFolderSelectionDialog();
+  auto* dialog_window = test_api.GetFolderSelectionDialogWindow();
+  ASSERT_TRUE(dialog_window);
+  auto* transient_root = wm::GetTransientRoot(dialog_window);
+  ASSERT_TRUE(transient_root);
+  EXPECT_EQ(transient_root->GetId(),
+            ash::kShellWindowId_CaptureModeFolderSelectionDialogOwner);
+  EXPECT_NE(transient_root, browser()->window()->GetNativeWindow());
+}
diff --git a/chrome/browser/ui/ash/thumbnail_loader.cc b/chrome/browser/ui/ash/thumbnail_loader.cc
index ea76ac2..c257891 100644
--- a/chrome/browser/ui/ash/thumbnail_loader.cc
+++ b/chrome/browser/ui/ash/thumbnail_loader.cc
@@ -24,6 +24,7 @@
 #include "extensions/browser/api/messaging/native_message_host.h"
 #include "extensions/common/api/messaging/messaging_endpoint.h"
 #include "extensions/common/api/messaging/port_id.h"
+#include "extensions/common/api/messaging/serialization_format.h"
 #include "extensions/common/extension.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "services/data_decoder/public/cpp/data_decoder.h"
@@ -286,7 +287,8 @@
       base::BindOnce(&ThumbnailLoader::OnThumbnailLoaded,
                      weak_factory_.GetWeakPtr(), request_id, request.size));
   const extensions::PortId port_id(base::UnguessableToken::Create(),
-                                   1 /* port_number */, true /* is_opener */);
+                                   1 /* port_number */, true /* is_opener */,
+                                   extensions::SerializationFormat::kJson);
   auto native_message_port = std::make_unique<extensions::NativeMessagePort>(
       message_service->GetChannelDelegate(), port_id,
       std::move(native_message_host));
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.cc b/chrome/browser/ui/autofill/chrome_autofill_client.cc
index 3fd7c23d..306e07f 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.cc
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.cc
@@ -770,6 +770,19 @@
   autofill_error_dialog_controller_.Show(error_dialog_type);
 }
 
+void ChromeAutofillClient::ShowAutofillProgressDialog(
+    base::OnceClosure cancel_callback) {
+  DCHECK(autofill_progress_dialog_controller_);
+  autofill_progress_dialog_controller_->ShowDialog(std::move(cancel_callback));
+}
+
+void ChromeAutofillClient::CloseAutofillProgressDialog(
+    bool show_confirmation_before_closing) {
+  DCHECK(autofill_progress_dialog_controller_);
+  autofill_progress_dialog_controller_->DismissDialog(
+      show_confirmation_before_closing);
+}
+
 bool ChromeAutofillClient::IsAutofillAssistantShowing() {
   auto* assistant_runtime_manager =
       autofill_assistant::RuntimeManager::GetForWebContents(web_contents());
@@ -899,7 +912,10 @@
           GetPersonalDataManager()->app_locale())),
       unmask_controller_(
           user_prefs::UserPrefs::Get(web_contents->GetBrowserContext())),
-      autofill_error_dialog_controller_(web_contents) {
+      autofill_error_dialog_controller_(web_contents),
+      autofill_progress_dialog_controller_(
+          std::make_unique<AutofillProgressDialogControllerImpl>(
+              web_contents)) {
   // TODO(crbug.com/928595): Replace the closure with a callback to the
   // renderer that indicates if log messages should be sent from the
   // renderer.
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.h b/chrome/browser/ui/autofill/chrome_autofill_client.h
index 0ec3d011..52cc68d 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.h
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.h
@@ -18,6 +18,7 @@
 #include "chrome/browser/autofill/autofill_gstatic_reader.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/autofill/payments/autofill_error_dialog_controller_impl.h"
+#include "chrome/browser/ui/autofill/payments/autofill_progress_dialog_controller_impl.h"
 #include "components/autofill/core/browser/autofill_client.h"
 #include "components/autofill/core/browser/logging/log_manager.h"
 #include "components/autofill/core/browser/payments/legal_message_line.h"
@@ -161,6 +162,9 @@
       const std::u16string& cvc,
       const gfx::Image& card_image) override;
   void ShowVirtualCardErrorDialog(bool is_permanent_error) override;
+  void ShowAutofillProgressDialog(base::OnceClosure cancel_callback) override;
+  void CloseAutofillProgressDialog(
+      bool show_confirmation_before_closing) override;
   bool IsAutofillAssistantShowing() override;
   bool IsAutocompleteEnabled() override;
   void PropagateAutofillPredictions(
@@ -220,6 +224,8 @@
 #endif
   CardUnmaskPromptControllerImpl unmask_controller_;
   AutofillErrorDialogControllerImpl autofill_error_dialog_controller_;
+  std::unique_ptr<AutofillProgressDialogControllerImpl>
+      autofill_progress_dialog_controller_;
 
   WEB_CONTENTS_USER_DATA_KEY_DECL();
 };
diff --git a/chrome/browser/ui/autofill/payments/autofill_progress_dialog_controller_impl.cc b/chrome/browser/ui/autofill/payments/autofill_progress_dialog_controller_impl.cc
index 18a1421..378bfeee1 100644
--- a/chrome/browser/ui/autofill/payments/autofill_progress_dialog_controller_impl.cc
+++ b/chrome/browser/ui/autofill/payments/autofill_progress_dialog_controller_impl.cc
@@ -15,23 +15,45 @@
     : web_contents_(web_contents) {}
 
 AutofillProgressDialogControllerImpl::~AutofillProgressDialogControllerImpl() {
-  Dismiss();
+  if (autofill_progress_dialog_view_) {
+    autofill_progress_dialog_view_->Dismiss(
+        /*show_confirmation_before_closing=*/false);
+    autofill_progress_dialog_view_ = nullptr;
+  }
 }
 
-void AutofillProgressDialogControllerImpl::ShowDialog() {
+void AutofillProgressDialogControllerImpl::ShowDialog(
+    base::OnceClosure cancel_callback) {
   DCHECK(!autofill_progress_dialog_view_);
 
+  cancel_callback_ = std::move(cancel_callback);
   autofill_progress_dialog_view_ =
       AutofillProgressDialogView::CreateAndShow(this);
 }
 
-void AutofillProgressDialogControllerImpl::ShowConfirmation() {
-  DCHECK(autofill_progress_dialog_view_);
-  autofill_progress_dialog_view_->ShowConfirmation();
+void AutofillProgressDialogControllerImpl::DismissDialog(
+    bool show_confirmation_before_closing) {
+  if (!autofill_progress_dialog_view_)
+    return;
+
+  autofill_progress_dialog_view_->Dismiss(show_confirmation_before_closing);
+  autofill_progress_dialog_view_ = nullptr;
 }
 
 void AutofillProgressDialogControllerImpl::OnDismissed() {
-  autofill_progress_dialog_view_ = nullptr;
+  // If the |autofill_progress_dialog_view_| is not a nullptr. It means the
+  // dismissal was triggered by the user cancelling the flow. Thus we should
+  // invoke the |cancel_callback_|.
+  if (autofill_progress_dialog_view_) {
+    autofill_progress_dialog_view_ = nullptr;
+    std::move(cancel_callback_).Run();
+    // TODO(crbug.com/1243475): Add metrics.
+    return;
+  }
+
+  // Otherwise it was triggered by the backend components. The
+  // |cancel_callback_| will not be invoked but be reset.
+  cancel_callback_.Reset();
 }
 
 const std::u16string AutofillProgressDialogControllerImpl::GetTitle() {
@@ -59,9 +81,4 @@
   return web_contents_;
 }
 
-void AutofillProgressDialogControllerImpl::Dismiss() {
-  if (autofill_progress_dialog_view_)
-    autofill_progress_dialog_view_->Dismiss();
-}
-
 }  // namespace autofill
diff --git a/chrome/browser/ui/autofill/payments/autofill_progress_dialog_controller_impl.h b/chrome/browser/ui/autofill/payments/autofill_progress_dialog_controller_impl.h
index 1d39ff3..4f471d7 100644
--- a/chrome/browser/ui/autofill/payments/autofill_progress_dialog_controller_impl.h
+++ b/chrome/browser/ui/autofill/payments/autofill_progress_dialog_controller_impl.h
@@ -29,8 +29,8 @@
 
   ~AutofillProgressDialogControllerImpl() override;
 
-  void ShowDialog();
-  void ShowConfirmation();
+  void ShowDialog(base::OnceClosure cancel_callback);
+  void DismissDialog(bool show_confirmation_before_closing);
 
   // AutofillProgressDialogController.
   void OnDismissed() override;
@@ -46,12 +46,13 @@
   }
 
  private:
-  // Dismiss the progress bar dialog if showing.
-  void Dismiss();
-
   content::WebContents* web_contents_;
+
   // View that displays the error dialog.
   AutofillProgressDialogView* autofill_progress_dialog_view_ = nullptr;
+
+  // Callback function invoked when the cancel button is clicked.
+  base::OnceClosure cancel_callback_;
 };
 
 }  // namespace autofill
diff --git a/chrome/browser/ui/autofill/payments/autofill_progress_dialog_view.h b/chrome/browser/ui/autofill/payments/autofill_progress_dialog_view.h
index 8d9df40..fb2dd36 100644
--- a/chrome/browser/ui/autofill/payments/autofill_progress_dialog_view.h
+++ b/chrome/browser/ui/autofill/payments/autofill_progress_dialog_view.h
@@ -16,10 +16,7 @@
   virtual ~AutofillProgressDialogView() = default;
 
   // Called by the controller to dismiss the dialog.
-  virtual void Dismiss() = 0;
-
-  // Called by the controller to show the confirmation view and message.
-  virtual void ShowConfirmation() = 0;
+  virtual void Dismiss(bool show_confirmation_before_closing) = 0;
 
   // Factory function for creating and showing the view.
   static AutofillProgressDialogView* CreateAndShow(
diff --git a/chrome/browser/ui/browser_command_controller.cc b/chrome/browser/ui/browser_command_controller.cc
index b746f90..68088c5 100644
--- a/chrome/browser/ui/browser_command_controller.cc
+++ b/chrome/browser/ui/browser_command_controller.cc
@@ -1101,8 +1101,8 @@
   // Hosted app browser commands.
   const bool enable_copy_url =
       is_web_app_or_custom_tab ||
-      base::FeatureList::IsEnabled(sharing_hub::kSharingHubDesktopOmnibox) ||
-      base::FeatureList::IsEnabled(sharing_hub::kSharingHubDesktopAppMenu);
+      sharing_hub::SharingHubOmniboxEnabled(browser_->profile()) ||
+      sharing_hub::SharingHubAppMenuEnabled(browser_->profile());
   command_updater_.UpdateCommandEnabled(IDC_COPY_URL, enable_copy_url);
   command_updater_.UpdateCommandEnabled(IDC_OPEN_IN_CHROME,
                                         is_web_app_or_custom_tab);
diff --git a/chrome/browser/ui/toolbar/app_menu_model.cc b/chrome/browser/ui/toolbar/app_menu_model.cc
index c3627779..c335077 100644
--- a/chrome/browser/ui/toolbar/app_menu_model.cc
+++ b/chrome/browser/ui/toolbar/app_menu_model.cc
@@ -854,7 +854,7 @@
 
   AddItemWithStringId(IDC_PRINT, IDS_PRINT);
 
-  if (!base::FeatureList::IsEnabled(sharing_hub::kSharingHubDesktopAppMenu) ||
+  if (!sharing_hub::SharingHubAppMenuEnabled(browser()->profile()) ||
       browser_->profile()->IsIncognitoProfile() ||
       browser_->profile()->IsGuestSession()) {
     if (media_router::MediaRouterEnabled(browser()->profile()))
diff --git a/chrome/browser/ui/user_education/tutorial/tutorial.cc b/chrome/browser/ui/user_education/tutorial/tutorial.cc
index 894ce64..62d8265 100644
--- a/chrome/browser/ui/user_education/tutorial/tutorial.cc
+++ b/chrome/browser/ui/user_education/tutorial/tutorial.cc
@@ -109,10 +109,16 @@
 
         tutorial_service->HideCurrentBubbleIfShowing();
 
+        base::RepeatingClosure abort_callback = base::BindRepeating(
+            [](TutorialService* tutorial_service) {
+              tutorial_service->AbortTutorial();
+            },
+            base::Unretained(tutorial_service));
+
         std::unique_ptr<TutorialBubble> bubble =
             bubble_factory_registry->CreateBubbleForTrackedElement(
                 element, title_text_, body_text_, arrow_, progress_,
-                is_last_step_);
+                std::move(abort_callback), is_last_step_);
         tutorial_service->SetCurrentBubble(std::move(bubble));
       },
       base::Unretained(tutorial_service),
diff --git a/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory.cc b/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory.cc
index 67c22af..6e8bd0d 100644
--- a/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory.cc
+++ b/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory.cc
@@ -4,6 +4,7 @@
 
 #include "chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory.h"
 
+#include "base/callback_forward.h"
 #include "chrome/browser/ui/user_education/tutorial/tutorial_bubble.h"
 #include "chrome/browser/ui/user_education/tutorial/tutorial_description.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
@@ -16,9 +17,10 @@
     absl::optional<std::u16string> body_text,
     TutorialDescription::Step::Arrow arrow,
     absl::optional<std::pair<int, int>> progress,
+    base::RepeatingClosure abort_callback,
     bool is_last_step) {
   if (!CanBuildBubbleForTrackedElement(element))
     return nullptr;
   return CreateBubble(element, title_text, body_text, arrow, progress,
-                      is_last_step);
+                      abort_callback, is_last_step);
 }
diff --git a/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory.h b/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory.h
index a0ceec8..8871aaf 100644
--- a/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory.h
+++ b/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory.h
@@ -5,6 +5,7 @@
 #ifndef CHROME_BROWSER_UI_USER_EDUCATION_TUTORIAL_TUTORIAL_BUBBLE_FACTORY_H_
 #define CHROME_BROWSER_UI_USER_EDUCATION_TUTORIAL_TUTORIAL_BUBBLE_FACTORY_H_
 
+#include "base/callback_forward.h"
 #include "chrome/browser/ui/user_education/tutorial/tutorial_bubble.h"
 #include "chrome/browser/ui/user_education/tutorial/tutorial_description.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
@@ -26,7 +27,8 @@
       absl::optional<std::u16string> body_text,
       TutorialDescription::Step::Arrow arrow,
       absl::optional<std::pair<int, int>> progress,
-      bool is_last_step);
+      base::RepeatingClosure abort_callback,
+      bool is_last_step = false);
 
  private:
   // Called by the Tutorial to show the bubble.
@@ -36,7 +38,8 @@
       absl::optional<std::u16string> body_text,
       TutorialDescription::Step::Arrow arrow,
       absl::optional<std::pair<int, int>> progress,
-      bool is_last_step) = 0;
+      base::RepeatingClosure abort_callback,
+      bool is_last_step = false) = 0;
 
   // Returns true iff the bubble owner can show a bubble for the TrackedElement.
   virtual bool CanBuildBubbleForTrackedElement(ui::TrackedElement* element) = 0;
diff --git a/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory_registry.cc b/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory_registry.cc
index c67cde1..4f123c4 100644
--- a/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory_registry.cc
+++ b/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory_registry.cc
@@ -25,11 +25,13 @@
     absl::optional<std::u16string> body_text,
     TutorialDescription::Step::Arrow arrow,
     absl::optional<std::pair<int, int>> progress,
+    base::RepeatingClosure abort_callback,
     bool is_last_step) {
   for (const auto& bubble_factory : bubble_factories_) {
     std::unique_ptr<TutorialBubble> bubble =
         bubble_factory->CreateBubbleIfElementIsValid(
-            element, title_text, body_text, arrow, progress, is_last_step);
+            element, title_text, body_text, arrow, progress, abort_callback,
+            is_last_step);
     if (bubble)
       return bubble;
   }
diff --git a/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory_registry.h b/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory_registry.h
index 70f9485..358acca 100644
--- a/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory_registry.h
+++ b/chrome/browser/ui/user_education/tutorial/tutorial_bubble_factory_registry.h
@@ -35,7 +35,8 @@
       absl::optional<std::u16string> body_text,
       TutorialDescription::Step::Arrow arrow,
       absl::optional<std::pair<int, int>> progress,
-      bool is_last_step);
+      base::RepeatingClosure abort_callback,
+      bool is_last_step = false);
 
  private:
   // the list of registered bubble factories
diff --git a/chrome/browser/ui/views/autofill/payments/autofill_progress_dialog_views.cc b/chrome/browser/ui/views/autofill/payments/autofill_progress_dialog_views.cc
index bfd4ee91..30256c8 100644
--- a/chrome/browser/ui/views/autofill/payments/autofill_progress_dialog_views.cc
+++ b/chrome/browser/ui/views/autofill/payments/autofill_progress_dialog_views.cc
@@ -17,6 +17,12 @@
 
 namespace autofill {
 
+namespace {
+// The delay before dismissing the dialog after the progress throber shows the
+// checkmark. This delay is for users to identify the status change.
+constexpr int kDelayBeforeDismissingDialogInSeconds = 1;
+}  // namespace
+
 AutofillProgressDialogViews::AutofillProgressDialogViews(
     AutofillProgressDialogController* controller)
     : controller_(controller) {
@@ -61,22 +67,23 @@
   return dialog_view;
 }
 
-void AutofillProgressDialogViews::Dismiss() {
-  if (controller_) {
-    controller_->OnDismissed();
-    controller_ = nullptr;
+void AutofillProgressDialogViews::Dismiss(
+    bool show_confirmation_before_closing) {
+  // If |show_confirmation_before_closing| is true, show the confirmation and
+  // close the widget with a delay.
+  if (show_confirmation_before_closing) {
+    label_->SetText(controller_->GetConfirmationMessage());
+    progress_throbber_->SetChecked(true);
+    base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+        FROM_HERE,
+        base::BindOnce(&AutofillProgressDialogViews::CloseWidget,
+                       weak_ptr_factory_.GetWeakPtr()),
+        base::Seconds(kDelayBeforeDismissingDialogInSeconds));
+    return;
   }
-  GetWidget()->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
-}
 
-void AutofillProgressDialogViews::ShowConfirmation() {
-  label_->SetText(controller_->GetConfirmationMessage());
-  progress_throbber_->SetChecked(true);
-  base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
-      FROM_HERE,
-      base::BindOnce(&AutofillProgressDialogViews::Dismiss,
-                     weak_ptr_factory_.GetWeakPtr()),
-      base::Seconds(1));
+  // Otherwise close the widget directly.
+  CloseWidget();
 }
 
 void AutofillProgressDialogViews::AddedToWidget() {
@@ -98,4 +105,12 @@
   return controller_->GetTitle();
 }
 
+void AutofillProgressDialogViews::CloseWidget() {
+  if (controller_) {
+    controller_->OnDismissed();
+    controller_ = nullptr;
+  }
+  GetWidget()->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
+}
+
 }  // namespace autofill
diff --git a/chrome/browser/ui/views/autofill/payments/autofill_progress_dialog_views.h b/chrome/browser/ui/views/autofill/payments/autofill_progress_dialog_views.h
index 91c7308..89a3259c 100644
--- a/chrome/browser/ui/views/autofill/payments/autofill_progress_dialog_views.h
+++ b/chrome/browser/ui/views/autofill/payments/autofill_progress_dialog_views.h
@@ -27,8 +27,7 @@
   ~AutofillProgressDialogViews() override;
 
   // AutofillProgressDialogView:
-  void Dismiss() override;
-  void ShowConfirmation() override;
+  void Dismiss(bool show_confirmation_before_closing) override;
 
   // DialogDelegate:
   void AddedToWidget() override;
@@ -36,6 +35,9 @@
   std::u16string GetWindowTitle() const override;
 
  private:
+  // Close the widget of this view, and notify controller.
+  void CloseWidget();
+
   AutofillProgressDialogController* controller_ = nullptr;
   views::Label* label_ = nullptr;
   views::Throbber* progress_throbber_ = nullptr;
diff --git a/chrome/browser/ui/views/autofill/payments/autofill_progress_dialog_views_browsertest.cc b/chrome/browser/ui/views/autofill/payments/autofill_progress_dialog_views_browsertest.cc
index 3b1824c..bbf1635 100644
--- a/chrome/browser/ui/views/autofill/payments/autofill_progress_dialog_views_browsertest.cc
+++ b/chrome/browser/ui/views/autofill/payments/autofill_progress_dialog_views_browsertest.cc
@@ -28,7 +28,9 @@
         browser()->tab_strip_model()->GetActiveWebContents());
   }
 
-  void ShowUi(const std::string& name) override { controller()->ShowDialog(); }
+  void ShowUi(const std::string& name) override {
+    controller()->ShowDialog(base::DoNothing());
+  }
 
   AutofillProgressDialogViews* GetDialogViews() {
     DCHECK(controller());
diff --git a/chrome/browser/ui/views/location_bar/location_bar_view.cc b/chrome/browser/ui/views/location_bar/location_bar_view.cc
index bd32df2..879e349 100644
--- a/chrome/browser/ui/views/location_bar/location_bar_view.cc
+++ b/chrome/browser/ui/views/location_bar/location_bar_view.cc
@@ -344,8 +344,7 @@
       params.types_enabled.push_back(PageActionIconType::kSharingHub);
     }
 #else
-    if (base::FeatureList::IsEnabled(sharing_hub::kSharingHubDesktopOmnibox) &&
-        !is_popup_mode_)
+    if (sharing_hub::SharingHubOmniboxEnabled(profile_) && !is_popup_mode_)
       params.types_enabled.push_back(PageActionIconType::kSharingHub);
 #endif
   }
diff --git a/chrome/browser/ui/views/select_file_dialog_extension.cc b/chrome/browser/ui/views/select_file_dialog_extension.cc
index bdfa062..576d28f 100644
--- a/chrome/browser/ui/views/select_file_dialog_extension.cc
+++ b/chrome/browser/ui/views/select_file_dialog_extension.cc
@@ -10,7 +10,7 @@
 #include <utility>
 
 #include "ash/constants/ash_features.h"
-#include "ash/public/cpp/capture_mode/capture_mode_api.h"
+#include "ash/public/cpp/shell_window_ids.h"
 #include "ash/public/cpp/tablet_mode.h"
 #include "base/bind.h"
 #include "base/callback.h"
@@ -48,6 +48,7 @@
 #include "extensions/browser/app_window/app_window.h"
 #include "extensions/browser/app_window/native_app_window.h"
 #include "extensions/browser/extension_system.h"
+#include "ui/aura/window.h"
 #include "ui/base/base_window.h"
 #include "ui/gfx/color_palette.h"
 #include "ui/shell_dialogs/select_file_policy.h"
@@ -442,8 +443,21 @@
   // The web contents to associate the dialog with.
   content::WebContents* web_contents = nullptr;
 
+  // The folder selection dialog created for capture mode should never be
+  // parented to a browser window (if one exists). https://crbug.com/1258842.
+  const bool is_for_capture_mode =
+      owner.window &&
+      owner.window->GetId() ==
+          ash::kShellWindowId_CaptureModeFolderSelectionDialogOwner;
+
+  const bool skip_finding_browser = is_for_capture_mode ||
+                                    owner.android_task_id.has_value() ||
+                                    owner.lacros_window_id.has_value();
+
+  can_resize_ = !ash::TabletMode::IsInTabletMode() && !is_for_capture_mode;
+
   // Obtain BaseWindow and WebContents if the owner window is browser.
-  if (!owner.android_task_id.has_value() && !owner.lacros_window_id.has_value())
+  if (!skip_finding_browser)
     FindRuntimeContext(owner.window, &base_window, &web_contents);
 
   if (web_contents)
@@ -475,11 +489,6 @@
   gfx::NativeWindow parent_window =
       base_window ? base_window->GetNativeWindow() : owner.window;
 
-#if BUILDFLAG(IS_CHROMEOS_ASH)
-  can_resize_ =
-      !ash::TabletMode::IsInTabletMode() && !ash::IsCaptureModeSessionActive();
-#endif
-
   if (ash::features::IsFileManagerSwaEnabled()) {
     // SystemFilesAppDialogDelegate is a self-deleting class that calls the
     // delete operator once the dialog for which it was created has been closed.
diff --git a/chrome/browser/ui/views/toolbar/app_menu.cc b/chrome/browser/ui/views/toolbar/app_menu.cc
index 4a0cf37..3e71f202 100644
--- a/chrome/browser/ui/views/toolbar/app_menu.cc
+++ b/chrome/browser/ui/views/toolbar/app_menu.cc
@@ -984,11 +984,8 @@
   if (command_id == IDC_MORE_TOOLS_MENU)
     return true;
 
-  if (command_id == IDC_SHARING_HUB_MENU) {
-    DCHECK(
-        base::FeatureList::IsEnabled(sharing_hub::kSharingHubDesktopAppMenu));
+  if (command_id == IDC_SHARING_HUB_MENU)
     return true;
-  }
 
   // The items representing the cut menu (cut/copy/paste), zoom menu
   // (increment/decrement/reset) and extension toolbar view are always enabled.
diff --git a/chrome/browser/ui/views/user_education/feature_promo_bubble_view.h b/chrome/browser/ui/views/user_education/feature_promo_bubble_view.h
index 028babc..709e4e9 100644
--- a/chrome/browser/ui/views/user_education/feature_promo_bubble_view.h
+++ b/chrome/browser/ui/views/user_education/feature_promo_bubble_view.h
@@ -89,9 +89,11 @@
     absl::optional<base::TimeDelta> timeout_after_interaction;
 
     // Used to call feature specific logic on dismiss.
+    // (TODO) dpenning: move to using a OnceClosure.
     absl::optional<base::RepeatingClosure> dismiss_callback;
 
     // Used to call feature specific logic on timeout.
+    // (TODO) dpenning: move to using a OnceClosure.
     base::RepeatingClosure timeout_callback;
   };
 
@@ -138,6 +140,8 @@
   base::TimeDelta timeout_after_interaction_;
 
   base::OneShotTimer auto_close_timer_;
+
+  // (TODO) dpenning: move to using a OnceClosure.
   base::RepeatingClosure timeout_callback_;
 };
 
diff --git a/chrome/browser/ui/views/user_education/tutorial_bubble_factory_views.cc b/chrome/browser/ui/views/user_education/tutorial_bubble_factory_views.cc
index 59147d6..b55f7b9 100644
--- a/chrome/browser/ui/views/user_education/tutorial_bubble_factory_views.cc
+++ b/chrome/browser/ui/views/user_education/tutorial_bubble_factory_views.cc
@@ -46,6 +46,7 @@
     absl::optional<std::u16string> body_text,
     TutorialDescription::Step::Arrow arrow,
     absl::optional<std::pair<int, int>> progress,
+    base::RepeatingClosure abort_callback,
     bool is_last_step) {
   if (!element->IsA<views::TrackedElementViews>())
     return nullptr;
@@ -98,6 +99,11 @@
   params.timeout_after_interaction =
       is_last_step ? kDelayWithInteraction : kDelayZero;
 
+  if (abort_callback) {
+    params.has_close_button = true;
+    params.dismiss_callback = abort_callback;
+  }
+
   return std::make_unique<TutorialBubbleViews>(
       owner_impl->ShowBubble(std::move(params), base::BindOnce([] {})));
 }
diff --git a/chrome/browser/ui/views/user_education/tutorial_bubble_factory_views.h b/chrome/browser/ui/views/user_education/tutorial_bubble_factory_views.h
index a929281..dbdb7b0 100644
--- a/chrome/browser/ui/views/user_education/tutorial_bubble_factory_views.h
+++ b/chrome/browser/ui/views/user_education/tutorial_bubble_factory_views.h
@@ -30,7 +30,8 @@
       absl::optional<std::u16string> body_text,
       TutorialDescription::Step::Arrow arrow,
       absl::optional<std::pair<int, int>> progress,
-      bool is_last_step) override;
+      base::RepeatingClosure abort_callback,
+      bool is_last_step = false) override;
 
   bool CanBuildBubbleForTrackedElement(ui::TrackedElement* element) override;
 };
diff --git a/chrome/browser/ui/webui/extensions/extensions_internals_source.cc b/chrome/browser/ui/webui/extensions/extensions_internals_source.cc
index c57ea39..836be23 100644
--- a/chrome/browser/ui/webui/extensions/extensions_internals_source.cc
+++ b/chrome/browser/ui/webui/extensions/extensions_internals_source.cc
@@ -160,10 +160,6 @@
   }
   if (disable_reasons & extensions::disable_reason::DISABLE_BLOCKED_BY_POLICY)
     disable_reasons_value.Append("DISABLE_BLOCKED_BY_POLICY");
-  if (disable_reasons &
-      extensions::disable_reason::DISABLE_REMOTELY_FOR_MALWARE) {
-    disable_reasons_value.Append("DISABLE_REMOTELY_FOR_MALWARE");
-  }
   return disable_reasons_value;
 }
 // The JSON we generate looks like this:
diff --git a/chrome/browser/ui/webui/settings/about_handler.cc b/chrome/browser/ui/webui/settings/about_handler.cc
index 5863f43..777be39 100644
--- a/chrome/browser/ui/webui/settings/about_handler.cc
+++ b/chrome/browser/ui/webui/settings/about_handler.cc
@@ -479,9 +479,10 @@
   }
 
   std::u16string channel;
-  bool is_powerwash_allowed;
-  if (!args->GetString(0, &channel) ||
-      !args->GetBoolean(1, &is_powerwash_allowed)) {
+  bool is_powerwash_allowed = false;
+  if (args->GetString(0, &channel) && args->GetList()[1].is_bool()) {
+    is_powerwash_allowed = args->GetList()[1].GetBool();
+  } else {
     LOG(ERROR) << "Can't parse SetChannel() args";
     return;
   }
diff --git a/chrome/browser/ui/webui/settings/chrome_cleanup_handler_win.cc b/chrome/browser/ui/webui/settings/chrome_cleanup_handler_win.cc
index 3efe0ec..64d4778 100644
--- a/chrome/browser/ui/webui/settings/chrome_cleanup_handler_win.cc
+++ b/chrome/browser/ui/webui/settings/chrome_cleanup_handler_win.cc
@@ -204,7 +204,8 @@
 void ChromeCleanupHandler::HandleStartScanning(const base::ListValue* args) {
   CHECK_EQ(1U, args->GetList().size());
   bool allow_logs_upload = false;
-  args->GetBoolean(0, &allow_logs_upload);
+  if (args->GetList()[0].is_bool())
+    allow_logs_upload = args->GetList()[0].GetBool();
 
   // If this operation is not allowed the UI should be disabled.
   CHECK(controller_->IsAllowedByPolicy());
@@ -230,7 +231,8 @@
 void ChromeCleanupHandler::HandleStartCleanup(const base::ListValue* args) {
   CHECK_EQ(1U, args->GetList().size());
   bool allow_logs_upload = false;
-  args->GetBoolean(0, &allow_logs_upload);
+  if (args->GetList()[0].is_bool())
+    allow_logs_upload = args->GetList()[0].GetBool();
 
   // The state is propagated to all open tabs and should be consistent.
   DCHECK_EQ(controller_->logs_enabled(profile_), allow_logs_upload);
@@ -251,7 +253,8 @@
     const base::ListValue* args) {
   CHECK_EQ(1U, args->GetList().size());
   bool details_section_visible = false;
-  args->GetBoolean(0, &details_section_visible);
+  if (args->GetList()[0].is_bool())
+    details_section_visible = args->GetList()[0].GetBool();
 
   if (details_section_visible) {
     base::RecordAction(
diff --git a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.cc b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.cc
index 552d6cb..9f1bce8 100644
--- a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.cc
@@ -98,16 +98,18 @@
 void AccessibilityHandler::HandleSetStartupSoundEnabled(
     const base::ListValue* args) {
   DCHECK_EQ(1U, args->GetList().size());
-  bool enabled;
-  args->GetBoolean(0, &enabled);
+  bool enabled = false;
+  if (args->GetList()[0].is_bool())
+    enabled = args->GetList()[0].GetBool();
   AccessibilityManager::Get()->SetStartupSoundEnabled(enabled);
 }
 
 void AccessibilityHandler::HandleRecordSelectedShowShelfNavigationButtonsValue(
     const base::ListValue* args) {
   DCHECK_EQ(1U, args->GetList().size());
-  bool enabled;
-  args->GetBoolean(0, &enabled);
+  bool enabled = false;
+  if (args->GetList()[0].is_bool())
+    enabled = args->GetList()[0].GetBool();
 
   a11y_nav_buttons_toggle_metrics_reporter_timer_.Start(
       FROM_HERE, base::Seconds(10),
diff --git a/chrome/browser/ui/webui/settings/chromeos/android_apps_handler.cc b/chrome/browser/ui/webui/settings/chromeos/android_apps_handler.cc
index ddaa8ca..8c4a8bb 100644
--- a/chrome/browser/ui/webui/settings/chromeos/android_apps_handler.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/android_apps_handler.cc
@@ -108,7 +108,8 @@
 void AndroidAppsHandler::ShowAndroidAppsSettings(const base::ListValue* args) {
   CHECK_EQ(1U, args->GetList().size());
   bool activated_from_keyboard = false;
-  args->GetBoolean(0, &activated_from_keyboard);
+  if (args->GetList()[0].is_bool())
+    activated_from_keyboard = args->GetList()[0].GetBool();
   int flags = activated_from_keyboard ? ui::EF_NONE : ui::EF_LEFT_MOUSE_BUTTON;
 
   app_service_proxy_->Launch(
diff --git a/chrome/browser/ui/webui/settings/chromeos/device_stylus_handler.cc b/chrome/browser/ui/webui/settings/chromeos/device_stylus_handler.cc
index 9543bdf..62a907c0 100644
--- a/chrome/browser/ui/webui/settings/chromeos/device_stylus_handler.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/device_stylus_handler.cc
@@ -138,7 +138,8 @@
 void StylusHandler::HandleSetPreferredNoteTakingAppEnabledOnLockScreen(
     const base::ListValue* args) {
   bool enabled = false;
-  CHECK(args->GetBoolean(0, &enabled));
+  CHECK(args->GetList()[0].is_bool());
+  enabled = args->GetList()[0].GetBool();
 
   NoteTakingHelper::Get()->SetPreferredAppEnabledOnLockScreen(
       Profile::FromWebUI(web_ui()), enabled);
diff --git a/chrome/browser/ui/webui/settings/chromeos/multidevice_handler.cc b/chrome/browser/ui/webui/settings/chromeos/multidevice_handler.cc
index 7c084ad..214076d 100644
--- a/chrome/browser/ui/webui/settings/chromeos/multidevice_handler.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/multidevice_handler.cc
@@ -351,7 +351,8 @@
 void MultideviceHandler::HandleSetSmartLockSignInEnabled(
     const base::ListValue* args) {
   bool enabled = false;
-  CHECK(args->GetBoolean(0, &enabled));
+  if (args->GetList()[0].is_bool())
+    enabled = args->GetList()[0].GetBool();
 
   std::string auth_token;
   bool auth_token_present = args->GetString(1, &auth_token);
diff --git a/chrome/browser/ui/webui/settings/people_handler.cc b/chrome/browser/ui/webui/settings/people_handler.cc
index c649d95..5d3936c 100644
--- a/chrome/browser/ui/webui/settings/people_handler.cc
+++ b/chrome/browser/ui/webui/settings/people_handler.cc
@@ -613,7 +613,8 @@
 
 void PeopleHandler::HandleSignout(const base::ListValue* args) {
   bool delete_profile = false;
-  args->GetBoolean(0, &delete_profile);
+  if (args->GetList()[0].is_bool())
+    delete_profile = args->GetList()[0].GetBool();
   base::FilePath profile_path = profile_->GetPath();
 
   if (!signin_util::IsUserSignoutAllowedForProfile(profile_)) {
diff --git a/chrome/browser/ui/webui/settings/protocol_handlers_handler.cc b/chrome/browser/ui/webui/settings/protocol_handlers_handler.cc
index 9515ab3..80c2efe 100644
--- a/chrome/browser/ui/webui/settings/protocol_handlers_handler.cc
+++ b/chrome/browser/ui/webui/settings/protocol_handlers_handler.cc
@@ -190,7 +190,8 @@
 void ProtocolHandlersHandler::HandleSetHandlersEnabled(
     const base::ListValue* args) {
   bool enabled = true;
-  CHECK(args->GetBoolean(0, &enabled));
+  CHECK(args->GetList()[0].is_bool());
+  enabled = args->GetList()[0].GetBool();
   if (enabled)
     GetProtocolHandlerRegistry()->Enable();
   else
diff --git a/chrome/browser/ui/webui/settings/reset_settings_handler.cc b/chrome/browser/ui/webui/settings/reset_settings_handler.cc
index 6449021c..72f8d4e 100644
--- a/chrome/browser/ui/webui/settings/reset_settings_handler.cc
+++ b/chrome/browser/ui/webui/settings/reset_settings_handler.cc
@@ -138,7 +138,8 @@
   std::string callback_id;
   CHECK(args->GetString(0, &callback_id));
   bool send_settings = false;
-  CHECK(args->GetBoolean(1, &send_settings));
+  CHECK(args->GetList()[1].is_bool());
+  send_settings = args->GetList()[1].GetBool();
   std::string request_origin_string;
   CHECK(args->GetString(2, &request_origin_string));
   reset_report::ChromeResetReport::ResetRequestOrigin request_origin =
diff --git a/chrome/browser/ui/webui/settings/site_settings_handler.cc b/chrome/browser/ui/webui/settings/site_settings_handler.cc
index d6518df..8223325 100644
--- a/chrome/browser/ui/webui/settings/site_settings_handler.cc
+++ b/chrome/browser/ui/webui/settings/site_settings_handler.cc
@@ -1127,8 +1127,8 @@
   CHECK(args->GetString(1, &secondary_pattern_string));
   std::string type;
   CHECK(args->GetString(2, &type));
-  bool incognito;
-  CHECK(args->GetBoolean(3, &incognito));
+  CHECK(args->GetList()[3].is_bool());
+  bool incognito = args->GetList()[3].GetBool();
 
   ContentSettingsType content_type =
       site_settings::ContentSettingsTypeFromGroupName(type);
@@ -1193,8 +1193,8 @@
   CHECK(args->GetString(2, &type));
   std::string value;
   CHECK(args->GetString(3, &value));
-  bool incognito;
-  CHECK(args->GetBoolean(4, &incognito));
+  CHECK(args->GetList()[4].is_bool());
+  bool incognito = args->GetList()[4].GetBool();
 
   ContentSettingsType content_type =
       site_settings::ContentSettingsTypeFromGroupName(type);
@@ -1458,8 +1458,8 @@
     return;
 
   CHECK_EQ(1U, args->GetList().size());
-  bool value;
-  CHECK(args->GetBoolean(0, &value));
+  CHECK(args->GetList()[0].is_bool());
+  bool value = args->GetList()[0].GetBool();
 
   profile_->GetPrefs()->SetBoolean(prefs::kBlockAutoplayEnabled, value);
 }
diff --git a/chrome/browser/ui/webui/webapks/webapks_handler.cc b/chrome/browser/ui/webui/webapks/webapks_handler.cc
index 248c458..bb2ca76 100644
--- a/chrome/browser/ui/webui/webapks/webapks_handler.cc
+++ b/chrome/browser/ui/webui/webapks/webapks_handler.cc
@@ -11,7 +11,7 @@
 #include "chrome/browser/android/shortcut_helper.h"
 #include "content/public/browser/web_ui.h"
 #include "third_party/blink/public/common/manifest/manifest_util.h"
-#include "ui/android/color_helpers.h"
+#include "ui/android/color_utils_android.h"
 #include "ui/gfx/color_utils.h"
 
 WebApksHandler::WebApksHandler()
diff --git a/chrome/browser/web_applications/web_app_constants.h b/chrome/browser/web_applications/web_app_constants.h
index 6f1764a2..11c5be6 100644
--- a/chrome/browser/web_applications/web_app_constants.h
+++ b/chrome/browser/web_applications/web_app_constants.h
@@ -286,9 +286,15 @@
 
 // A result how `WebAppIconDownloader` processed the list of icon urls.
 enum class IconsDownloadedResult {
-  // Icon downloading requests fulfilled.
+  // At least one (or all if `WebAppIconDownloader::FailAllIfAnyFail()`
+  // was called) icon urls were downloaded into the given `icons_map`.
   kCompleted,
-  // All icon downloading requests cancelled.
+  // There was an error downloading the icons, and `icons_map` is empty.
+  // Errors can include:
+  // * All icon downloads failed.
+  // * At least one icon download failed and
+  //  `WebAppIconDownloader::FailAllIfAnyFail()` was called.
+  // * Unexpected navigations or state changes on the `web_contents`.
   kCancelled,
 };
 
diff --git a/chrome/browser/web_applications/web_app_icon_downloader.cc b/chrome/browser/web_applications/web_app_icon_downloader.cc
index 5c9a3eb..10b8ded 100644
--- a/chrome/browser/web_applications/web_app_icon_downloader.cc
+++ b/chrome/browser/web_applications/web_app_icon_downloader.cc
@@ -109,7 +109,7 @@
     const GURL& image_url,
     const std::vector<SkBitmap>& bitmaps,
     const std::vector<gfx::Size>& original_bitmap_sizes) {
-  // Request may have been canceled by DidFinishNavigation().
+  // Request may have been canceled by PrimaryPageChanged().
   if (in_progress_requests_.erase(id) == 0)
     return;
 
@@ -144,7 +144,6 @@
     CompleteCallback();
 }
 
-// content::WebContentsObserver overrides:
 void WebAppIconDownloader::PrimaryPageChanged(content::Page& page) {
   CancelDownloads();
 }
diff --git a/chrome/browser/web_applications/web_app_icon_downloader.h b/chrome/browser/web_applications/web_app_icon_downloader.h
index cdf3479..3bf69eb 100644
--- a/chrome/browser/web_applications/web_app_icon_downloader.h
+++ b/chrome/browser/web_applications/web_app_icon_downloader.h
@@ -86,7 +86,7 @@
                           const std::vector<SkBitmap>& bitmaps,
                           const std::vector<gfx::Size>& original_bitmap_sizes);
 
-  // content::WebContentsObserver overrides:
+  // content::WebContentsObserver:
   void PrimaryPageChanged(content::Page& page) override;
   void DidUpdateFaviconURL(
       content::RenderFrameHost* rfh,
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 24e33fa..1875f07 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1634050758-81ac571898085b700004c0e280809306718f9da2.profdata
+chrome-win32-main-1634072245-4c09bdcbc793607463f7ca39634458b6e4988617.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 0bd63ad..103b759 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1634050758-7b5caff70f9b52b0f1ba9a90b5545e65e5dac729.profdata
+chrome-win64-main-1634072245-1b1c585a799cb124650c63f35c2823e95a980cc8.profdata
diff --git a/chrome/common/extensions/api/cryptotoken_private.idl b/chrome/common/extensions/api/cryptotoken_private.idl
index 1fa079c..3b61724 100644
--- a/chrome/common/extensions/api/cryptotoken_private.idl
+++ b/chrome/common/extensions/api/cryptotoken_private.idl
@@ -19,9 +19,12 @@
     DOMString appId;
     // The origin of the caller.
     DOMString origin;
-    // Identifies the tab in which the registration is occuring so that any
+    // Identifies the tab in which the request is occuring so that any
     // permissions prompt is correctly located.
     long tabId;
+    // Identifies the frame in which the frame is occuring. Required
+    // but ignored for `canAppIdGetAttestation`.
+    long frameId;
   };
 
   interface Functions {
diff --git a/chrome/renderer/extensions/extension_hooks_delegate.cc b/chrome/renderer/extensions/extension_hooks_delegate.cc
index 4063e16..520d643 100644
--- a/chrome/renderer/extensions/extension_hooks_delegate.cc
+++ b/chrome/renderer/extensions/extension_hooks_delegate.cc
@@ -228,8 +228,10 @@
   }
 
   v8::Local<v8::Value> v8_message = arguments[1];
+
   std::unique_ptr<Message> message = messaging_util::MessageFromV8(
-      script_context->v8_context(), v8_message, &error);
+      script_context->v8_context(), v8_message,
+      messaging_util::GetSerializationFormat(*script_context), &error);
   if (!message) {
     RequestResult result(RequestResult::INVALID_INVOCATION);
     result.error = std::move(error);
diff --git a/chrome/renderer/extensions/extension_hooks_delegate_unittest.cc b/chrome/renderer/extensions/extension_hooks_delegate_unittest.cc
index fa3a666..ab27f50 100644
--- a/chrome/renderer/extensions/extension_hooks_delegate_unittest.cc
+++ b/chrome/renderer/extensions/extension_hooks_delegate_unittest.cc
@@ -7,6 +7,7 @@
 #include "base/strings/stringprintf.h"
 #include "content/public/common/child_process_host.h"
 #include "extensions/common/api/messaging/messaging_endpoint.h"
+#include "extensions/common/api/messaging/serialization_format.h"
 #include "extensions/common/extension_builder.h"
 #include "extensions/common/extension_messages.h"
 #include "extensions/common/value_builder.h"
@@ -178,7 +179,7 @@
 
   const std::string kChannel = "chrome.extension.sendRequest";
   base::UnguessableToken other_context_id = base::UnguessableToken::Create();
-  const PortId port_id(other_context_id, 0, false);
+  const PortId port_id(other_context_id, 0, false, SerializationFormat::kJson);
 
   ExtensionMsg_TabConnectionInfo tab_connection_info;
   tab_connection_info.frame_id = 0;
@@ -207,8 +208,9 @@
 
   // Post the message to the receiver. Since the receiver doesn't respond, the
   // channel should remain open.
-  messaging_service()->DeliverMessage(script_context_set(), port_id,
-                                      Message("\"message\"", false), nullptr);
+  messaging_service()->DeliverMessage(
+      script_context_set(), port_id,
+      Message("\"message\"", SerializationFormat::kJson, false), nullptr);
   ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
   EXPECT_TRUE(
       messaging_service()->HasPortForTesting(script_context(), port_id));
diff --git a/chrome/renderer/extensions/tabs_hooks_delegate.cc b/chrome/renderer/extensions/tabs_hooks_delegate.cc
index c2b3bfd..3bcf061 100644
--- a/chrome/renderer/extensions/tabs_hooks_delegate.cc
+++ b/chrome/renderer/extensions/tabs_hooks_delegate.cc
@@ -84,8 +84,10 @@
   int tab_id = messaging_util::ExtractIntegerId(arguments[0]);
   v8::Local<v8::Value> v8_message = arguments[1];
   std::string error;
+
   std::unique_ptr<Message> message = messaging_util::MessageFromV8(
-      script_context->v8_context(), v8_message, &error);
+      script_context->v8_context(), v8_message,
+      messaging_util::GetSerializationFormat(*script_context), &error);
   if (!message) {
     RequestResult result(RequestResult::INVALID_INVOCATION);
     result.error = std::move(error);
@@ -119,8 +121,10 @@
   v8::Local<v8::Value> v8_message = arguments[1];
   DCHECK(!v8_message.IsEmpty());
   std::string error;
+
   std::unique_ptr<Message> message = messaging_util::MessageFromV8(
-      script_context->v8_context(), v8_message, &error);
+      script_context->v8_context(), v8_message,
+      messaging_util::GetSerializationFormat(*script_context), &error);
   if (!message) {
     RequestResult result(RequestResult::INVALID_INVOCATION);
     result.error = std::move(error);
@@ -154,7 +158,8 @@
 
   gin::Handle<GinPort> port = messaging_service_->Connect(
       script_context, MessageTarget::ForTab(tab_id, options.frame_id),
-      options.channel_name);
+      options.channel_name,
+      messaging_util::GetSerializationFormat(*script_context));
   DCHECK(!port.IsEmpty());
 
   RequestResult result(RequestResult::HANDLED);
diff --git a/chrome/services/sharing/nearby/nearby_connections_unittest.cc b/chrome/services/sharing/nearby/nearby_connections_unittest.cc
index ef9b8cb..f491c3c 100644
--- a/chrome/services/sharing/nearby/nearby_connections_unittest.cc
+++ b/chrome/services/sharing/nearby/nearby_connections_unittest.cc
@@ -1000,8 +1000,7 @@
         initiated_run_loop.Quit();
       });
 
-  ClientProxy* client_proxy =
-      StartAdvertising(fake_connection_life_cycle_listener, endpoint_data);
+  StartAdvertising(fake_connection_life_cycle_listener, endpoint_data);
   initiated_run_loop.Run();
 
   base::RunLoop accepted_run_loop;
@@ -1012,8 +1011,7 @@
       });
 
   FakePayloadListener fake_payload_listener;
-  client_proxy =
-      AcceptConnection(fake_payload_listener, endpoint_data.remote_endpoint_id);
+  AcceptConnection(fake_payload_listener, endpoint_data.remote_endpoint_id);
   accepted_run_loop.Run();
 }
 
diff --git a/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/webapps/WebApkIntentDataProviderBuilder.java b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/webapps/WebApkIntentDataProviderBuilder.java
index d5056df..0b179ae 100644
--- a/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/webapps/WebApkIntentDataProviderBuilder.java
+++ b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/webapps/WebApkIntentDataProviderBuilder.java
@@ -9,11 +9,11 @@
 
 import org.chromium.blink.mojom.DisplayMode;
 import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
-import org.chromium.chrome.browser.browserservices.intents.WebappConstants;
 import org.chromium.chrome.browser.webapps.WebApkIntentDataProviderFactory;
 import org.chromium.components.webapps.ShortcutSource;
 import org.chromium.components.webapps.WebApkDistributor;
 import org.chromium.device.mojom.ScreenOrientationLockType;
+import org.chromium.ui.util.ColorUtils;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -54,8 +54,7 @@
     public BrowserServicesIntentDataProvider build() {
         return WebApkIntentDataProviderFactory.create(new Intent(), mUrl, mScope, null, null, null,
                 null, mDisplayMode, ScreenOrientationLockType.DEFAULT, ShortcutSource.UNKNOWN,
-                WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING,
-                WebappConstants.MANIFEST_COLOR_INVALID_OR_MISSING, Color.WHITE,
+                ColorUtils.INVALID_COLOR, ColorUtils.INVALID_COLOR, Color.WHITE,
                 false /* isPrimaryIconMaskable */, false /* isSplashIconMaskable */,
                 mWebApkPackageName, /* shellApkVersion */ 1, mManifestUrl, mUrl,
                 WebApkDistributor.BROWSER,
diff --git a/chrome/test/base/testing_profile_manager.cc b/chrome/test/base/testing_profile_manager.cc
index 7ce2420..de4379c 100644
--- a/chrome/test/base/testing_profile_manager.cc
+++ b/chrome/test/base/testing_profile_manager.cc
@@ -204,6 +204,17 @@
 
 void TestingProfileManager::DeleteAllTestingProfiles() {
   DCHECK(called_set_up_);
+
+  ProfileAttributesStorage& storage =
+      profile_manager_->GetProfileAttributesStorage();
+  for (auto& name_profile_pair : testing_profiles_) {
+    TestingProfile* profile = name_profile_pair.second;
+    if (profile->IsGuestSession() || profile->IsSystemProfile()) {
+      // Guest and System profiles aren't added to Storage.
+      continue;
+    }
+    storage.RemoveProfile(profile->GetPath());
+  }
   profile_manager_->profiles_info_.clear();
 }
 
diff --git a/chrome/test/data/policy/policy_test_cases.json b/chrome/test/data/policy/policy_test_cases.json
index 8f81e4c..a3bb9bc 100644
--- a/chrome/test/data/policy/policy_test_cases.json
+++ b/chrome/test/data/policy/policy_test_cases.json
@@ -11094,6 +11094,12 @@
   "ReportUploadFrequency": {
     "reason_for_missing_test": "Maps into CrosSettings"
   },
+  "ReportDeviceNetworkTelemetryCollectionRateMs": {
+    "reason_for_missing_test": "Maps into CrosSettings"
+  },
+  "ReportDeviceNetworkTelemetryEventCheckingRateMs": {
+    "reason_for_missing_test": "Maps into CrosSettings"
+  },
   "DeviceAllowNewUsers": {
     "reason_for_missing_test": "Maps into CrosSettings"
   },
diff --git a/chrome/test/data/updater/Keystone.ticketstore b/chrome/test/data/updater/Keystone.ticketstore
new file mode 100644
index 0000000..ceab3f7
--- /dev/null
+++ b/chrome/test/data/updater/Keystone.ticketstore
Binary files differ
diff --git a/chrome/test/data/webrtc/webrtc_getdisplaymedia_test.html b/chrome/test/data/webrtc/webrtc_getdisplaymedia_test.html
index 66787c32..ea2ee12 100644
--- a/chrome/test/data/webrtc/webrtc_getdisplaymedia_test.html
+++ b/chrome/test/data/webrtc/webrtc_getdisplaymedia_test.html
@@ -9,14 +9,17 @@
   'use strict';
   var settings;
   var video_track;
-  var has_audio_track = false;
+  var audio_track;
+  var stored_media_stream;
 
   function handleSuccess(stream) {
-    has_audio_track = stream.getAudioTracks().length == 1;
     settings = stream.getVideoTracks()[0].getSettings();
     var video = document.querySelector('video');
     video.srcObject = stream;
     video_track = stream.getVideoTracks()[0];
+    if (stream.getAudioTracks().length > 0) {
+      audio_track = stream.getAudioTracks()[0];
+    }
     video.play();
     returnToTest("capture-success");
   }
@@ -38,7 +41,28 @@
   }
 
   function hasAudioTrack() {
-    returnToTest(has_audio_track ? "true" : "false");
+    returnToTest(`${!!audio_track}`);
+  }
+
+  function getVideoTrackType() {
+    if (!video_track) {
+      returnToTest("error-no-video-track");
+    }
+    returnToTest(video_track.constructor.name);
+  }
+
+  function getVideoCloneTrackType() {
+    if (!video_track) {
+      returnToTest("error-no-video-track-to-clone");
+    }
+    returnToTest(video_track.clone().constructor.name);
+  }
+
+  function getAudioTrackType() {
+    if (!audio_track) {
+      returnToTest("error-no-audio-track");
+    }
+    returnToTest(audio_track.constructor.name);
   }
 
   function getDisplaySurfaceSetting() {
@@ -87,10 +111,6 @@
       returnToTest("ended");
     }
   }
-
-  // function readLastResultFromEmbedded() {
-  //   // Blocks until the result is reported by the embedded page.
-  // }
   </script>
 </head>
 <body>
diff --git a/chrome/test/data/webui/chromeos/shortcut_customization/shortcut_customization_test.js b/chrome/test/data/webui/chromeos/shortcut_customization/shortcut_customization_test.js
index db564d6..6b8b741 100644
--- a/chrome/test/data/webui/chromeos/shortcut_customization/shortcut_customization_test.js
+++ b/chrome/test/data/webui/chromeos/shortcut_customization/shortcut_customization_test.js
@@ -309,6 +309,33 @@
     assertEquals(']', actualAccelerator.key_display);
   });
 
+  test('RemoveAccelerator', async () => {
+    await flushTasks();
+
+    // Open dialog for first accelerator in View Desk subsection.
+    await openDialogForAcceleratorInSubsection_(/*View Desk*/ 1);
+    let editDialog = page.shadowRoot.querySelector('#editDialog');
+    assertTrue(!!editDialog);
+
+    // Grab the first accelerator from Virtual Desks subsection.
+    let acceleratorList = editDialog.shadowRoot.querySelector('cr-dialog')
+                              .querySelectorAll('accelerator-edit-view');
+    assertEquals(1, acceleratorList.length);
+    const editView = acceleratorList[0];
+
+    // Click on remove button.
+    editView.shadowRoot.querySelector('#deleteButton').click();
+
+    await flushTasks();
+
+    // Requery the accelerator elements.
+    acceleratorList = editDialog.shadowRoot.querySelector('cr-dialog')
+                          .querySelectorAll('accelerator-edit-view');
+
+    // Expect that the accelerator has now been removed.
+    assertEquals(0, acceleratorList.length);
+  });
+
   suite('FakeMojoProviderTest', () => {
     test('SettingGettingTestProvider', () => {
       // TODO(zentaro): Replace with fake when built.
diff --git a/chrome/test/data/webui/cr_components/chromeos/bluetooth/bluetooth_base_page_test.js b/chrome/test/data/webui/cr_components/chromeos/bluetooth/bluetooth_base_page_test.js
index b8d32dd9..657b033 100644
--- a/chrome/test/data/webui/cr_components/chromeos/bluetooth/bluetooth_base_page_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/bluetooth/bluetooth_base_page_test.js
@@ -49,12 +49,19 @@
     flush();
   }
 
-  test('Title is shown', function() {
+  test('Title and loading indicator are shown', function() {
     const title = bluetoothBasePage.shadowRoot.querySelector('#title');
     assertTrue(!!title);
     assertEquals(
         bluetoothBasePage.i18n('bluetoothPairNewDevice'),
         title.textContent.trim());
+
+    const getProgress = () =>
+        bluetoothBasePage.shadowRoot.querySelector('paper-progress');
+    assertFalse(!!getProgress());
+    bluetoothBasePage.showScanProgress = true;
+    flush();
+    assertTrue(!!getProgress());
   });
 
   test('Button states', function() {
diff --git a/chrome/updater/BUILD.gn b/chrome/updater/BUILD.gn
index 2f2761f..63953b1 100644
--- a/chrome/updater/BUILD.gn
+++ b/chrome/updater/BUILD.gn
@@ -224,6 +224,7 @@
         "//chrome/updater/win:app_install_controller",
         "//chrome/updater/win/ui",
       ]
+
       sources += [
         "activity_impl_win.cc",
         "app/app_install_win.cc",
@@ -235,6 +236,7 @@
         "app/server/win/server.h",
         "app/server/win/service_main.cc",
         "app/server/win/service_main.h",
+        "app/server/win/wrl_classes.cc",
         "device_management/dm_storage_win.cc",
         "lib_util_win.cc",
         "policy/win/group_policy_manager.cc",
@@ -273,7 +275,6 @@
         "win/win_constants.h",
         "win/win_util.cc",
         "win/win_util.h",
-        "win/wrl_module.h",
       ]
       deps += [
         "//chrome/installer/util:with_no_strings",
@@ -548,6 +549,9 @@
 
     if (is_mac) {
       sources += [
+        "mac/keystone/ks_tickets.h",
+        "mac/keystone/ks_tickets.mm",
+        "mac/keystone/ks_tickets_unittest.mm",
         "mac/keystone/ksadmin_unittest.cc",
         "mac/net/network_unittest.cc",
         "mac/scoped_xpc_service_mock.h",
@@ -573,6 +577,7 @@
       ]
 
       data = [
+        "//chrome/test/data/updater/Keystone.ticketstore",
         "//chrome/test/data/updater/updater_setup_test_dmg.dmg",
         "//chrome/test/data/updater/updater_qualification_app_dmg.crx",
       ]
diff --git a/chrome/updater/app/server/win/server.cc b/chrome/updater/app/server/win/server.cc
index c663f36..3c93976 100644
--- a/chrome/updater/app/server/win/server.cc
+++ b/chrome/updater/app/server/win/server.cc
@@ -4,10 +4,11 @@
 
 #include "chrome/updater/app/server/win/server.h"
 
-#include <wrl/implements.h>
+#include <wrl/module.h>
 
 #include <algorithm>
 #include <memory>
+#include <string>
 #include <utility>
 
 #include "base/bind.h"
@@ -17,6 +18,8 @@
 #include "base/files/file_path.h"
 #include "base/logging.h"
 #include "base/memory/ref_counted.h"
+#include "base/strings/strcat.h"
+#include "base/strings/utf_string_conversions.h"
 #include "base/system/sys_info.h"
 #include "base/task/task_traits.h"
 #include "base/task/thread_pool.h"
@@ -36,7 +39,6 @@
 #include "chrome/updater/win/setup/setup_util.h"
 #include "chrome/updater/win/setup/uninstall.h"
 #include "chrome/updater/win/win_constants.h"
-#include "chrome/updater/win/wrl_module.h"
 #include "components/prefs/pref_service.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
@@ -47,6 +49,18 @@
   return base::CommandLine::ForCurrentProcess()->HasSwitch(kComServiceSwitch);
 }
 
+std::wstring GetCOMGroup(const std::wstring& prefix, UpdaterScope scope) {
+  return base::StrCat({prefix, base::ASCIIToWide(UpdaterScopeToString(scope))});
+}
+
+std::wstring COMGroup(UpdaterScope scope) {
+  return GetCOMGroup(L"Active", scope);
+}
+
+std::wstring COMGroupInternal(UpdaterScope scope) {
+  return GetCOMGroup(L"Internal", scope);
+}
+
 }  // namespace
 
 // Returns a leaky singleton of the App instance.
@@ -84,99 +98,26 @@
 }
 
 HRESULT ComServerApp::RegisterClassObjects() {
-  Microsoft::WRL::ComPtr<IUnknown> factory;
-  unsigned int flags = Microsoft::WRL::ModuleType::OutOfProc;
-
-  HRESULT hr = Microsoft::WRL::Details::CreateClassFactory<
-      Microsoft::WRL::SimpleClassFactory<UpdaterImpl>>(
-      &flags, nullptr, __uuidof(IClassFactory), &factory);
-  if (FAILED(hr)) {
-    LOG(ERROR) << "Factory creation for UpdaterImpl failed; hr: " << hr;
-    return hr;
-  }
-
-  Microsoft::WRL::ComPtr<IClassFactory> class_factory_updater;
-  hr = factory.As(&class_factory_updater);
-  if (FAILED(hr)) {
-    LOG(ERROR) << "IClassFactory object creation failed; hr: " << hr;
-    return hr;
-  }
-  factory.Reset();
-
-  hr = Microsoft::WRL::Details::CreateClassFactory<
-      Microsoft::WRL::SimpleClassFactory<LegacyOnDemandImpl>>(
-      &flags, nullptr, __uuidof(IClassFactory), &factory);
-  if (FAILED(hr)) {
-    LOG(ERROR) << "Factory creation for LegacyOnDemandImpl failed; hr: " << hr;
-    return hr;
-  }
-
-  Microsoft::WRL::ComPtr<IClassFactory> class_factory_legacy_ondemand;
-  hr = factory.As(&class_factory_legacy_ondemand);
-  if (FAILED(hr)) {
-    LOG(ERROR) << "IClassFactory object creation failed; hr: " << hr;
-    return hr;
-  }
-
-  // The pointer in this array is unowned. Do not release it.
-  IClassFactory* class_factories[] = {class_factory_updater.Get(),
-                                      class_factory_legacy_ondemand.Get()};
-  std::vector<CLSID> class_ids = GetActiveServers(updater_scope());
-  std::vector<DWORD> cookies(class_ids.size());
-  hr = Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule()
-           .RegisterCOMObject(nullptr, &class_ids[0], class_factories,
-                              &cookies[0], class_ids.size());
-  if (FAILED(hr)) {
-    LOG(ERROR) << "RegisterCOMObject failed; hr: " << hr;
-    return hr;
-  }
-
-  cookies.swap(cookies_);
-  return hr;
+  // Register COM class objects that are under either the ActiveSystem or the
+  // ActiveUser group.
+  // See wrl_classes.cc for details on the COM classes within the group.
+  return Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule()
+      .RegisterObjects(COMGroup(updater_scope()).c_str());
 }
 
 HRESULT ComServerApp::RegisterInternalClassObjects() {
-  Microsoft::WRL::ComPtr<IUnknown> factory;
-  unsigned int flags = Microsoft::WRL::ModuleType::OutOfProc;
-
-  HRESULT hr = Microsoft::WRL::Details::CreateClassFactory<
-      Microsoft::WRL::SimpleClassFactory<UpdaterInternalImpl>>(
-      &flags, nullptr, __uuidof(IClassFactory), &factory);
-  if (FAILED(hr)) {
-    LOG(ERROR) << "Factory creation for UpdaterInternalImpl failed; hr: " << hr;
-    return hr;
-  }
-
-  Microsoft::WRL::ComPtr<IClassFactory> class_factory_updater_internal;
-  hr = factory.As(&class_factory_updater_internal);
-  if (FAILED(hr)) {
-    LOG(ERROR) << "IClassFactory object creation failed; hr: " << hr;
-    return hr;
-  }
-  factory.Reset();
-
-  // The pointer in this array is unowned. Do not release it.
-  IClassFactory* class_factories[] = {class_factory_updater_internal.Get()};
-  std::vector<CLSID> class_ids = GetSideBySideServers(updater_scope());
-  std::vector<DWORD> cookies(class_ids.size());
-  hr = Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule()
-           .RegisterCOMObject(nullptr, &class_ids[0], class_factories,
-                              &cookies[0], class_ids.size());
-  if (FAILED(hr)) {
-    LOG(ERROR) << "RegisterCOMObject failed; hr: " << hr;
-    return hr;
-  }
-
-  cookies.swap(cookies_);
-  return hr;
+  // Register COM class objects that are under either the InternalSystem or the
+  // InternalUser group.
+  // See wrl_classes.cc for details on the COM classes within the group.
+  return Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule()
+      .RegisterObjects(COMGroupInternal(updater_scope()).c_str());
 }
 
 void ComServerApp::UnregisterClassObjects() {
-  auto& module = Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule();
   const HRESULT hr =
-      module.UnregisterCOMObject(nullptr, cookies_.data(), cookies_.size());
-  if (FAILED(hr))
-    LOG(ERROR) << "UnregisterCOMObject failed; hr: " << hr;
+      Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule()
+          .UnregisterObjects();
+  LOG_IF(ERROR, FAILED(hr)) << "UnregisterObjects failed; hr: " << hr;
 }
 
 void ComServerApp::CreateWRLModule() {
diff --git a/chrome/updater/app/server/win/server.h b/chrome/updater/app/server/win/server.h
index fc78fba..689b796 100644
--- a/chrome/updater/app/server/win/server.h
+++ b/chrome/updater/app/server/win/server.h
@@ -7,8 +7,6 @@
 
 #include <windows.h>
 
-#include <vector>
-
 #include "base/callback_forward.h"
 #include "base/check.h"
 #include "base/memory/scoped_refptr.h"
@@ -94,9 +92,6 @@
   // Handles COM setup and registration.
   void Start(base::OnceCallback<HRESULT()> register_callback);
 
-  // Identifier of registered class objects used for unregistration.
-  std::vector<DWORD> cookies_;
-
   // While this object lives, COM can be used by all threads in the program.
   base::win::ScopedCOMInitializer com_initializer_;
 
diff --git a/chrome/updater/app/server/win/service_main.cc b/chrome/updater/app/server/win/service_main.cc
index e2c2ffd..6cd814e 100644
--- a/chrome/updater/app/server/win/service_main.cc
+++ b/chrome/updater/app/server/win/service_main.cc
@@ -6,6 +6,7 @@
 
 #include <atlsecurity.h>
 #include <sddl.h>
+#include <wrl/module.h>
 
 #include <string>
 #include <type_traits>
@@ -21,7 +22,6 @@
 #include "chrome/updater/constants.h"
 #include "chrome/updater/win/win_constants.h"
 #include "chrome/updater/win/win_util.h"
-#include "chrome/updater/win/wrl_module.h"
 
 namespace updater {
 
diff --git a/chrome/updater/app/server/win/wrl_classes.cc b/chrome/updater/app/server/win/wrl_classes.cc
new file mode 100644
index 0000000..334dfb8
--- /dev/null
+++ b/chrome/updater/app/server/win/wrl_classes.cc
@@ -0,0 +1,60 @@
+// 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 <wrl/module.h>
+
+#include "chrome/updater/app/server/win/com_classes.h"
+#include "chrome/updater/app/server/win/com_classes_legacy.h"
+
+namespace updater {
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wmissing-braces"
+#pragma clang diagnostic ignored "-Wc++98-compat-extra-semi"
+
+// CoCreatableClassWithFactoryEx takes three parameters, `className`, `factory`,
+// and `serverName`:
+// * `className` identifies the COM class from the IDL file, and not the
+// corresponding C++ implementation class.
+// * `factory` identifies the class factory that creates the COM object, and is
+// typically Microsoft::WRL::SimpleClassFactory<>.
+// * `serverName` is used below to group COM classes that need to be
+// registered/unregistered together in a single process by using
+// Microsoft::WRL::Module::RegisterObjects and
+// Microsoft::WRL::Module::UnregisterObjects, respectively.
+//
+// Below, we are registering the following groups of COM objects:
+// * ActiveSystem
+// * ActiveUser
+// * InternalSystem
+// * InternalUser
+CoCreatableClassWithFactoryEx(UpdaterSystemClass,
+                              Microsoft::WRL::SimpleClassFactory<UpdaterImpl>,
+                              ActiveSystem);
+CoCreatableClassWithFactoryEx(
+    GoogleUpdate3WebSystemClass,
+    Microsoft::WRL::SimpleClassFactory<LegacyOnDemandImpl>,
+    ActiveSystem);
+
+CoCreatableClassWithFactoryEx(UpdaterUserClass,
+                              Microsoft::WRL::SimpleClassFactory<UpdaterImpl>,
+                              ActiveUser);
+CoCreatableClassWithFactoryEx(
+    GoogleUpdate3WebUserClass,
+    Microsoft::WRL::SimpleClassFactory<LegacyOnDemandImpl>,
+    ActiveUser);
+
+CoCreatableClassWithFactoryEx(
+    UpdaterInternalSystemClass,
+    Microsoft::WRL::SimpleClassFactory<UpdaterInternalImpl>,
+    InternalSystem);
+
+CoCreatableClassWithFactoryEx(
+    UpdaterInternalUserClass,
+    Microsoft::WRL::SimpleClassFactory<UpdaterInternalImpl>,
+    InternalUser);
+
+#pragma clang diagnostic pop
+
+}  // namespace updater
diff --git a/chrome/updater/mac/BUILD.gn b/chrome/updater/mac/BUILD.gn
index 2ddd6b91..f8db594 100644
--- a/chrome/updater/mac/BUILD.gn
+++ b/chrome/updater/mac/BUILD.gn
@@ -131,6 +131,8 @@
 executable("ksadmin") {
   output_name = "ksadmin"
   sources = [
+    "keystone/ks_tickets.h",
+    "keystone/ks_tickets.mm",
     "keystone/ksadmin.h",
     "keystone/ksadmin.mm",
     "keystone/ksadmin_main.cc",
diff --git a/chrome/updater/mac/keystone/ks_tickets.h b/chrome/updater/mac/keystone/ks_tickets.h
new file mode 100644
index 0000000..a46d72a
--- /dev/null
+++ b/chrome/updater/mac/keystone/ks_tickets.h
@@ -0,0 +1,20 @@
+// 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_UPDATER_MAC_KEYSTONE_KS_TICKETS_H_
+#define CHROME_UPDATER_MAC_KEYSTONE_KS_TICKETS_H_
+
+#import <Foundation/Foundation.h>
+
+@class KSPathExistenceChecker;
+
+// KSTicketStore holds a class method for reading an NSDictionary of NSString
+// to KSTickets.
+@interface KSTicketStore : NSObject
+
++ (nullable NSDictionary*)readStoreWithPath:(nonnull NSString*)path;
+
+@end
+
+#endif  // CHROME_UPDATER_MAC_KEYSTONE_KS_TICKETS_H_
diff --git a/chrome/updater/mac/keystone/ks_tickets.mm b/chrome/updater/mac/keystone/ks_tickets.mm
new file mode 100644
index 0000000..42f75e1c
--- /dev/null
+++ b/chrome/updater/mac/keystone/ks_tickets.mm
@@ -0,0 +1,286 @@
+// 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 "chrome/updater/mac/keystone/ks_tickets.h"
+
+#import <Foundation/Foundation.h>
+
+#include "base/logging.h"
+#include "base/notreached.h"
+#include "base/strings/sys_string_conversions.h"
+
+@interface KSPathExistenceChecker : NSObject <NSSecureCoding> {
+ @private
+  NSString* path_;
+}
+@end
+
+@interface KSTicket : NSObject <NSSecureCoding>
+@end
+
+@implementation KSTicketStore
+
++ (nullable NSDictionary*)readStoreWithPath:(nonnull NSString*)path {
+  if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
+    VLOG(0) << "Ticket store does not exist at "
+            << base::SysNSStringToUTF8(path);
+    return [NSDictionary dictionary];
+  }
+  NSError* readError = nil;
+  NSData* storeData = [NSData dataWithContentsOfFile:path
+                                             options:0  // Use normal IO
+                                               error:&readError];
+  if (!storeData) {
+    VLOG(0) << "Failed to decode ticket store at "
+            << base::SysNSStringToUTF8(path) << ": " << readError;
+    return nil;
+  }
+  if (![storeData length])
+    return [NSDictionary dictionary];
+  NSDictionary* store = nil;
+  @try {  // Unarchiver can throw
+    NSKeyedUnarchiver* unpacker =
+        [[NSKeyedUnarchiver alloc] initForReadingWithData:storeData];
+    unpacker.requiresSecureCoding = YES;
+    NSSet* classes =
+        [NSSet setWithObjects:[NSDictionary class], [KSTicket class],
+                              [KSPathExistenceChecker class], [NSArray class],
+                              [NSURL class], nil];
+    store = [unpacker decodeObjectOfClasses:classes
+                                     forKey:NSKeyedArchiveRootObjectKey];
+  } @catch (id e) {
+    VLOG(0) << base::SysNSStringToUTF8(
+        [NSString stringWithFormat:@"Ticket exception %@", e]);
+    return nil;
+  }
+  if (!store || ![store isKindOfClass:[NSDictionary class]]) {
+    VLOG(0) << "Ticket store is not a dictionary.";
+    return nil;
+  }
+  return store;
+}
+
+@end
+
+@implementation KSPathExistenceChecker
+
++ (BOOL)supportsSecureCoding {
+  return YES;
+}
+
+- (void)dealloc {
+  [path_ release];
+  [super dealloc];
+}
+
+- (id)initWithCoder:(NSCoder*)coder {
+  if ((self = [super init])) {
+    @try {
+      path_ = [[coder decodeObjectOfClass:[NSString class]
+                                   forKey:@"path"] retain];
+    } @catch (id e) {
+      VLOG(0) << base::SysNSStringToUTF8(
+          [NSString stringWithFormat:@"Coder exception %@", e]);
+      return nil;
+    }
+  }
+  return self;
+}
+
+- (void)encodeWithCoder:(NSCoder*)coder {
+  NOTREACHED() << "KSPathExistenceChecker::encodeWithCoder not implemented.";
+}
+
+- (NSString*)description {
+  // Formatting must stay the same in ksadmin output.
+  return [NSString
+      stringWithFormat:@"<%@:0x222222222222 path=%@>", [self class], path_];
+}
+
+@end
+
+NSString* const kKSTicketCohortKey = @"Cohort";
+NSString* const kKSTicketCohortHintKey = @"CohortHint";
+NSString* const kKSTicketCohortNameKey = @"CohortName";
+
+@implementation KSTicket {
+ @private
+  NSString* productID_;
+  NSString* version_;
+  KSPathExistenceChecker* existenceChecker_;
+  NSURL* serverURL_;
+  NSDate* creationDate_;
+  NSString* serverType_;
+  NSString* tag_;
+  NSString* tagPath_;
+  NSString* tagKey_;
+  NSString* brandPath_;
+  NSString* brandKey_;
+  NSString* versionPath_;
+  NSString* versionKey_;
+  NSString* cohort_;
+  NSString* cohortHint_;
+  NSString* cohortName_;
+  int32_t ticketVersion_;
+}
+
++ (BOOL)supportsSecureCoding {
+  return YES;
+}
+
+// Tries to obtain the server URL which may be NSURL or NSString object.
+// Verifies the read objects and guarantees that the returned object is NSURL.
+// The method may throw.
+- (NSURL*)decodeServerURL:(NSCoder*)decoder {
+  id serverURL = [decoder decodeObjectOfClasses:[NSSet setWithArray:@[
+                            [NSString class],
+                            [NSURL class],
+                          ]]
+                                         forKey:@"server_url"];
+  if (!serverURL)
+    return nil;
+  if ([serverURL isKindOfClass:[NSString class]]) {
+    return [NSURL URLWithString:serverURL];  // May throw
+  }
+  return (NSURL*)serverURL;
+}
+
+- (id)initWithCoder:(NSCoder*)coder {
+  if ((self = [super init])) {
+    @try {
+      productID_ = [[coder decodeObjectOfClass:[NSString class]
+                                        forKey:@"product_id"] retain];
+      version_ = [[coder decodeObjectOfClass:[NSString class]
+                                      forKey:@"version"] retain];
+      existenceChecker_ =
+          [[coder decodeObjectOfClass:[KSPathExistenceChecker class]
+                               forKey:@"existence_checker"] retain];
+      serverURL_ = [[self decodeServerURL:coder] retain];
+      creationDate_ = [[coder decodeObjectOfClass:[NSDate class]
+                                           forKey:@"creation_date"] retain];
+      serverType_ = [[coder decodeObjectOfClass:[NSString class]
+                                         forKey:@"serverType"] retain];
+      tag_ = [[coder decodeObjectOfClass:[NSString class]
+                                  forKey:@"tag"] retain];
+      tagPath_ = [[coder decodeObjectOfClass:[NSString class]
+                                      forKey:@"tagPath"] retain];
+      tagKey_ = [[coder decodeObjectOfClass:[NSString class]
+                                     forKey:@"tagKey"] retain];
+      brandPath_ = [[coder decodeObjectOfClass:[NSString class]
+                                        forKey:@"brandPath"] retain];
+      brandKey_ = [[coder decodeObjectOfClass:[NSString class]
+                                       forKey:@"brandKey"] retain];
+      versionPath_ = [[coder decodeObjectOfClass:[NSString class]
+                                          forKey:@"versionPath"] retain];
+      versionKey_ = [[coder decodeObjectOfClass:[NSString class]
+                                         forKey:@"versionKey"] retain];
+      cohort_ = [[coder decodeObjectOfClass:[NSString class]
+                                     forKey:kKSTicketCohortKey] retain];
+      cohortHint_ = [[coder decodeObjectOfClass:[NSString class]
+                                         forKey:kKSTicketCohortHintKey] retain];
+      cohortName_ = [[coder decodeObjectOfClass:[NSString class]
+                                         forKey:kKSTicketCohortNameKey] retain];
+      ticketVersion_ = [coder decodeInt32ForKey:@"ticketVersion"];
+    } @catch (id e) {
+      VLOG(0) << base::SysNSStringToUTF8(
+          [NSString stringWithFormat:@"Coder exception %@", e]);
+      return nil;
+    }
+  }
+  return self;
+}
+
+- (void)dealloc {
+  [productID_ release];
+  [version_ release];
+  [existenceChecker_ release];
+  [serverURL_ release];
+  [creationDate_ release];
+  [serverType_ release];
+  [tag_ release];
+  [tagPath_ release];
+  [tagKey_ release];
+  [brandPath_ release];
+  [brandKey_ release];
+  [versionPath_ release];
+  [versionKey_ release];
+  [cohort_ release];
+  [cohortHint_ release];
+  [cohortName_ release];
+
+  [super dealloc];
+}
+
+- (void)encodeWithCoder:(NSCoder*)coder {
+  NOTREACHED() << "KSTicket::encodeWithCoder not implemented.";
+}
+
+- (NSUInteger)hash {
+  return [productID_ hash] + [version_ hash] + [existenceChecker_ hash] +
+         [serverURL_ hash] + [creationDate_ hash];
+}
+
+- (NSString*)description {
+  // Keep the description stable.  Clients depend on the output formatting
+  // as "fieldname=value" without any additional quoting. We cannot use
+  // KSDescription() here because of these legacy formatting restrictions. In
+  // particular, ksadmin output must not be substantially changed.
+  NSString* serverTypeString = @"";
+  if (serverType_) {
+    serverTypeString =
+        [NSString stringWithFormat:@"\n\tserverType=%@", serverType_];
+  }
+  NSString* tagString = @"";
+  if (tag_) {
+    tagString = [NSString stringWithFormat:@"\n\ttag=%@", tag_];
+  }
+  NSString* tagPathString = @"";
+  if (tagPath_ && tagKey_) {
+    tagPathString = [NSString
+        stringWithFormat:@"\n\ttagPath=%@\n\ttagKey=%@", tagPath_, tagKey_];
+  }
+  NSString* brandPathString = @"";
+  if (brandPath_ && brandKey_) {
+    brandPathString =
+        [NSString stringWithFormat:@"\n\tbrandPath=%@\n\tbrandKey=%@",
+                                   brandPath_, brandKey_];
+  }
+  NSString* versionPathString = @"";
+  if (versionPath_ && versionKey_) {
+    versionPathString =
+        [NSString stringWithFormat:@"\n\tversionPath=%@\n\tversionKey=%@",
+                                   versionPath_, versionKey_];
+  }
+  NSString* cohortString = @"";
+  if ([cohort_ length]) {
+    cohortString = [NSString stringWithFormat:@"\n\tcohort=%@", cohort_];
+    if ([cohortName_ length]) {
+      cohortString = [cohortString
+          stringByAppendingFormat:@"\n\tcohortName=%@", cohortName_];
+    }
+  }
+  NSString* cohortHintString = @"";
+  if ([cohortHint_ length]) {
+    cohortHintString =
+        [NSString stringWithFormat:@"\n\tcohortHint=%@", cohortHint_];
+  }
+  NSString* ticketVersionString =
+      [NSString stringWithFormat:@"\n\tticketVersion=%d", ticketVersion_];
+  // Dates used to be parsed and stored as GMT and printed in GMT. That
+  // changed in 10.7 to be GMT with timezone information, so use a custom
+  // description string that matches our old output.
+  NSDateFormatter* dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
+  [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
+  [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
+  NSString* gmtDate = [dateFormatter stringFromDate:creationDate_];
+  return [NSString
+      stringWithFormat:@"<%@:0x222222222222\n\tproductID=%@\n\tversion=%@\n\t"
+                       @"xc=%@%@\n\turl=%@\n\tcreationDate=%@%@%@%@%@%@%@%@\n>",
+                       [self class], productID_, version_, existenceChecker_,
+                       serverTypeString, serverURL_, gmtDate, tagString,
+                       tagPathString, brandPathString, versionPathString,
+                       cohortString, cohortHintString, ticketVersionString];
+}
+
+@end
diff --git a/chrome/updater/mac/keystone/ks_tickets_unittest.mm b/chrome/updater/mac/keystone/ks_tickets_unittest.mm
new file mode 100644
index 0000000..9753f0b
--- /dev/null
+++ b/chrome/updater/mac/keystone/ks_tickets_unittest.mm
@@ -0,0 +1,120 @@
+// 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 "chrome/updater/mac/keystone/ks_tickets.h"
+
+#include "base/files/file_path.h"
+#include "base/path_service.h"
+#include "base/strings/sys_string_conversions.h"
+#include "chrome/common/chrome_paths.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace updater {
+
+TEST(KSTicketsTest, Decode) {
+  @autoreleasepool {
+    base::FilePath test_data_path;
+    ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_path));
+    NSDictionary* store = [KSTicketStore
+        readStoreWithPath:base::SysUTF8ToNSString(
+                              test_data_path
+                                  .Append(FILE_PATH_LITERAL("updater"))
+                                  .Append(
+                                      FILE_PATH_LITERAL("Keystone.ticketstore"))
+                                  .AsUTF8Unsafe())];
+
+    ASSERT_TRUE(store);
+    EXPECT_EQ([store count], 5ul);
+    EXPECT_EQ(
+        base::SysNSStringToUTF8(
+            [[store objectForKey:@"com.google.chrome.canary"] description]),
+        "<KSTicket:0x222222222222\n"
+        "\tproductID=com.google.Chrome.canary\n"
+        "\tversion=96.0.4662.0\n"
+        "\txc=<KSPathExistenceChecker:0x222222222222 "
+        "path=/Applications/Google Chrome Canary.app>\n"
+        "\tserverType=Omaha\n"
+        "\turl=https://tools.google.com/service/update2\n"
+        "\tcreationDate=2020-11-16 20:11:41\n"
+        "\ttag=canary\n"
+        "\ttagPath=/Applications/Google Chrome Canary.app/Contents/Info.plist\n"
+        "\ttagKey=KSChannelID\n"
+        "\tversionPath="
+        "/Applications/Google Chrome Canary.app/Contents/Info.plist\n"
+        "\tversionKey=KSVersion\n"
+        "\tcohort=1:0:\n"
+        "\tcohortName=Canary\n"
+        "\tticketVersion=1\n"
+        ">");
+    EXPECT_EQ(
+        base::SysNSStringToUTF8([[store
+            objectForKey:@"com.google.chrome_remote_desktop"] description]),
+        "<KSTicket:0x222222222222\n"
+        "\tproductID=com.google.chrome_remote_desktop\n"
+        "\tversion=94.0.4606.27\n"
+        "\txc=<KSPathExistenceChecker:0x222222222222 "
+        "path=/Library/LaunchAgents/org.chromium.chromoting.plist>\n"
+        "\tserverType=Omaha\n"
+        "\turl=https://tools.google.com/service/update2\n"
+        "\tcreationDate=2020-12-14 20:31:58\n"
+        "\tcohort=1:10ql:\n"
+        "\tcohortName=Stable\n"
+        "\tticketVersion=1\n"
+        ">");
+
+    EXPECT_EQ(base::SysNSStringToUTF8([[store
+                  objectForKey:@"com.google.secureconnect"] description]),
+              "<KSTicket:0x222222222222\n"
+              "\tproductID=com.google.SecureConnect\n"
+              "\tversion=2.1.8\n"
+              "\txc=<KSPathExistenceChecker:0x222222222222 "
+              "path=/Library/Application Support/Google/Endpoint "
+              "Verification/ApiHelper>\n"
+              "\tserverType=Omaha\n"
+              "\turl=https://tools.google.com/service/update2\n"
+              "\tcreationDate=2019-12-16 17:25:15\n"
+              "\tcohort=1::\n"
+              "\tticketVersion=1\n"
+              ">");
+
+    EXPECT_EQ(
+        base::SysNSStringToUTF8(
+            [[store objectForKey:@"com.google.chrome"] description]),
+        "<KSTicket:0x222222222222\n"
+        "\tproductID=com.google.Chrome\n"
+        "\tversion=94.0.4606.71\n"
+        "\txc=<KSPathExistenceChecker:0x222222222222 "
+        "path=/Applications/Google Chrome.app>\n"
+        "\tserverType=Omaha\n"
+        "\turl=https://tools.google.com/service/update2\n"
+        "\tcreationDate=2019-12-19 17:15:37\n"
+        "\ttagPath=/Applications/Google Chrome.app/Contents/Info.plist\n"
+        "\ttagKey=KSChannelID\n"
+        "\tbrandPath=/Library/Google/Google Chrome Brand.plist\n"
+        "\tbrandKey=KSBrandID\n"
+        "\tversionPath=/Applications/Google Chrome.app/Contents/Info.plist\n"
+        "\tversionKey=KSVersion\n"
+        "\tcohort=1:1y5:\n"
+        "\tcohortName=Stable\n"
+        "\tticketVersion=1\n"
+        ">");
+
+    EXPECT_EQ(base::SysNSStringToUTF8(
+                  [[store objectForKey:@"com.google.keystone"] description]),
+              "<KSTicket:0x222222222222\n"
+              "\tproductID=com.google.Keystone\n"
+              "\tversion=1.3.16.180\n"
+              "\txc=<KSPathExistenceChecker:0x222222222222 "
+              "path=/Library/Google/GoogleSoftwareUpdate/"
+              "GoogleSoftwareUpdate.bundle>\n"
+              "\turl=https://tools.google.com/service/update2\n"
+              "\tcreationDate=2019-12-16 17:18:45\n"
+              "\tcohort=1:0:\n"
+              "\tcohortName=Everyone\n"
+              "\tticketVersion=1\n"
+              ">");
+  }
+}
+
+}  // namespace updater
diff --git a/chrome/updater/mac/keystone/ksadmin.mm b/chrome/updater/mac/keystone/ksadmin.mm
index 9749c21..d8de9ce 100644
--- a/chrome/updater/mac/keystone/ksadmin.mm
+++ b/chrome/updater/mac/keystone/ksadmin.mm
@@ -20,11 +20,14 @@
 #include "base/memory/scoped_refptr.h"
 #include "base/message_loop/message_pump_type.h"
 #include "base/strings/string_util.h"
+#include "base/strings/sys_string_conversions.h"
 #include "base/task/single_thread_task_executor.h"
 #include "base/task/thread_pool.h"
 #include "base/task/thread_pool/thread_pool_instance.h"
 #include "base/threading/thread_restrictions.h"
 #include "chrome/updater/app/app.h"
+#include "chrome/updater/mac/keystone/ks_tickets.h"
+#include "chrome/updater/mac/mac_util.h"
 #include "chrome/updater/mac/update_service_proxy.h"
 #include "chrome/updater/registration_data.h"
 #include "chrome/updater/update_service.h"
@@ -87,12 +90,36 @@
   return result;
 }
 
+bool HasSwitch(const std::string& arg,
+               const std::map<std::string, std::string>& switches) {
+  if (base::Contains(switches, arg))
+    return true;
+  const static std::map<std::string, std::vector<std::string>> aliases = {
+      {kCommandDelete, {"d"}},        {kCommandInstall, {"i"}},
+      {kCommandList, {"l"}},          {kCommandKsadminVersion, {"k"}},
+      {kCommandPrintTag, {"G"}},      {kCommandPrintTickets, {"print", "p"}},
+      {kCommandRegister, {"r"}},      {kCommandSystemStore, {"S"}},
+      {kCommandUserInitiated, {"F"}}, {kCommandUserStore, {"U"}},
+  };
+  if (!base::Contains(aliases, arg))
+    return false;
+  for (const auto& alias : aliases.at(arg)) {
+    if (base::Contains(switches, alias))
+      return true;
+  }
+  return false;
+}
+
+UpdaterScope Scope(const std::map<std::string, std::string>& switches) {
+  return HasSwitch(kCommandSystemStore, switches) ? UpdaterScope::kSystem
+                                                  : UpdaterScope::kUser;
+}
+
 class KSAdminApp : public App {
  public:
   explicit KSAdminApp(const std::map<std::string, std::string>& switches)
-      : service_proxy_(
-            base::MakeRefCounted<UpdateServiceProxy>(GetUpdaterScope())),
-        switches_(switches) {}
+      : switches_(switches),
+        service_proxy_(base::MakeRefCounted<UpdateServiceProxy>(Scope())) {}
 
  private:
   ~KSAdminApp() override = default;
@@ -107,11 +134,12 @@
   void PrintVersion();
   void PrintTickets();
 
-  bool HasSwitch(const std::string& arg);
-  std::string SwitchValue(const std::string& arg);
+  UpdaterScope Scope() const;
+  bool HasSwitch(const std::string& arg) const;
+  std::string SwitchValue(const std::string& arg) const;
 
-  scoped_refptr<UpdateServiceProxy> service_proxy_;
   const std::map<std::string, std::string> switches_;
+  scoped_refptr<UpdateServiceProxy> service_proxy_;
 };
 
 void KSAdminApp::PrintUsage(const std::string& error_message) {
@@ -204,26 +232,11 @@
           base::BindOnce(&KSAdminApp::Shutdown, this)));
 }
 
-bool KSAdminApp::HasSwitch(const std::string& arg) {
-  if (base::Contains(switches_, arg))
-    return true;
-  const static std::map<std::string, std::vector<std::string>> aliases = {
-      {kCommandDelete, {"d"}},        {kCommandInstall, {"i"}},
-      {kCommandList, {"l"}},          {kCommandKsadminVersion, {"k"}},
-      {kCommandPrintTag, {"G"}},      {kCommandPrintTickets, {"print", "p"}},
-      {kCommandRegister, {"r"}},      {kCommandSystemStore, {"S"}},
-      {kCommandUserInitiated, {"F"}}, {kCommandUserStore, {"U"}},
-  };
-  if (!base::Contains(aliases, arg))
-    return false;
-  for (const auto& alias : aliases.at(arg)) {
-    if (base::Contains(switches_, alias))
-      return true;
-  }
-  return false;
+bool KSAdminApp::HasSwitch(const std::string& arg) const {
+  return updater::HasSwitch(arg, switches_);
 }
 
-std::string KSAdminApp::SwitchValue(const std::string& arg) {
+std::string KSAdminApp::SwitchValue(const std::string& arg) const {
   if (base::Contains(switches_, arg))
     return switches_.at(arg);
   const static std::map<std::string, std::string> aliases = {
@@ -239,6 +252,10 @@
   return base::Contains(switches_, alias) ? switches_.at(alias) : "";
 }
 
+UpdaterScope KSAdminApp::Scope() const {
+  return updater::Scope(switches_);
+}
+
 void KSAdminApp::Delete() {
   // TODO(crbug.com/1250524): Implement.
   Shutdown(1);
@@ -255,8 +272,26 @@
 }
 
 void KSAdminApp::PrintTickets() {
-  // TODO(crbug.com/1250524): Implement.
-  Shutdown(1);
+  // TODO(crbug.com/1250524): Print tickets owned by Chromium Updater. If there
+  // are any, suppress printing any legacy tickets.
+  @autoreleasepool {
+    const NSDictionary* store = [KSTicketStore
+        readStoreWithPath:base::SysUTF8ToNSString(
+                              GetKeystoneFolderPath(Scope())
+                                  ->Append(FILE_PATH_LITERAL("TicketStore"))
+                                  .Append(
+                                      FILE_PATH_LITERAL("Keystone.ticketstore"))
+                                  .AsUTF8Unsafe())];
+    if (store.count > 0) {
+      for (const id key in store) {
+        printf("%s\n",
+               base::SysNSStringToUTF8([store[key] description]).c_str());
+      }
+    } else {
+      printf("No tickets\n");
+    }
+  }
+  Shutdown(0);
 }
 
 void KSAdminApp::FirstTaskRun() {
@@ -289,12 +324,11 @@
 int KSAdminAppMain(int argc, char* argv[]) {
   base::AtExitManager exit_manager;
   base::CommandLine::Init(argc, argv);
-  // TODO(crbug.com/1250524): GetUpdaterScope() won't work: callers do not pass
-  // --system but rather --system-store. Check also other GetUpdaterScope calls
-  // in this code.
-  updater::InitLogging(GetUpdaterScope(), FILE_PATH_LITERAL("updater.log"));
+  std::map<std::string, std::string> command_line =
+      ParseCommandLine(argc, argv);
+  updater::InitLogging(Scope(command_line), FILE_PATH_LITERAL("updater.log"));
   base::SingleThreadTaskExecutor main_task_executor(base::MessagePumpType::UI);
-  return base::MakeRefCounted<KSAdminApp>(ParseCommandLine(argc, argv))->Run();
+  return base::MakeRefCounted<KSAdminApp>(command_line)->Run();
 }
 
 }  // namespace updater
diff --git a/chrome/updater/service_factory_win.cc b/chrome/updater/service_factory_win.cc
index 1ded800..0d3920b 100644
--- a/chrome/updater/service_factory_win.cc
+++ b/chrome/updater/service_factory_win.cc
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <wrl/module.h>
+
 #include "base/logging.h"
 #include "base/memory/ref_counted.h"
 #include "chrome/updater/updater_scope.h"
 #include "chrome/updater/win/update_service_internal_proxy.h"
 #include "chrome/updater/win/update_service_proxy.h"
-#include "chrome/updater/win/wrl_module.h"
 
 namespace updater {
 namespace {
diff --git a/chrome/updater/updater_scope.h b/chrome/updater/updater_scope.h
index bbda4d0..84255c2 100644
--- a/chrome/updater/updater_scope.h
+++ b/chrome/updater/updater_scope.h
@@ -6,6 +6,7 @@
 #define CHROME_UPDATER_UPDATER_SCOPE_H_
 
 #include <ostream>
+#include <string>
 
 namespace updater {
 
@@ -18,15 +19,19 @@
   kSystem = 2,
 };
 
-inline std::ostream& operator<<(std::ostream& os, UpdaterScope scope) {
+inline std::string UpdaterScopeToString(UpdaterScope scope) {
   switch (scope) {
     case UpdaterScope::kUser:
-      return os << "User";
+      return "User";
     case UpdaterScope::kSystem:
-      return os << "System";
+      return "System";
   }
 }
 
+inline std::ostream& operator<<(std::ostream& os, UpdaterScope scope) {
+  return os << UpdaterScopeToString(scope).c_str();
+}
+
 // Returns the scope of the updater, which is either per-system or per-user.
 // The updater scope is determined from command line arguments of the process,
 // the presence and content of the --tag argument, and the integrity level
diff --git a/chrome/updater/win/wrl_module.h b/chrome/updater/win/wrl_module.h
deleted file mode 100644
index a563076..0000000
--- a/chrome/updater/win/wrl_module.h
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2019 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_UPDATER_WIN_WRL_MODULE_H_
-#define CHROME_UPDATER_WIN_WRL_MODULE_H_
-
-#if !defined(__WRL_CLASSIC_COM_STRICT__)
-#error "WRL must not depend on WinRT."
-#endif
-
-#include <wrl/module.h>
-
-#endif  // CHROME_UPDATER_WIN_WRL_MODULE_H_
diff --git a/chromecast/base/chromecast_switches.cc b/chromecast/base/chromecast_switches.cc
index 68b80b1..04c0e1c 100644
--- a/chromecast/base/chromecast_switches.cc
+++ b/chromecast/base/chromecast_switches.cc
@@ -186,6 +186,9 @@
 // Whether to enable the drawing of rounded window corners in the root window.
 const char kEnableRoundedWindowCorners[] = "enable-rounded-window-corners";
 
+// Whether in hospitality mode
+const char kManagedMode[] = "managed-mode";
+
 // Endpoint that the mixer service listens on. This is a path for a UNIX domain
 // socket (default is /tmp/mixer-service).
 const char kMixerServiceEndpoint[] = "mixer-service-endpoint";
diff --git a/chromecast/base/chromecast_switches.h b/chromecast/base/chromecast_switches.h
index d06ee62..7ccb5a2 100644
--- a/chromecast/base/chromecast_switches.h
+++ b/chromecast/base/chromecast_switches.h
@@ -84,6 +84,7 @@
 extern const char kBackGestureHorizontalThreshold[];
 extern const char kEnableTopDragGesture[];
 extern const char kEnableRoundedWindowCorners[];
+extern const char kManagedMode[];
 
 // Background color used when Chromium hasn't rendered anything yet.
 extern const char kCastAppBackgroundColor[];
diff --git a/chromecast/browser/BUILD.gn b/chromecast/browser/BUILD.gn
index 52159fe..0c48413 100644
--- a/chromecast/browser/BUILD.gn
+++ b/chromecast/browser/BUILD.gn
@@ -584,6 +584,8 @@
     "cast_web_contents.h",
     "cast_web_view.cc",
     "cast_web_view.h",
+    "gesture_router.cc",
+    "gesture_router.h",
   ]
 
   # Need to expose this so that internal public_configs are propagated.
@@ -592,6 +594,7 @@
     ":client",
     "//chromecast/browser/mojom",
     "//chromecast/common/mojom",
+    "//chromecast/mojo",
     "//content/public/browser",
   ]
 
@@ -601,7 +604,9 @@
     "//chromecast/graphics",
     "//chromecast/ui:back_gesture_router",
     "//chromecast/ui/mojom",
+    "//content/public/browser",
     "//content/public/common",
+    "//mojo/public/cpp/bindings",
     "//ui/events",
     "//url",
   ]
diff --git a/chromecast/browser/android/cast_content_window_android.cc b/chromecast/browser/android/cast_content_window_android.cc
index d37e553..ec2f56e 100644
--- a/chromecast/browser/android/cast_content_window_android.cc
+++ b/chromecast/browser/android/cast_content_window_android.cc
@@ -181,8 +181,8 @@
     const base::android::JavaParamRef<jobject>& callback) {
   auto wrapper =
       std::make_unique<GestureConsumedCallbackWrapper>(env, callback);
-  if (delegate_) {
-    delegate_->ConsumeGesture(
+  if (gesture_router()) {
+    gesture_router()->ConsumeGesture(
         static_cast<GestureType>(gesture_type),
         base::BindOnce(&GestureConsumedCallbackWrapper::Invoke,
                        std::move(wrapper)));
diff --git a/chromecast/browser/cast_content_gesture_handler.cc b/chromecast/browser/cast_content_gesture_handler.cc
index 85b19b8..795a086 100644
--- a/chromecast/browser/cast_content_gesture_handler.cc
+++ b/chromecast/browser/cast_content_gesture_handler.cc
@@ -15,7 +15,7 @@
 }  // namespace
 
 CastContentGestureHandler::CastContentGestureHandler(
-    base::WeakPtr<CastContentWindow::Delegate> delegate,
+    GestureRouter* delegate,
     bool enable_top_drag_gesture)
     : priority_(Priority::NONE),
       enable_top_drag_gesture_(enable_top_drag_gesture),
@@ -26,8 +26,7 @@
   DCHECK(delegate_);
 }
 
-CastContentGestureHandler::CastContentGestureHandler(
-    base::WeakPtr<CastContentWindow::Delegate> delegate)
+CastContentGestureHandler::CastContentGestureHandler(GestureRouter* delegate)
     : CastContentGestureHandler(
           delegate,
           GetSwitchValueBoolean(switches::kEnableTopDragGesture, false)) {}
diff --git a/chromecast/browser/cast_content_gesture_handler.h b/chromecast/browser/cast_content_gesture_handler.h
index 2071c67d..c130897 100644
--- a/chromecast/browser/cast_content_gesture_handler.h
+++ b/chromecast/browser/cast_content_gesture_handler.h
@@ -8,7 +8,7 @@
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
 #include "base/timer/elapsed_timer.h"
-#include "chromecast/browser/cast_content_window.h"
+#include "chromecast/browser/gesture_router.h"
 #include "chromecast/graphics/gestures/cast_gesture_handler.h"
 
 namespace chromecast {
@@ -17,8 +17,9 @@
 // CastContentWindow::Delegate.
 class CastContentGestureHandler : public CastGestureHandler {
  public:
-  explicit CastContentGestureHandler(
-      base::WeakPtr<CastContentWindow::Delegate> delegate);
+  explicit CastContentGestureHandler(GestureRouter* delegate);
+  CastContentGestureHandler(GestureRouter* delegate,
+                            bool enable_top_drag_gesture);
   ~CastContentGestureHandler() override;
 
   // CastGestureHandler implementation:
@@ -33,9 +34,6 @@
   void SetPriority(Priority priority);
 
  private:
-  friend class CastContentGestureHandlerTest;
-  CastContentGestureHandler(base::WeakPtr<CastContentWindow::Delegate> delegate,
-                            bool enable_top_drag_gesture);
   GestureType GestureForSwipeOrigin(CastSideSwipeOrigin swipe_origin);
 
   Priority priority_;
@@ -44,7 +42,7 @@
 
   // Number of pixels past swipe origin to consider as a back gesture.
   const int back_horizontal_threshold_;
-  base::WeakPtr<CastContentWindow::Delegate> const delegate_;
+  GestureRouter* const delegate_;
   base::ElapsedTimer current_swipe_time_;
 };
 
diff --git a/chromecast/browser/cast_content_gesture_handler_test.cc b/chromecast/browser/cast_content_gesture_handler_test.cc
index 87ee316..2052321 100644
--- a/chromecast/browser/cast_content_gesture_handler_test.cc
+++ b/chromecast/browser/cast_content_gesture_handler_test.cc
@@ -40,211 +40,200 @@
 constexpr gfx::Point kOngoingRightGesturePoint1(400, 50);
 constexpr gfx::Point kRightGestureEndPoint(200, 60);
 
+class MockGestureHandler : public mojom::GestureHandler {
+ public:
+  MockGestureHandler() = default;
+  ~MockGestureHandler() override = default;
+
+  MOCK_METHOD(void, OnBackGesture, (OnBackGestureCallback), (override));
+  MOCK_METHOD(void, OnBackGestureProgress, (const gfx::Point&), (override));
+  MOCK_METHOD(void, OnTopDragGestureProgress, (const gfx::Point&), (override));
+  MOCK_METHOD(void, OnTopDragGestureDone, (), (override));
+  MOCK_METHOD(void,
+              OnRightDragGestureProgress,
+              (const gfx::Point&),
+              (override));
+  MOCK_METHOD(void, OnRightDragGestureDone, (), (override));
+  MOCK_METHOD(void, OnBackGestureCancel, (), (override));
+  MOCK_METHOD(void, OnTapGesture, (), (override));
+  MOCK_METHOD(void, OnTapDownGesture, (), (override));
+};
+
 }  // namespace
 
 class CastContentGestureHandlerTest : public testing::Test {
  public:
-  CastContentGestureHandlerTest()
-      : delegate_(new testing::StrictMock<MockCastContentWindowDelegate>),
-        dispatcher_(delegate_->AsWeakPtr(), true) {}
+  CastContentGestureHandlerTest() {}
+
+  void SetUp() final {
+    gesture_router_ = std::make_unique<GestureRouter>();
+    dispatcher_ = std::make_unique<CastContentGestureHandler>(
+        gesture_router_.get(), true);
+    gesture_router_->SetHandler(&handler_);
+  }
 
  protected:
-  std::unique_ptr<testing::StrictMock<MockCastContentWindowDelegate>> delegate_;
-  CastContentGestureHandler dispatcher_;
+  std::unique_ptr<GestureRouter> gesture_router_;
+  std::unique_ptr<CastContentGestureHandler> dispatcher_;
+  MockGestureHandler handler_;
 };
 
 // Verify the simple case of a left swipe with the right horizontal leads to
 // back.
 TEST_F(CastContentGestureHandlerTest, VerifySimpleBackSuccess) {
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::TOP_DRAG)))
-      .WillRepeatedly(Return(false));
-
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::GO_BACK)))
-      .WillRepeatedly(Return(true));
-  EXPECT_CALL(*delegate_, GestureProgress(Eq(GestureType::GO_BACK),
-                                          Eq(kOngoingBackGesturePoint1)));
-  EXPECT_CALL(*delegate_, ConsumeGesture(Eq(GestureType::GO_BACK), _))
-      .WillRepeatedly(
-          [](auto, auto callback) { std::move(callback).Run(true); });
-  dispatcher_.CanHandleSwipe(CastSideSwipeOrigin::LEFT);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::BEGIN,
-                              CastSideSwipeOrigin::LEFT, kLeftSidePoint);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
-                              CastSideSwipeOrigin::LEFT,
-                              kOngoingBackGesturePoint1);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::END,
-                              CastSideSwipeOrigin::LEFT,
-                              kValidBackGestureEndPoint);
+  gesture_router_->SetCanTopDrag(false);
+  gesture_router_->SetCanGoBack(true);
+  EXPECT_CALL(handler_, OnBackGestureProgress(Eq(kOngoingBackGesturePoint1)));
+  EXPECT_CALL(handler_, OnBackGesture(_));
+  ASSERT_TRUE(dispatcher_->CanHandleSwipe(CastSideSwipeOrigin::LEFT));
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::BEGIN,
+                               CastSideSwipeOrigin::LEFT, kLeftSidePoint);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
+                               CastSideSwipeOrigin::LEFT,
+                               kOngoingBackGesturePoint1);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::END,
+                               CastSideSwipeOrigin::LEFT,
+                               kValidBackGestureEndPoint);
 }
 
 // Verify that if the finger is not lifted, that's not a back gesture.
 TEST_F(CastContentGestureHandlerTest, VerifyNoDispatchOnNoLift) {
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::TOP_DRAG)))
-      .WillRepeatedly(Return(false));
-
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::GO_BACK)))
-      .WillRepeatedly(Return(true));
-  EXPECT_CALL(*delegate_, ConsumeGesture(Eq(GestureType::GO_BACK), _)).Times(0);
-  EXPECT_CALL(*delegate_, GestureProgress(Eq(GestureType::GO_BACK),
-                                          Eq(kValidBackGestureEndPoint)));
-  EXPECT_CALL(*delegate_,
-              GestureProgress(Eq(GestureType::GO_BACK), Eq(kPastTheEndPoint1)));
-  dispatcher_.CanHandleSwipe(CastSideSwipeOrigin::LEFT);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::BEGIN,
-                              CastSideSwipeOrigin::LEFT, kLeftSidePoint);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
-                              CastSideSwipeOrigin::LEFT,
-                              kValidBackGestureEndPoint);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
-                              CastSideSwipeOrigin::LEFT, kPastTheEndPoint1);
+  gesture_router_->SetCanTopDrag(false);
+  gesture_router_->SetCanGoBack(true);
+  EXPECT_CALL(handler_, OnBackGestureProgress(Eq(kValidBackGestureEndPoint)));
+  EXPECT_CALL(handler_, OnBackGesture(_)).Times(0);
+  EXPECT_CALL(handler_, OnBackGestureProgress(Eq(kPastTheEndPoint1)));
+  ASSERT_TRUE(dispatcher_->CanHandleSwipe(CastSideSwipeOrigin::LEFT));
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::BEGIN,
+                               CastSideSwipeOrigin::LEFT, kLeftSidePoint);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
+                               CastSideSwipeOrigin::LEFT,
+                               kValidBackGestureEndPoint);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
+                               CastSideSwipeOrigin::LEFT, kPastTheEndPoint1);
 }
 
 // Verify that multiple 'continue' events still only lead to one back
 // invocation.
 TEST_F(CastContentGestureHandlerTest, VerifyOnlySingleDispatch) {
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::TOP_DRAG)))
-      .WillRepeatedly(Return(false));
+  gesture_router_->SetCanTopDrag(false);
+  gesture_router_->SetCanGoBack(true);
 
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::GO_BACK)))
-      .WillRepeatedly(Return(true));
-  EXPECT_CALL(*delegate_, GestureProgress(Eq(GestureType::GO_BACK),
-                                          Eq(kValidBackGestureEndPoint)));
-  EXPECT_CALL(*delegate_,
-              GestureProgress(Eq(GestureType::GO_BACK), Eq(kPastTheEndPoint1)));
-  EXPECT_CALL(*delegate_, ConsumeGesture(Eq(GestureType::GO_BACK), _))
-      .WillRepeatedly(
-          [](auto, auto callback) { std::move(callback).Run(true); });
-  dispatcher_.CanHandleSwipe(CastSideSwipeOrigin::LEFT);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::BEGIN,
-                              CastSideSwipeOrigin::LEFT, kLeftSidePoint);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
-                              CastSideSwipeOrigin::LEFT,
-                              kValidBackGestureEndPoint);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
-                              CastSideSwipeOrigin::LEFT, kPastTheEndPoint1);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::END,
-                              CastSideSwipeOrigin::LEFT, kPastTheEndPoint2);
+  EXPECT_CALL(handler_, OnBackGestureProgress(Eq(kValidBackGestureEndPoint)));
+  EXPECT_CALL(handler_, OnBackGestureProgress(Eq(kPastTheEndPoint1)));
+  EXPECT_CALL(handler_, OnBackGesture(_));
+  ASSERT_TRUE(dispatcher_->CanHandleSwipe(CastSideSwipeOrigin::LEFT));
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::BEGIN,
+                               CastSideSwipeOrigin::LEFT, kLeftSidePoint);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
+                               CastSideSwipeOrigin::LEFT,
+                               kValidBackGestureEndPoint);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
+                               CastSideSwipeOrigin::LEFT, kPastTheEndPoint1);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::END,
+                               CastSideSwipeOrigin::LEFT, kPastTheEndPoint2);
 }
 
 // Verify that if the delegate says it doesn't handle back that we won't try to
 // ask them to consume it.
 TEST_F(CastContentGestureHandlerTest, VerifyDelegateDoesNotConsumeUnwanted) {
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::TOP_DRAG)))
-      .WillRepeatedly(Return(false));
-
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::GO_BACK)))
-      .WillRepeatedly(Return(false));
-  dispatcher_.CanHandleSwipe(CastSideSwipeOrigin::LEFT);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::BEGIN,
-                              CastSideSwipeOrigin::LEFT, kLeftSidePoint);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
-                              CastSideSwipeOrigin::LEFT,
-                              kValidBackGestureEndPoint);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::END,
-                              CastSideSwipeOrigin::LEFT, kPastTheEndPoint2);
+  gesture_router_->SetCanTopDrag(false);
+  gesture_router_->SetCanGoBack(false);
+  ASSERT_FALSE(dispatcher_->CanHandleSwipe(CastSideSwipeOrigin::LEFT));
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::BEGIN,
+                               CastSideSwipeOrigin::LEFT, kLeftSidePoint);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
+                               CastSideSwipeOrigin::LEFT,
+                               kValidBackGestureEndPoint);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::END,
+                               CastSideSwipeOrigin::LEFT, kPastTheEndPoint2);
 }
 
 // Verify that a not-left gesture doesn't lead to a swipe.
 TEST_F(CastContentGestureHandlerTest, VerifyNotLeftSwipeIsNotBack) {
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::TOP_DRAG)))
-      .WillRepeatedly(Return(false));
+  gesture_router_->SetCanTopDrag(false);
 
-  dispatcher_.CanHandleSwipe(CastSideSwipeOrigin::TOP);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::BEGIN,
-                              CastSideSwipeOrigin::TOP, kTopSidePoint);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
-                              CastSideSwipeOrigin::TOP,
-                              kOngoingTopGesturePoint2);
+  ASSERT_FALSE(dispatcher_->CanHandleSwipe(CastSideSwipeOrigin::TOP));
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::BEGIN,
+                               CastSideSwipeOrigin::TOP, kTopSidePoint);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
+                               CastSideSwipeOrigin::TOP,
+                               kOngoingTopGesturePoint2);
 }
 
 // Verify that if the gesture doesn't go far enough horizontally that we will
 // not consider it a swipe.
 TEST_F(CastContentGestureHandlerTest, VerifyNotFarEnoughRightIsNotBack) {
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::TOP_DRAG)))
-      .WillRepeatedly(Return(false));
+  gesture_router_->SetCanTopDrag(false);
+  gesture_router_->SetCanGoBack(true);
 
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::GO_BACK)))
-      .WillRepeatedly(Return(true));
-  EXPECT_CALL(*delegate_, GestureProgress(Eq(GestureType::GO_BACK),
-                                          Eq(kOngoingBackGesturePoint1)));
-  EXPECT_CALL(*delegate_, CancelGesture(Eq(GestureType::GO_BACK)));
-  dispatcher_.CanHandleSwipe(CastSideSwipeOrigin::LEFT);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::BEGIN,
-                              CastSideSwipeOrigin::LEFT, kLeftSidePoint);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
-                              CastSideSwipeOrigin::LEFT,
-                              kOngoingBackGesturePoint1);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::END,
-                              CastSideSwipeOrigin::LEFT,
-                              kOngoingBackGesturePoint2);
+  EXPECT_CALL(handler_, OnBackGestureProgress(Eq(kOngoingBackGesturePoint1)));
+  EXPECT_CALL(handler_, OnBackGestureCancel());
+  ASSERT_TRUE(dispatcher_->CanHandleSwipe(CastSideSwipeOrigin::LEFT));
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::BEGIN,
+                               CastSideSwipeOrigin::LEFT, kLeftSidePoint);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
+                               CastSideSwipeOrigin::LEFT,
+                               kOngoingBackGesturePoint1);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::END,
+                               CastSideSwipeOrigin::LEFT,
+                               kOngoingBackGesturePoint2);
 }
 
 // Verify that if the gesture ends before going far enough, that's also not a
 // swipe.
 TEST_F(CastContentGestureHandlerTest, VerifyNotFarEnoughRightAndEndIsNotBack) {
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::TOP_DRAG)))
-      .WillRepeatedly(Return(false));
+  gesture_router_->SetCanTopDrag(false);
+  gesture_router_->SetCanGoBack(true);
 
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::GO_BACK)))
-      .WillRepeatedly(Return(true));
-  EXPECT_CALL(*delegate_, GestureProgress(Eq(GestureType::GO_BACK),
-                                          Eq(kOngoingBackGesturePoint1)));
-  EXPECT_CALL(*delegate_, CancelGesture(Eq(GestureType::GO_BACK)));
-  dispatcher_.CanHandleSwipe(CastSideSwipeOrigin::LEFT);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::BEGIN,
-                              CastSideSwipeOrigin::LEFT, kLeftSidePoint);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
-                              CastSideSwipeOrigin::LEFT,
-                              kOngoingBackGesturePoint1);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::END,
-                              CastSideSwipeOrigin::LEFT,
-                              kOngoingBackGesturePoint2);
+  EXPECT_CALL(handler_, OnBackGestureProgress(Eq(kOngoingBackGesturePoint1)));
+  EXPECT_CALL(handler_, OnBackGestureCancel());
+  ASSERT_TRUE(dispatcher_->CanHandleSwipe(CastSideSwipeOrigin::LEFT));
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::BEGIN,
+                               CastSideSwipeOrigin::LEFT, kLeftSidePoint);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
+                               CastSideSwipeOrigin::LEFT,
+                               kOngoingBackGesturePoint1);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::END,
+                               CastSideSwipeOrigin::LEFT,
+                               kOngoingBackGesturePoint2);
 }
 
 // Verify simple top-down drag.
 TEST_F(CastContentGestureHandlerTest, VerifySimpleTopSuccess) {
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::TOP_DRAG)))
-      .WillRepeatedly(Return(true));
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::GO_BACK)))
-      .WillRepeatedly(Return(false));
+  gesture_router_->SetCanTopDrag(true);
+  gesture_router_->SetCanGoBack(false);
 
-  EXPECT_CALL(*delegate_, GestureProgress(Eq(GestureType::TOP_DRAG),
-                                          Eq(kOngoingTopGesturePoint1)));
-  EXPECT_CALL(*delegate_, ConsumeGesture(Eq(GestureType::TOP_DRAG), _))
-      .WillRepeatedly(
-          [](auto, auto callback) { std::move(callback).Run(true); });
-  dispatcher_.CanHandleSwipe(CastSideSwipeOrigin::TOP);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::BEGIN,
-                              CastSideSwipeOrigin::TOP, kTopSidePoint);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
-                              CastSideSwipeOrigin::TOP,
-                              kOngoingTopGesturePoint1);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::END,
-                              CastSideSwipeOrigin::LEFT, kTopGestureEndPoint);
+  EXPECT_CALL(handler_, OnTopDragGestureProgress(Eq(kOngoingTopGesturePoint1)));
+  EXPECT_CALL(handler_, OnTopDragGestureDone());
+  ASSERT_TRUE(dispatcher_->CanHandleSwipe(CastSideSwipeOrigin::TOP));
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::BEGIN,
+                               CastSideSwipeOrigin::TOP, kTopSidePoint);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
+                               CastSideSwipeOrigin::TOP,
+                               kOngoingTopGesturePoint1);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::END,
+                               CastSideSwipeOrigin::TOP, kTopGestureEndPoint);
 }
 
 // Verify simple right-to-left drag.
 TEST_F(CastContentGestureHandlerTest, VerifySimpleRightSuccess) {
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::RIGHT_DRAG)))
-      .WillRepeatedly(Return(true));
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::GO_BACK)))
-      .WillRepeatedly(Return(false));
-  EXPECT_CALL(*delegate_, CanHandleGesture(Eq(GestureType::TOP_DRAG)))
-      .WillRepeatedly(Return(false));
+  gesture_router_->SetCanRightDrag(true);
+  gesture_router_->SetCanTopDrag(false);
+  gesture_router_->SetCanGoBack(false);
 
-  EXPECT_CALL(*delegate_, GestureProgress(Eq(GestureType::RIGHT_DRAG),
-                                          Eq(kOngoingRightGesturePoint1)));
-  EXPECT_CALL(*delegate_, ConsumeGesture(Eq(GestureType::RIGHT_DRAG), _))
-      .WillRepeatedly(
-          [](auto, auto callback) { std::move(callback).Run(true); });
-  dispatcher_.CanHandleSwipe(CastSideSwipeOrigin::RIGHT);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::BEGIN,
-                              CastSideSwipeOrigin::RIGHT, kRightSidePoint);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
-                              CastSideSwipeOrigin::RIGHT,
-                              kOngoingRightGesturePoint1);
-  dispatcher_.HandleSideSwipe(CastSideSwipeEvent::END,
-                              CastSideSwipeOrigin::LEFT, kRightGestureEndPoint);
+  EXPECT_CALL(handler_,
+              OnRightDragGestureProgress(Eq(kOngoingRightGesturePoint1)));
+  EXPECT_CALL(handler_, OnRightDragGestureDone());
+  ASSERT_TRUE(dispatcher_->CanHandleSwipe(CastSideSwipeOrigin::RIGHT));
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::BEGIN,
+                               CastSideSwipeOrigin::RIGHT, kRightSidePoint);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
+                               CastSideSwipeOrigin::RIGHT,
+                               kOngoingRightGesturePoint1);
+  dispatcher_->HandleSideSwipe(CastSideSwipeEvent::END,
+                               CastSideSwipeOrigin::RIGHT,
+                               kRightGestureEndPoint);
 }
 
 }  // namespace chromecast
diff --git a/chromecast/browser/cast_content_window.cc b/chromecast/browser/cast_content_window.cc
index bff258e..c7a90734 100644
--- a/chromecast/browser/cast_content_window.cc
+++ b/chromecast/browser/cast_content_window.cc
@@ -8,10 +8,24 @@
 
 CastContentWindow::CastContentWindow(base::WeakPtr<Delegate> delegate,
                                      mojom::CastWebViewParamsPtr params)
-    : delegate_(delegate), params_(std::move(params)) {}
+    : delegate_(delegate), params_(std::move(params)) {
+  RegisterBackGestureRouter(gesture_router());
+}
 
 CastContentWindow::~CastContentWindow() = default;
 
+void CastContentWindow::SetCastWebContents(CastWebContents* cast_web_contents) {
+  cast_web_contents_ = cast_web_contents;
+  // Must provide binder callbacks with WeakPtr since CastContentWindow + these
+  // interface implementations are destroyed before CastWebContents.
+  cast_web_contents_->local_interfaces()->AddBinder(base::BindRepeating(
+      &CastContentWindow::BindReceiver, weak_factory_.GetWeakPtr()));
+  cast_web_contents_->local_interfaces()->AddBinder(base::BindRepeating(
+      &CastContentWindow::BindActivityWindow, weak_factory_.GetWeakPtr()));
+  cast_web_contents_->local_interfaces()->AddBinder(
+      gesture_router()->GetBinder());
+}
+
 void CastContentWindow::AddObserver(Observer* observer) {
   observer_list_.AddObserver(observer);
 }
@@ -25,6 +39,19 @@
   receiver_.Bind(std::move(receiver));
 }
 
+void CastContentWindow::BindActivityWindow(
+    mojo::PendingReceiver<mojom::ActivityWindow> receiver) {
+  activity_window_receiver_.Bind(std::move(receiver));
+}
+
+void CastContentWindow::Show() {
+  RequestVisibility(VisibilityPriority::STICKY_ACTIVITY);
+}
+
+void CastContentWindow::Hide() {
+  RequestMoveOut();
+}
+
 mojom::MediaControlUi* CastContentWindow::media_controls() {
   return nullptr;
 }
diff --git a/chromecast/browser/cast_content_window.h b/chromecast/browser/cast_content_window.h
index 200a4d5..2b6974e 100644
--- a/chromecast/browser/cast_content_window.h
+++ b/chromecast/browser/cast_content_window.h
@@ -11,9 +11,11 @@
 #include "base/observer_list.h"
 #include "base/observer_list_types.h"
 #include "chromecast/browser/cast_web_contents.h"
+#include "chromecast/browser/gesture_router.h"
 #include "chromecast/browser/mojom/cast_content_window.mojom.h"
 #include "chromecast/browser/mojom/cast_web_service.mojom.h"
 #include "chromecast/browser/visibility_types.h"
+#include "chromecast/common/mojom/activity_window.mojom.h"
 #include "chromecast/graphics/gestures/cast_gesture_handler.h"
 #include "chromecast/ui/back_gesture_router.h"
 #include "chromecast/ui/mojom/media_control_ui.mojom.h"
@@ -27,32 +29,14 @@
 // Class that represents the "window" a WebContents is displayed in cast_shell.
 // For Linux, this represents an Aura window. For Android, this is a Activity.
 // See CastContentWindowAura and CastContentWindowAndroid.
-class CastContentWindow : public mojom::CastContentWindow {
+class CastContentWindow : public mojom::CastContentWindow,
+                          public mojom::ActivityWindow {
  public:
   class Delegate {
    public:
     // Notify window destruction.
     virtual void OnWindowDestroyed() {}
 
-    using GestureHandledCallback = base::OnceCallback<void(bool)>;
-
-    // Check to see if the gesture can be handled by the delegate. This is
-    // called prior to ConsumeGesture().
-    virtual bool CanHandleGesture(GestureType gesture_type) = 0;
-
-    // Called while a system UI gesture is in progress.
-    virtual void GestureProgress(GestureType gesture_type,
-                                 const gfx::Point& touch_location) {}
-
-    // Called when an in-progress system UI gesture is cancelled (for example
-    // when the finger is lifted before the completion of the gesture.)
-    virtual void CancelGesture(GestureType gesture_type) {}
-
-    // Consume and handle a completed UI gesture. Invokes the callback with a
-    // boolean indicating whether the gesture was handled or not.
-    virtual void ConsumeGesture(GestureType gesture_type,
-                                GestureHandledCallback handled_callback) = 0;
-
     // Notify visibility change for this window.
     virtual void OnVisibilityChange(VisibilityType visibility_type) {}
 
@@ -74,11 +58,10 @@
   ~CastContentWindow() override;
 
   // |cast_web_contents| must outlive the CastContentWindow.
-  void SetCastWebContents(CastWebContents* cast_web_contents) {
-    cast_web_contents_ = cast_web_contents;
-  }
+  void SetCastWebContents(CastWebContents* cast_web_contents);
 
   CastWebContents* cast_web_contents() { return cast_web_contents_; }
+  GestureRouter* gesture_router() { return &gesture_router_; }
 
   // mojom::CastContentWindow implementation:
   void CreateWindow(mojom::ZOrder z_order,
@@ -90,6 +73,10 @@
   void SetActivityContext(base::Value activity_context) override = 0;
   void SetHostContext(base::Value host_context) override = 0;
 
+  // mojom::ActivityWindow implementation:
+  void Show() override;
+  void Hide() override;
+
   // Notify the window that its visibility type has changed. This should only
   // ever be called by the window manager.
   // TODO(seantopping): Make this private to the window manager.
@@ -114,6 +101,9 @@
   void BindReceiver(mojo::PendingReceiver<mojom::CastContentWindow> receiver);
 
  protected:
+  void BindActivityWindow(
+      mojo::PendingReceiver<mojom::ActivityWindow> receiver);
+
   // Camel case due to conflict with WebContentsObserver::web_contents().
   content::WebContents* WebContents() {
     return cast_web_contents() ? cast_web_contents()->web_contents() : nullptr;
@@ -123,8 +113,11 @@
   base::WeakPtr<Delegate> delegate_;
   mojom::CastWebViewParamsPtr params_;
 
+  GestureRouter gesture_router_;
   mojo::Receiver<mojom::CastContentWindow> receiver_{this};
+  mojo::Receiver<mojom::ActivityWindow> activity_window_receiver_{this};
   base::ObserverList<Observer> observer_list_;
+  base::WeakPtrFactory<CastContentWindow> weak_factory_{this};
 };
 
 }  // namespace chromecast
diff --git a/chromecast/browser/cast_content_window_aura.cc b/chromecast/browser/cast_content_window_aura.cc
index f69bef18d..8efd130 100644
--- a/chromecast/browser/cast_content_window_aura.cc
+++ b/chromecast/browser/cast_content_window_aura.cc
@@ -93,7 +93,7 @@
     : CastContentWindow(delegate, std::move(params)),
       window_manager_(window_manager),
       gesture_dispatcher_(
-          std::make_unique<CastContentGestureHandler>(delegate_)),
+          std::make_unique<CastContentGestureHandler>(gesture_router())),
       window_(nullptr),
       has_screen_access_(false),
       resize_window_when_navigation_starts_(true) {}
diff --git a/chromecast/browser/cast_web_contents.h b/chromecast/browser/cast_web_contents.h
index c79b720..3a0e0a8 100644
--- a/chromecast/browser/cast_web_contents.h
+++ b/chromecast/browser/cast_web_contents.h
@@ -18,6 +18,7 @@
 #include "chromecast/browser/mojom/cast_web_contents.mojom.h"
 #include "chromecast/browser/web_types.h"
 #include "chromecast/common/mojom/feature_manager.mojom.h"
+#include "chromecast/mojo/interface_bundle.h"
 #include "content/public/common/media_playback_renderer_type.mojom.h"
 #include "mojo/public/cpp/bindings/generic_pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
@@ -255,6 +256,9 @@
   // registry or any registered InterfaceProvider.
   virtual bool TryBindReceiver(mojo::GenericPendingReceiver& receiver) = 0;
 
+  // Locally-registered interfaces which are exposed to render frames.
+  virtual InterfaceBundle* local_interfaces() = 0;
+
   // Used for owner to pass its |InterfaceProvider| pointers to CastWebContents.
   // It is owner's responsibility to make sure each |InterfaceProvider| pointer
   // has distinct mojo interface set.
diff --git a/chromecast/browser/cast_web_contents_impl.cc b/chromecast/browser/cast_web_contents_impl.cc
index 833aca7..7486d64b 100644
--- a/chromecast/browser/cast_web_contents_impl.cc
+++ b/chromecast/browser/cast_web_contents_impl.cc
@@ -444,6 +444,11 @@
 
 bool CastWebContentsImpl::TryBindReceiver(
     mojo::GenericPendingReceiver& receiver) {
+  // First try binding local interfaces.
+  if (local_interfaces_.TryBindReceiver(receiver)) {
+    return true;
+  }
+
   const std::string interface_name = *receiver.interface_name();
   mojo::ScopedMessagePipeHandle interface_pipe = receiver.PassPipe();
 
@@ -468,6 +473,10 @@
   return true;
 }
 
+InterfaceBundle* CastWebContentsImpl::local_interfaces() {
+  return &local_interfaces_;
+}
+
 void CastWebContentsImpl::RegisterInterfaceProvider(
     const InterfaceSet& interface_set,
     service_manager::InterfaceProvider* interface_provider) {
diff --git a/chromecast/browser/cast_web_contents_impl.h b/chromecast/browser/cast_web_contents_impl.h
index 4b9dbe5..032dbe0 100644
--- a/chromecast/browser/cast_web_contents_impl.h
+++ b/chromecast/browser/cast_web_contents_impl.h
@@ -86,6 +86,7 @@
       const InterfaceSet& interface_set,
       service_manager::InterfaceProvider* interface_provider) override;
   bool TryBindReceiver(mojo::GenericPendingReceiver& receiver) override;
+  InterfaceBundle* local_interfaces() override;
   void BlockMediaLoading(bool blocked) override;
   void BlockMediaStarting(bool blocked) override;
   void EnableBackgroundVideoPlayback(bool enabled) override;
@@ -219,6 +220,7 @@
 
   base::ObserverList<CastWebContentsObserver>::Unchecked observer_list_;
 
+  InterfaceBundle local_interfaces_;
   RemoteInterfaces remote_interfaces_;
 
   const scoped_refptr<base::SequencedTaskRunner> task_runner_;
diff --git a/chromecast/browser/gesture_router.cc b/chromecast/browser/gesture_router.cc
new file mode 100644
index 0000000..67f10e5
--- /dev/null
+++ b/chromecast/browser/gesture_router.cc
@@ -0,0 +1,238 @@
+// 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 "chromecast/browser/gesture_router.h"
+
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "chromecast/base/chromecast_switches.h"
+
+namespace chromecast {
+
+GestureRouter::GestureRouter()
+    : gesture_source_receiver_(this),
+      handler_(nullptr),
+      can_go_back_(false),
+      can_top_drag_(false),
+      can_right_drag_(false),
+      managed_mode_(GetSwitchValueBoolean(switches::kManagedMode, false)),
+      weak_factory_(this) {}
+
+GestureRouter::~GestureRouter() = default;
+
+bool GestureRouter::CanHandleGesture(GestureType gesture_type) {
+  switch (gesture_type) {
+    case GestureType::GO_BACK:
+      return CanGoBack();
+    case GestureType::TAP:
+      return true;
+    case GestureType::TAP_DOWN:
+      return true;
+    case GestureType::TOP_DRAG:
+      return CanTopDrag();
+    case GestureType::RIGHT_DRAG:
+      return CanRightDrag();
+    default:
+      return false;
+  }
+}
+
+void GestureRouter::GestureProgress(GestureType gesture_type,
+                                    const gfx::Point& touch_location) {
+  switch (gesture_type) {
+    case GestureType::GO_BACK:
+      if (!CanGoBack())
+        return;
+      SendBackGestureProgress(touch_location);
+      break;
+    case GestureType::TOP_DRAG:
+      if (!CanTopDrag())
+        return;
+      SendTopDragGestureProgress(touch_location);
+      break;
+    case GestureType::RIGHT_DRAG:
+      if (!CanRightDrag())
+        return;
+      SendRightDragGestureProgress(touch_location);
+      break;
+    default:
+      return;
+  }
+}
+
+void GestureRouter::CancelGesture(GestureType gesture_type) {
+  switch (gesture_type) {
+    case GestureType::GO_BACK:
+      if (!CanGoBack())
+        return;
+      SendBackGestureCancel();
+      break;
+    default:
+      return;
+  }
+}
+
+void GestureRouter::ConsumeGesture(GestureType gesture_type,
+                                   GestureHandledCallback handled_callback) {
+  switch (gesture_type) {
+    case GestureType::NO_GESTURE:
+      std::move(handled_callback).Run(false);
+      break;
+    case GestureType::GO_BACK:
+      if (!CanGoBack()) {
+        std::move(handled_callback).Run(false);
+        return;
+      }
+      SendBackGesture(std::move(handled_callback));
+      break;
+    case GestureType::TAP:
+      SendTapGesture();
+      std::move(handled_callback).Run(true);
+      break;
+    case GestureType::TAP_DOWN:
+      SendTapDownGesture();
+      std::move(handled_callback).Run(true);
+      break;
+    case GestureType::TOP_DRAG:
+      SendTopDragGestureDone();
+      std::move(handled_callback).Run(true);
+      break;
+    case GestureType::RIGHT_DRAG:
+      SendRightDragGestureDone();
+      std::move(handled_callback).Run(true);
+      break;
+    default:
+      std::move(handled_callback).Run(false);
+  }
+}
+
+void GestureRouter::SetConsumeGestureCallback(ConsumerCallback callback) {
+  consumer_callback_ = callback;
+}
+
+void GestureRouter::SetHandler(mojom::GestureHandler* handler) {
+  handler_ = handler;
+}
+
+void GestureRouter::Subscribe(
+    mojo::PendingRemote<mojom::GestureHandler> pending_handler_remote) {
+  if (handler_remote_.is_bound()) {
+    handler_remote_.reset();
+  }
+  handler_remote_.Bind(std::move(pending_handler_remote));
+  SetHandler(handler_remote_.get());
+}
+
+void GestureRouter::SetCanGoBack(bool can_go_back) {
+  can_go_back_ = can_go_back;
+  if (delegate_)
+    delegate_->SetCanGoBack(can_go_back_);
+}
+
+bool GestureRouter::CanGoBack() const {
+  return can_go_back_ && (consumer_callback_ || handler_);
+}
+
+void GestureRouter::SendBackGesture(
+    base::OnceCallback<void(bool)> was_handled_callback) {
+  if (consumer_callback_) {
+    consumer_callback_.Run(GestureType::GO_BACK,
+                           std::move(was_handled_callback));
+    return;
+  }
+  if (!handler_)
+    return;
+  handler_->OnBackGesture(std::move(was_handled_callback));
+}
+
+void GestureRouter::SendBackGestureProgress(const gfx::Point& touch_location) {
+  if (!handler_)
+    return;
+  handler_->OnBackGestureProgress(touch_location);
+}
+
+void GestureRouter::SendBackGestureCancel() {
+  if (!handler_)
+    return;
+  handler_->OnBackGestureCancel();
+}
+
+void GestureRouter::SetCanTopDrag(bool can_top_drag) {
+  can_top_drag_ = can_top_drag;
+}
+
+bool GestureRouter::CanTopDrag() const {
+  return handler_ && can_top_drag_ && !managed_mode_;
+}
+
+void GestureRouter::SendTopDragGestureDone() {
+  if (!handler_)
+    return;
+  handler_->OnTopDragGestureDone();
+}
+
+void GestureRouter::SendTopDragGestureProgress(
+    const gfx::Point& touch_location) {
+  if (!handler_)
+    return;
+  handler_->OnTopDragGestureProgress(touch_location);
+}
+
+void GestureRouter::SetCanRightDrag(bool can_right_drag) {
+  can_right_drag_ = can_right_drag;
+}
+
+bool GestureRouter::CanRightDrag() const {
+  return handler_ && can_right_drag_;
+}
+
+void GestureRouter::SendRightDragGestureDone() {
+  if (!handler_)
+    return;
+  handler_->OnRightDragGestureDone();
+}
+
+void GestureRouter::SendRightDragGestureProgress(
+    const gfx::Point& touch_location) {
+  if (!handler_)
+    return;
+  handler_->OnRightDragGestureProgress(touch_location);
+}
+
+void GestureRouter::SendTapGesture() {
+  if (!handler_)
+    return;
+  handler_->OnTapGesture();
+}
+
+void GestureRouter::SendTapDownGesture() {
+  if (!handler_)
+    return;
+  handler_->OnTapDownGesture();
+}
+
+void GestureRouter::SetBackGestureDelegate(Delegate* delegate) {
+  delegate_ = delegate;
+  if (delegate_)
+    delegate_->SetCanGoBack(can_go_back_);
+}
+
+base::RepeatingCallback<void(mojo::PendingReceiver<mojom::GestureSource>)>
+GestureRouter::GetBinder() {
+  return base::BindRepeating(&GestureRouter::BindGestureSource,
+                             weak_factory_.GetWeakPtr());
+}
+
+void GestureRouter::BindGestureSource(
+    mojo::PendingReceiver<mojom::GestureSource> request) {
+  if (gesture_source_receiver_.is_bound()) {
+    LOG(WARNING) << "Gesture router is being re-bound.";
+    gesture_source_receiver_.reset();
+  }
+  gesture_source_receiver_.Bind(std::move(request));
+}
+
+}  // namespace chromecast
diff --git a/chromecast/browser/gesture_router.h b/chromecast/browser/gesture_router.h
new file mode 100644
index 0000000..b0a4999
--- /dev/null
+++ b/chromecast/browser/gesture_router.h
@@ -0,0 +1,110 @@
+// 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 CHROMECAST_BROWSER_GESTURE_ROUTER_H_
+#define CHROMECAST_BROWSER_GESTURE_ROUTER_H_
+
+#include "base/callback_forward.h"
+#include "base/memory/weak_ptr.h"
+#include "chromecast/browser/visibility_types.h"
+#include "chromecast/common/mojom/gesture.mojom.h"
+#include "chromecast/ui/back_gesture_router.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+
+namespace chromecast {
+
+// This class exposes Cast UI gesture hooks to JavaScript. The page notifies
+// its ability to handle specific gestures, which allows this class to perform
+// necessary routing.
+class GestureRouter : private mojom::GestureSource, public BackGestureRouter {
+ public:
+  GestureRouter();
+  GestureRouter(const GestureRouter&) = delete;
+  GestureRouter& operator=(const GestureRouter&) = delete;
+  ~GestureRouter() override;
+
+  using GestureHandledCallback = base::OnceCallback<void(bool)>;
+
+  using ConsumerCallback =
+      base::RepeatingCallback<void(GestureType, GestureHandledCallback)>;
+
+  // Check to see if the gesture can be handled by JS. This is called prior to
+  // ConsumeGesture().
+  bool CanHandleGesture(GestureType gesture_type);
+
+  // Called while a system UI gesture is in progress.
+  void GestureProgress(GestureType gesture_type,
+                       const gfx::Point& touch_location);
+
+  // Called when an in-progress system UI gesture is cancelled (for example
+  // when the finger is lifted before the completion of the gesture.)
+  void CancelGesture(GestureType gesture_type);
+
+  // Consume and handle a completed UI gesture. Invokes the callback with a
+  // boolean indicating whether the gesture was handled or not.
+  void ConsumeGesture(GestureType gesture_type,
+                      GestureHandledCallback handled_callback);
+
+  // Registers an in-process gesture consumer. If set, this supersedes the
+  // mojo handler path. This can be called multiple times, overriding a
+  // previously registered callback.
+  void SetConsumeGestureCallback(ConsumerCallback callback);
+
+  // mojom::GestureSource implementation:
+  void Subscribe(mojo::PendingRemote<mojom::GestureHandler>
+                     pending_handler_remote) override;
+  void SetCanGoBack(bool can_go_back) override;
+  void SetCanTopDrag(bool can_top_drag) override;
+  void SetCanRightDrag(bool can_right_drag) override;
+
+  bool CanGoBack() const;
+  void SendBackGesture(base::OnceCallback<void(bool)> was_handled_callback);
+  void SendBackGestureProgress(const gfx::Point& touch_location);
+  void SendBackGestureCancel();
+  bool CanTopDrag() const;
+  void SendTopDragGestureProgress(const gfx::Point& touch_location);
+  void SendTopDragGestureDone();
+  bool CanRightDrag() const;
+  void SendRightDragGestureProgress(const gfx::Point& touch_location);
+  void SendRightDragGestureDone();
+  void SendTapGesture();
+  void SendTapDownGesture();
+
+  // BackGestureRouter overrides:
+  void SetBackGestureDelegate(Delegate* delegate) override;
+
+  base::RepeatingCallback<void(mojo::PendingReceiver<mojom::GestureSource>)>
+  GetBinder();
+
+ private:
+  friend class CastContentGestureHandlerTest;
+  friend class CastContentWindowEmbeddedTest;
+
+  // Helper method for unit tests. Instead of having to register an async
+  // subscriber, tests can use SetHandler() to register a mock instead. This
+  // can be called multiple times.
+  void SetHandler(mojom::GestureHandler* handler);
+
+  void BindGestureSource(mojo::PendingReceiver<mojom::GestureSource> request);
+
+  mojo::Receiver<mojom::GestureSource> gesture_source_receiver_;
+  mojo::Remote<mojom::GestureHandler> handler_remote_;
+  mojom::GestureHandler* handler_;
+
+  bool can_go_back_;
+  bool can_top_drag_;
+  bool can_right_drag_;
+  bool managed_mode_;  // If in managed mode, disable top drag.
+  Delegate* delegate_ = nullptr;
+
+  ConsumerCallback consumer_callback_;
+
+  base::WeakPtrFactory<GestureRouter> weak_factory_;
+};
+
+}  // namespace chromecast
+
+#endif  // CHROMECAST_BROWSER_GESTURE_ROUTER_H_
diff --git a/chromecast/browser/service/cast_service_simple.cc b/chromecast/browser/service/cast_service_simple.cc
index 2a08934..276915f 100644
--- a/chromecast/browser/service/cast_service_simple.cc
+++ b/chromecast/browser/service/cast_service_simple.cc
@@ -90,16 +90,6 @@
 
 void CastServiceSimple::OnWindowDestroyed() {}
 
-bool CastServiceSimple::CanHandleGesture(GestureType gesture_type) {
-  return false;
-}
-
-void CastServiceSimple::ConsumeGesture(
-    GestureType gesture_type,
-    GestureHandledCallback handled_callback) {
-  std::move(handled_callback).Run(false);
-}
-
 void CastServiceSimple::OnVisibilityChange(VisibilityType visibility_type) {}
 
 }  // namespace shell
diff --git a/chromecast/browser/service/cast_service_simple.h b/chromecast/browser/service/cast_service_simple.h
index c1d70bd..f3d64aef 100644
--- a/chromecast/browser/service/cast_service_simple.h
+++ b/chromecast/browser/service/cast_service_simple.h
@@ -45,9 +45,6 @@
 
   // CastContentWindow::Delegate implementation:
   void OnWindowDestroyed() override;
-  bool CanHandleGesture(GestureType gesture_type) override;
-  void ConsumeGesture(GestureType gesture_type,
-                      GestureHandledCallback handled_callback) override;
   void OnVisibilityChange(VisibilityType visibility_type) override;
 
  private:
diff --git a/chromecast/browser/test/cast_browser_test.cc b/chromecast/browser/test/cast_browser_test.cc
index e9cb887..a3c7046 100644
--- a/chromecast/browser/test/cast_browser_test.cc
+++ b/chromecast/browser/test/cast_browser_test.cc
@@ -90,14 +90,5 @@
 
 void CastBrowserTest::OnVisibilityChange(VisibilityType visibility_type) {}
 
-bool CastBrowserTest::CanHandleGesture(GestureType gesture_type) {
-  return false;
-}
-
-void CastBrowserTest::ConsumeGesture(GestureType gesture_type,
-                                     GestureHandledCallback handled_callback) {
-  std::move(handled_callback).Run(false);
-}
-
 }  // namespace shell
 }  // namespace chromecast
diff --git a/chromecast/browser/test/cast_browser_test.h b/chromecast/browser/test/cast_browser_test.h
index 3e494c85..5fc8f36 100644
--- a/chromecast/browser/test/cast_browser_test.h
+++ b/chromecast/browser/test/cast_browser_test.h
@@ -53,9 +53,6 @@
   // CastWebView::Delegate implementation:
   void OnWindowDestroyed() override;
   void OnVisibilityChange(VisibilityType visibility_type) override;
-  bool CanHandleGesture(GestureType gesture_type) override;
-  void ConsumeGesture(GestureType gesture_type,
-                      GestureHandledCallback handled_callback) override;
 
   std::unique_ptr<CastWebViewFactory> web_view_factory_;
   std::unique_ptr<CastWebService> web_service_;
diff --git a/chromecast/browser/test/mock_cast_content_window_delegate.h b/chromecast/browser/test/mock_cast_content_window_delegate.h
index 4c0b157..ce5b1f988 100644
--- a/chromecast/browser/test/mock_cast_content_window_delegate.h
+++ b/chromecast/browser/test/mock_cast_content_window_delegate.h
@@ -18,14 +18,8 @@
   MockCastContentWindowDelegate();
   ~MockCastContentWindowDelegate() override;
 
-  MOCK_METHOD1(CanHandleGesture, bool(GestureType gesture_type));
-  MOCK_METHOD2(ConsumeGesture,
-               void(GestureType gesture_type,
-                    GestureHandledCallback handled_callback));
-  MOCK_METHOD1(CancelGesture, void(GestureType gesture_type));
-  MOCK_METHOD2(GestureProgress,
-               void(GestureType gesture_type,
-                    const gfx::Point& touch_location));
+  MOCK_METHOD(void, OnWindowDestroyed, (), (override));
+  MOCK_METHOD(void, OnVisibilityChange, (VisibilityType), (override));
 };
 
 }  // namespace chromecast
diff --git a/chromecast/browser/test/mock_cast_web_view.h b/chromecast/browser/test/mock_cast_web_view.h
index e998db15..4563d69 100644
--- a/chromecast/browser/test/mock_cast_web_view.h
+++ b/chromecast/browser/test/mock_cast_web_view.h
@@ -64,6 +64,7 @@
               (override));
   MOCK_METHOD(void, SetEnabledForRemoteDebugging, (bool), (override));
   MOCK_METHOD(void, GetMainFramePid, (GetMainFramePidCallback), (override));
+  MOCK_METHOD(InterfaceBundle*, local_interfaces, (), (override));
   MOCK_METHOD(void,
               RegisterInterfaceProvider,
               (const InterfaceSet&, service_manager::InterfaceProvider*),
diff --git a/chromecast/browser/webview/cast_content_window_embedded.cc b/chromecast/browser/webview/cast_content_window_embedded.cc
index 9f72bcf..a5c179a 100644
--- a/chromecast/browser/webview/cast_content_window_embedded.cc
+++ b/chromecast/browser/webview/cast_content_window_embedded.cc
@@ -163,8 +163,8 @@
 
   if (request.navigation && request.navigation.value() ==
                                 CastWindowEmbedder::NavigationType::GO_BACK) {
-    if (delegate_ && delegate_->CanHandleGesture(GestureType::GO_BACK)) {
-      delegate_->ConsumeGesture(
+    if (gesture_router()->CanHandleGesture(GestureType::GO_BACK)) {
+      gesture_router()->ConsumeGesture(
           GestureType::GO_BACK,
           base::BindOnce(&CastContentWindowEmbedded::ConsumeGestureCompleted,
                          base::Unretained(this)));
@@ -201,17 +201,18 @@
   }
 
   if (request.back_gesture_progress_event) {
-    if (delegate_ && delegate_->CanHandleGesture(GestureType::GO_BACK))
-      delegate_->GestureProgress(
+    if (gesture_router()->CanHandleGesture(GestureType::GO_BACK)) {
+      gesture_router()->GestureProgress(
           GestureType::GO_BACK,
           gfx::Point(request.back_gesture_progress_event.value().x,
                      request.back_gesture_progress_event.value().y));
+    }
     return;
   }
 
   if (request.back_gesture_cancel_event) {
-    if (delegate_ && delegate_->CanHandleGesture(GestureType::GO_BACK))
-      delegate_->CancelGesture(GestureType::GO_BACK);
+    if (gesture_router()->CanHandleGesture(GestureType::GO_BACK))
+      gesture_router()->CancelGesture(GestureType::GO_BACK);
     return;
   }
 }
diff --git a/chromecast/browser/webview/cast_content_window_embedded_unittest.cc b/chromecast/browser/webview/cast_content_window_embedded_unittest.cc
index 95014353..dee0064 100644
--- a/chromecast/browser/webview/cast_content_window_embedded_unittest.cc
+++ b/chromecast/browser/webview/cast_content_window_embedded_unittest.cc
@@ -125,18 +125,17 @@
       .Times(1)
       .WillOnce(Return(fake_window_id /* window_id */));
 
-  EXPECT_CALL(*mock_cast_content_window_delegate_,
-              CanHandleGesture(GestureType::GO_BACK))
-      .Times(1)
-      .WillOnce(Return(true));
+  cast_content_window_embedded_ = std::make_unique<CastContentWindowEmbedded>(
+      mock_cast_content_window_delegate_->AsWeakPtr(), GenerateParams(),
+      cast_window_embedder_.get(), false /* force_720p_resolution */);
 
-  // Delegate of the CastContentWindow indicts that it has handled the event.
-  EXPECT_CALL(*mock_cast_content_window_delegate_, ConsumeGesture(_, _))
-      .Times(1)
-      .WillOnce([](GestureType gesture_type,
-                   base::OnceCallback<void(bool)> consume_gesture_completed) {
-        std::move(consume_gesture_completed).Run(true);
-      });
+  cast_content_window_embedded_->gesture_router()->SetCanGoBack(true);
+  cast_content_window_embedded_->gesture_router()->SetConsumeGestureCallback(
+      base::BindRepeating(
+          [](GestureType gesture_type,
+             base::OnceCallback<void(bool)> consume_gesture_completed) {
+            std::move(consume_gesture_completed).Run(true);
+          }));
 
   // CastWindowEmbedder shall receive the ack of event handling
   EXPECT_CALL(
@@ -145,10 +144,6 @@
           fake_window_id, _, true, CastWindowEmbedder::NavigationType::GO_BACK))
       .Times(1);
 
-  cast_content_window_embedded_ = std::make_unique<CastContentWindowEmbedded>(
-      mock_cast_content_window_delegate_->AsWeakPtr(), GenerateParams(),
-      cast_window_embedder_.get(), false /* force_720p_resolution */);
-
   CastWindowEmbedder::EmbedderWindowEvent window_event;
   window_event.window_id = 1;
   window_event.navigation = CastWindowEmbedder::NavigationType::GO_BACK;
@@ -167,19 +162,20 @@
       .Times(1)
       .WillOnce(Return(fake_window_id /* window_id */));
 
-  EXPECT_CALL(*mock_cast_content_window_delegate_,
-              CanHandleGesture(GestureType::GO_BACK))
-      .Times(1)
-      .WillOnce(Return(true));
+  cast_content_window_embedded_ = std::make_unique<CastContentWindowEmbedded>(
+      mock_cast_content_window_delegate_->AsWeakPtr(), GenerateParams(),
+      cast_window_embedder_.get(), false /* force_720p_resolution */);
+
+  cast_content_window_embedded_->gesture_router()->SetCanGoBack(true);
 
   // Delegate of the CastContentWindow indicts that it dit not handle the
   // event.
-  EXPECT_CALL(*mock_cast_content_window_delegate_, ConsumeGesture(_, _))
-      .Times(1)
-      .WillOnce([](GestureType gesture_type,
-                   base::OnceCallback<void(bool)> consume_gesture_completed) {
-        std::move(consume_gesture_completed).Run(false /* handled */);
-      });
+  cast_content_window_embedded_->gesture_router()->SetConsumeGestureCallback(
+      base::BindRepeating(
+          [](GestureType gesture_type,
+             base::OnceCallback<void(bool)> consume_gesture_completed) {
+            std::move(consume_gesture_completed).Run(false);
+          }));
 
   // CastWindowEmbedder shall receive the ack of event handling
   EXPECT_CALL(*cast_window_embedder_,
@@ -188,10 +184,6 @@
                   CastWindowEmbedder::NavigationType::GO_BACK))
       .Times(1);
 
-  cast_content_window_embedded_ = std::make_unique<CastContentWindowEmbedded>(
-      mock_cast_content_window_delegate_->AsWeakPtr(), GenerateParams(),
-      cast_window_embedder_.get(), false /* force_720p_resolution */);
-
   CastWindowEmbedder::EmbedderWindowEvent window_event;
   window_event.window_id = 1;
   window_event.navigation = CastWindowEmbedder::NavigationType::GO_BACK;
@@ -210,15 +202,11 @@
       .Times(1)
       .WillOnce(Return(fake_window_id /* window_id */));
 
-  EXPECT_CALL(*mock_cast_content_window_delegate_,
-              CanHandleGesture(GestureType::GO_BACK))
-      .Times(1)
-      .WillOnce(Return(false));
+  cast_content_window_embedded_ = std::make_unique<CastContentWindowEmbedded>(
+      mock_cast_content_window_delegate_->AsWeakPtr(), GenerateParams(),
+      cast_window_embedder_.get(), false /* force_720p_resolution */);
 
-  // Delegate of the CastContentWindow indicts that it dit not handled the
-  // event.
-  EXPECT_CALL(*mock_cast_content_window_delegate_, ConsumeGesture(_, _))
-      .Times(0);
+  cast_content_window_embedded_->gesture_router()->SetCanGoBack(false);
 
   // CastWindowEmbedder shall receive the ack of event handling
   EXPECT_CALL(*cast_window_embedder_,
@@ -227,10 +215,6 @@
                   CastWindowEmbedder::NavigationType::GO_BACK))
       .Times(1);
 
-  cast_content_window_embedded_ = std::make_unique<CastContentWindowEmbedded>(
-      mock_cast_content_window_delegate_->AsWeakPtr(), GenerateParams(),
-      cast_window_embedder_.get(), false /* force_720p_resolution */);
-
   CastWindowEmbedder::EmbedderWindowEvent window_event;
   window_event.window_id = 1;
   window_event.navigation = CastWindowEmbedder::NavigationType::GO_BACK;
diff --git a/chromecast/cast_core/runtime_application_base.cc b/chromecast/cast_core/runtime_application_base.cc
index 7c2d08e..8f6d425 100644
--- a/chromecast/cast_core/runtime_application_base.cc
+++ b/chromecast/cast_core/runtime_application_base.cc
@@ -158,10 +158,6 @@
   return web_service_->CreateWebViewInternal(create_params, std::move(params));
 }
 
-bool RuntimeApplicationBase::CanHandleGesture(GestureType gesture_type) {
-  return false;
-}
-
 void RuntimeApplicationBase::StopApplication() {
   is_application_stopped_ = true;
 
@@ -194,10 +190,4 @@
   set_cast_session_id(std::string());
 }
 
-void RuntimeApplicationBase::ConsumeGesture(
-    GestureType gesture_type,
-    GestureHandledCallback handled_callback) {
-  std::move(handled_callback).Run(false);
-}
-
 }  // namespace chromecast
diff --git a/chromecast/cast_core/runtime_application_base.h b/chromecast/cast_core/runtime_application_base.h
index 6fb5c8e..56dde1b 100644
--- a/chromecast/cast_core/runtime_application_base.h
+++ b/chromecast/cast_core/runtime_application_base.h
@@ -80,11 +80,6 @@
   // Called following Launch() on |task_runner_|.
   void FinishLaunch(std::string core_application_service_endpoint);
 
-  // CastWebView::Delegate implementation:
-  bool CanHandleGesture(GestureType gesture_type) override;
-  void ConsumeGesture(GestureType gesture_type,
-                      GestureHandledCallback handled_callback) override;
-
   // RuntimeMessagePortApplicationServiceDelegate implementation:
   void PostMessage(const cast::web::Message& request,
                    cast::web::MessagePortStatus* response,
diff --git a/chromecast/common/feature_constants.cc b/chromecast/common/feature_constants.cc
index b4ff4883..b4a8e17 100644
--- a/chromecast/common/feature_constants.cc
+++ b/chromecast/common/feature_constants.cc
@@ -10,6 +10,17 @@
 const char kEnableTrackControlAppRendererFeatureUse[] =
     "track_control_renderer_feature_use";
 const char kEnablePlayready[] = "playready";
+const char kEnableDevMode[] = "dev_mode";
+const char kDevModeOrigin[] = "dev_mode_origin";
+const char kEnableAccessibilityControls[] = "accessibility_controls";
+const char kEnableSystemGestures[] = "system_gestures";
+const char kEnableWindowControls[] = "enable_window_controls";
+const char kEnableSettingsUiMojo[] = "enable_settings_ui_mojo";
+const char kDisableBackgroundTabTimerThrottle[] =
+    "disable_background_tab_timer_throttle";
+const char kDisableBackgroundSuspend[] = "disable_background_suspend";
+const char kEnableAssistantMessagePipe[] = "enable_assistant_message_pipe";
+const char kEnableDemoStandaloneMode[] = "enable_demo_standalone_mode";
 
 const char kKeyAppId[] = "app_id";
 const char kKeyAllowInsecureContent[] = "allow_insecure_content";
diff --git a/chromecast/common/feature_constants.h b/chromecast/common/feature_constants.h
index 2b821574..1e667c9d 100644
--- a/chromecast/common/feature_constants.h
+++ b/chromecast/common/feature_constants.h
@@ -20,6 +20,37 @@
 // Insecure content is allowed for the app.
 extern const char kKeyAllowInsecureContent[];
 
+// If dev mode is enabled, kDevModeOrigin will be set with origin url
+extern const char kEnableDevMode[];
+extern const char kDevModeOrigin[];
+
+// Permit changes to accessibility control settings?
+// (color inversion, high contrast text, etc.)
+extern const char kEnableAccessibilityControls[];
+
+// Permit subscription to platform system gesture events?
+extern const char kEnableSystemGestures[];
+
+// Enables window APIs for the webpage (show/hide, etc.)
+extern const char kEnableWindowControls[];
+
+// Enable mojo connection for the settings UI.
+extern const char kEnableSettingsUiMojo[];
+
+// Disable blink background tab timer throttling for making sure application in
+// in mini tile mode runs smoothly.
+extern const char kDisableBackgroundTabTimerThrottle[];
+
+// Sets RenderFrameMediaPlaybackOptions::kIsBackgroundMediaSuspendEnabled to
+// false.
+extern const char kDisableBackgroundSuspend[];
+
+// Enable sending/receiving messages to/from libassistant
+extern const char kEnableAssistantMessagePipe[];
+
+// Enable a standalone demo app to control privileged features.
+extern const char kEnableDemoStandaloneMode[];
+
 }  // namespace feature
 }  // namespace chromecast
 
diff --git a/chromecast/mojo/interface_bundle.h b/chromecast/mojo/interface_bundle.h
index 94a2fec4..20afa1f 100644
--- a/chromecast/mojo/interface_bundle.h
+++ b/chromecast/mojo/interface_bundle.h
@@ -12,6 +12,7 @@
 #include "base/sequence_checker.h"
 #include "chromecast/mojo/binder_factory.h"
 #include "chromecast/mojo/mojom/remote_interfaces.mojom.h"
+#include "mojo/public/cpp/bindings/generic_pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/receiver_set.h"
 #include "mojo/public/cpp/bindings/remote.h"
@@ -123,6 +124,17 @@
   void AddClient(
       mojo::PendingReceiver<mojom::RemoteInterfaces> receiver) override;
 
+  // Attempt to bind a generic receiver. Succeeds if there is an available
+  // implementation or binder callback registered.
+  bool TryBindReceiver(mojo::GenericPendingReceiver& receiver) {
+    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+    if (local_interfaces_.HasInterface(*receiver.interface_name())) {
+      local_interfaces_.Bind(*receiver.interface_name(), receiver.PassPipe());
+      return true;
+    }
+    return false;
+  }
+
  private:
   // For interfaces that are provided as a local pointer without any binding
   // logic, we can use MultiBinderFactory to expose a binding surface.
diff --git a/chromecast/renderer/BUILD.gn b/chromecast/renderer/BUILD.gn
index 41454ad..07344599 100644
--- a/chromecast/renderer/BUILD.gn
+++ b/chromecast/renderer/BUILD.gn
@@ -36,16 +36,26 @@
   sources = [
     "activity_filtering_websocket_handshake_throttle.cc",
     "activity_filtering_websocket_handshake_throttle.h",
+    "assistant_bindings.cc",
+    "assistant_bindings.h",
+    "cast_accessibility_bindings.cc",
+    "cast_accessibility_bindings.h",
     "cast_activity_url_filter_manager.cc",
     "cast_activity_url_filter_manager.h",
     "cast_content_renderer_client.cc",
     "cast_content_renderer_client.h",
     "cast_content_settings_client.cc",
     "cast_content_settings_client.h",
+    "cast_demo_bindings.cc",
+    "cast_demo_bindings.h",
     "cast_url_loader_throttle_provider.cc",
     "cast_url_loader_throttle_provider.h",
     "cast_websocket_handshake_throttle_provider.cc",
     "cast_websocket_handshake_throttle_provider.h",
+    "cast_window_manager_bindings.cc",
+    "cast_window_manager_bindings.h",
+    "feature_manager.cc",
+    "feature_manager.h",
     "feature_manager_on_associated_interface.cc",
     "feature_manager_on_associated_interface.h",
     "identification_settings_manager_renderer.cc",
@@ -57,10 +67,13 @@
     "native_bindings_helper.h",
     "queryable_data_store.cc",
     "queryable_data_store.h",
+    "settings_ui_bindings.cc",
+    "settings_ui_bindings.h",
   ]
 
   public_deps = [
     "//chromecast:chromecast_buildflags",
+    "//chromecast/browser/mojom",
     "//chromecast/common/mojom",
     "//content/public/renderer",
     "//media",
diff --git a/chromecast/renderer/DEPS b/chromecast/renderer/DEPS
index 8610ff4..592c935 100644
--- a/chromecast/renderer/DEPS
+++ b/chromecast/renderer/DEPS
@@ -1,4 +1,5 @@
 include_rules = [
+  "+chromecast/browser/mojom",
   "+chromecast/common",
   "+chromecast/crash",
   "+chromecast/media",
diff --git a/chromecast/renderer/assistant_bindings.cc b/chromecast/renderer/assistant_bindings.cc
new file mode 100644
index 0000000..55484f70
--- /dev/null
+++ b/chromecast/renderer/assistant_bindings.cc
@@ -0,0 +1,169 @@
+// 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 "chromecast/renderer/assistant_bindings.h"
+
+#include "base/check.h"
+#include "base/json/json_reader.h"
+#include "base/json/json_writer.h"
+#include "base/logging.h"
+#include "base/stl_util.h"
+#include "base/time/time.h"
+#include "content/public/renderer/render_frame.h"
+#include "content/public/renderer/render_thread.h"
+#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
+#include "third_party/blink/public/web/blink.h"
+#include "third_party/blink/public/web/web_local_frame.h"
+
+namespace chromecast {
+namespace shell {
+
+namespace {
+
+const int kMaxMessageQueueSize = 50;
+static const int64_t kDelayBetweenReconnectionInMillis = 100;
+
+const char kSetAssistantMessageHandlerMethodName[] =
+    "setAssistantMessageHandler";
+const char kSendAssistantRequestMethodName[] = "sendAssistantRequest";
+
+}  // namespace
+
+AssistantBindings::AssistantBindings(content::RenderFrame* frame,
+                                     const base::Value& feature_config)
+    : CastBinding(frame),
+      feature_config_(feature_config.Clone()),
+      message_client_binding_(this),
+      weak_factory_(this) {
+  weak_this_ = weak_factory_.GetWeakPtr();
+}
+
+AssistantBindings::~AssistantBindings() {}
+
+void AssistantBindings::OnMessage(base::Value message) {
+  if (assistant_message_handler_.IsEmpty()) {
+    return;
+  }
+
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::MicrotasksScope microtasks_scope(
+      isolate, v8::MicrotasksScope::kDoNotRunMicrotasks);
+  v8::HandleScope handle_scope(isolate);
+  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
+  v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
+  v8::Context::Scope context_scope(context);
+
+  v8::Local<v8::Function> handler = v8::Local<v8::Function>::New(
+      isolate, std::move(assistant_message_handler_));
+
+  std::string json;
+  base::JSONWriter::Write(message, &json);
+  v8::Local<v8::Value> message_val =
+      gin::Converter<std::string>::ToV8(isolate, json);
+
+  v8::Local<v8::Value> argv[] = {message_val};
+  web_frame->CallFunctionEvenIfScriptDisabled(handler, context->Global(),
+                                              base::size(argv), argv);
+
+  assistant_message_handler_ =
+      v8::UniquePersistent<v8::Function>(isolate, handler);
+}
+
+void AssistantBindings::Install(v8::Local<v8::Object> cast_platform,
+                                v8::Isolate* isolate) {
+  DVLOG(1) << "Installing AssistantBindings";
+
+  InstallBinding(isolate, cast_platform, kSetAssistantMessageHandlerMethodName,
+                 &AssistantBindings::SetAssistantMessageHandler,
+                 base::Unretained(this));
+  InstallBinding(isolate, cast_platform, kSendAssistantRequestMethodName,
+                 &AssistantBindings::SendAssistantRequest,
+                 base::Unretained(this));
+}
+
+void AssistantBindings::SetAssistantMessageHandler(
+    v8::Local<v8::Function> assistant_message_handler) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  assistant_message_handler_ =
+      v8::UniquePersistent<v8::Function>(isolate, assistant_message_handler);
+  ReconnectMessagePipe();
+}
+
+void AssistantBindings::SendAssistantRequest(const std::string& request) {
+  if (assistant_message_handler_.IsEmpty()) {
+    v8::Isolate* isolate = blink::MainThreadIsolate();
+    isolate->ThrowException(
+        v8::String::NewFromUtf8(isolate,
+                                "Error: assistant message handler is not set.",
+                                v8::NewStringType::kInternalized)
+            .ToLocalChecked());
+    return;
+  }
+  if (!message_pipe_.is_bound()) {
+    if (v8_to_assistant_queue_.size() < kMaxMessageQueueSize) {
+      v8_to_assistant_queue_.push_back(request);
+    } else {
+      LOG(WARNING) << "Messages sending to assistant overflow, will drop "
+                      "upcoming messages";
+    }
+    return;
+  }
+
+  v8_to_assistant_queue_.push_back(request);
+  FlushV8ToAssistantQueue();
+}
+
+void AssistantBindings::ReconnectMessagePipe() {
+  if (message_client_binding_.is_bound())
+    message_client_binding_.reset();
+  if (message_pipe_.is_bound())
+    message_pipe_.reset();
+  LOG(INFO) << "Creating message pipe";
+  base::Value* app_id =
+      feature_config_.FindKeyOfType("app_id", base::Value::Type::STRING);
+  DCHECK(app_id) << "Couldn't get app_id from feature config";
+  GetMojoInterface()->CreateMessagePipe(
+      app_id->GetString(), message_client_binding_.BindNewPipeAndPassRemote(),
+      message_pipe_.BindNewPipeAndPassReceiver());
+
+  reconnect_assistant_timer_.Stop();
+}
+
+void AssistantBindings::OnAssistantConnectionError() {
+  LOG(WARNING) << "Disconnected from assistant. Will reconnect every "
+               << kDelayBetweenReconnectionInMillis << " milliseconds";
+  assistant_.reset();
+  reconnect_assistant_timer_.Start(
+      FROM_HERE,
+      base::TimeDelta::FromMilliseconds(kDelayBetweenReconnectionInMillis),
+      this, &AssistantBindings::ReconnectMessagePipe);
+}
+
+void AssistantBindings::FlushV8ToAssistantQueue() {
+  DCHECK(message_pipe_.is_bound());
+
+  for (auto& request : v8_to_assistant_queue_) {
+    auto value = base::JSONReader::Read(request);
+    if (!value) {
+      LOG(ERROR) << "Unable to parse Assistant message JSON.";
+      continue;
+    }
+    message_pipe_->SendMessage(std::move(*value));
+  }
+  v8_to_assistant_queue_.clear();
+}
+
+const mojo::Remote<chromecast::mojom::AssistantMessageService>&
+AssistantBindings::GetMojoInterface() {
+  if (!assistant_.is_bound()) {
+    render_frame()->GetBrowserInterfaceBroker()->GetInterface(
+        assistant_.BindNewPipeAndPassReceiver());
+    assistant_.set_disconnect_handler(base::BindOnce(
+        &AssistantBindings::OnAssistantConnectionError, weak_this_));
+  }
+  return assistant_;
+}
+
+}  // namespace shell
+}  // namespace chromecast
diff --git a/chromecast/renderer/assistant_bindings.h b/chromecast/renderer/assistant_bindings.h
new file mode 100644
index 0000000..d243d92
--- /dev/null
+++ b/chromecast/renderer/assistant_bindings.h
@@ -0,0 +1,74 @@
+// 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 CHROMECAST_RENDERER_ASSISTANT_BINDINGS_H_
+#define CHROMECAST_RENDERER_ASSISTANT_BINDINGS_H_
+
+#include <string>
+#include <vector>
+
+#include "base/memory/weak_ptr.h"
+#include "base/timer/timer.h"
+#include "chromecast/common/mojom/assistant_messenger.mojom.h"
+#include "chromecast/renderer/native_bindings_helper.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+
+namespace chromecast {
+namespace shell {
+
+// When enabled, these bindings can be used to open a message channel with the
+// Assistant. These bindings are only enabled for a small set of first-party
+// apps.
+class AssistantBindings : public CastBinding,
+                          public chromecast::mojom::AssistantMessageClient {
+ public:
+  AssistantBindings(content::RenderFrame* frame,
+                    const base::Value& feature_config);
+  ~AssistantBindings() override;
+  AssistantBindings(const AssistantBindings&) = delete;
+  AssistantBindings& operator=(const AssistantBindings&) = delete;
+
+ private:
+  friend class ::chromecast::CastBinding;
+
+  // chromecast::mojom::AssistantMessageClient implementation:
+  void OnMessage(base::Value message) override;
+
+  // CastBinding implementation:
+  void Install(v8::Local<v8::Object> cast_platform,
+               v8::Isolate* isolate) override;
+
+  // Binding methods
+  void SetAssistantMessageHandler(
+      v8::Local<v8::Function> assistant_message_handler);
+  void SendAssistantRequest(const std::string& request);
+
+  void ReconnectMessagePipe();
+  void OnAssistantConnectionError();
+
+  void FlushV8ToAssistantQueue();
+
+  const mojo::Remote<chromecast::mojom::AssistantMessageService>&
+  GetMojoInterface();
+
+  base::RepeatingTimer reconnect_assistant_timer_;
+  mojo::Remote<chromecast::mojom::AssistantMessageService> assistant_;
+  base::Value feature_config_;
+
+  mojo::Receiver<chromecast::mojom::AssistantMessageClient>
+      message_client_binding_;
+  mojo::Remote<chromecast::mojom::AssistantMessagePipe> message_pipe_;
+  std::vector<std::string> v8_to_assistant_queue_;
+
+  v8::UniquePersistent<v8::Function> assistant_message_handler_;
+
+  base::WeakPtr<AssistantBindings> weak_this_;
+  base::WeakPtrFactory<AssistantBindings> weak_factory_;
+};
+
+}  // namespace shell
+}  // namespace chromecast
+
+#endif  // CHROMECAST_RENDERER_ASSISTANT_BINDINGS_H_
diff --git a/chromecast/renderer/cast_accessibility_bindings.cc b/chromecast/renderer/cast_accessibility_bindings.cc
new file mode 100644
index 0000000..cbe9791
--- /dev/null
+++ b/chromecast/renderer/cast_accessibility_bindings.cc
@@ -0,0 +1,243 @@
+// 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 "chromecast/renderer/cast_accessibility_bindings.h"
+
+#include <vector>
+
+#include "base/bind.h"
+#include "base/logging.h"
+#include "chromecast/common/feature_constants.h"
+#include "chromecast/renderer/feature_manager.h"
+#include "content/public/renderer/render_frame.h"
+#include "gin/data_object_builder.h"
+#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
+#include "third_party/blink/public/web/blink.h"
+#include "third_party/blink/public/web/web_local_frame.h"
+
+namespace chromecast {
+namespace shell {
+
+namespace {
+
+constexpr char kBindingsObjectName[] = "accessibility";
+constexpr char kSetColorInversionName[] = "setColorInversion";
+constexpr char kSetScreenReaderName[] = "setScreenReader";
+constexpr char kSetMagnificationGestureName[] = "setMagnificationGesture";
+constexpr char kGetAccessibilitySettings[] = "getAccessibilitySettings";
+constexpr char kIsColorInversionEnabled[] = "isColorInversionEnabled";
+constexpr char kIsScreenReaderEnabled[] = "isScreenReaderEnabled";
+constexpr char kIsMagnificationGestureEnabled[] =
+    "isMagnificationGestureEnabled";
+constexpr char kScreenReaderSettingChangedHandler[] =
+    "setScreenReaderSettingChangedHandler";
+constexpr char kColorInversionSettingChangedHandler[] =
+    "setColorInversionSettingChangedHandler";
+constexpr char kMagnificationGestureSettingChangedHandler[] =
+    "setMagnificationGestureSettingChangedHandler";
+
+void OnAccessibilityServiceConnectionError() {
+  LOG(ERROR) << "Connection error talking to accessibility manager.";
+}
+
+}  // namespace
+
+CastAccessibilityBindings::CastAccessibilityBindings(
+    content::RenderFrame* render_frame,
+    const FeatureManager* feature_manager)
+    : CastBinding(render_frame), feature_manager_(feature_manager) {
+  registry_.AddInterface(base::BindRepeating(
+      &CastAccessibilityBindings::OnCastAccessibilityClientRequest,
+      base::Unretained(this)));
+}
+
+CastAccessibilityBindings::~CastAccessibilityBindings() {}
+
+void CastAccessibilityBindings::OnInterfaceRequestForFrame(
+    const std::string& interface_name,
+    mojo::ScopedMessagePipeHandle* interface_pipe) {
+  registry_.TryBindInterface(interface_name, interface_pipe);
+}
+
+void CastAccessibilityBindings::Install(v8::Local<v8::Object> cast_platform,
+                                        v8::Isolate* isolate) {
+  v8::Local<v8::Object> AccessibilityObject =
+      EnsureObjectExists(isolate, cast_platform, kBindingsObjectName);
+
+  if (feature_manager_->FeatureEnabled(feature::kEnableAccessibilityControls)) {
+    InstallBinding(isolate, AccessibilityObject, kSetColorInversionName,
+                   &CastAccessibilityBindings::SetColorInversion,
+                   base::Unretained(this));
+    InstallBinding(isolate, AccessibilityObject, kSetScreenReaderName,
+                   &CastAccessibilityBindings::SetScreenReader,
+                   base::Unretained(this));
+    InstallBinding(isolate, AccessibilityObject, kSetMagnificationGestureName,
+                   &CastAccessibilityBindings::SetMagnificationGesture,
+                   base::Unretained(this));
+  }
+  // Getters and callback are always available.
+  InstallBinding(isolate, AccessibilityObject, kGetAccessibilitySettings,
+                 &CastAccessibilityBindings::GetAccessibilitySettings,
+                 base::Unretained(this));
+  InstallBinding(
+      isolate, AccessibilityObject, kScreenReaderSettingChangedHandler,
+      &CastAccessibilityBindings::SetScreenReaderSettingChangedHandler,
+      base::Unretained(this));
+  InstallBinding(
+      isolate, AccessibilityObject, kColorInversionSettingChangedHandler,
+      &CastAccessibilityBindings::SetColorInversionSettingChangedHandler,
+      base::Unretained(this));
+  InstallBinding(
+      isolate, AccessibilityObject, kMagnificationGestureSettingChangedHandler,
+      &CastAccessibilityBindings::SetMagnificationGestureSettingChangedHandler,
+      base::Unretained(this));
+}
+
+void CastAccessibilityBindings::SetColorInversion(bool enable) {
+  if (!BindAccessibility())
+    return;
+  accessibility_service_->SetColorInversion(enable);
+}
+
+void CastAccessibilityBindings::SetScreenReader(bool enable) {
+  if (!BindAccessibility())
+    return;
+  accessibility_service_->SetScreenReader(enable);
+}
+
+void CastAccessibilityBindings::SetMagnificationGesture(bool enable) {
+  if (!BindAccessibility())
+    return;
+  accessibility_service_->SetMagnificationGestureEnabled(enable);
+}
+
+v8::Local<v8::Value> CastAccessibilityBindings::GetAccessibilitySettings() {
+  DVLOG(2) << __FUNCTION__;
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+
+  if (!BindAccessibility())
+    return gin::ConvertToV8(isolate, false);
+
+  v8::Local<v8::Promise::Resolver> resolver =
+      v8::Promise::Resolver::New(isolate->GetCurrentContext()).ToLocalChecked();
+
+  PersistedResolver unique_resolver =
+      v8::Global<v8::Promise::Resolver>(isolate, resolver);
+  PersistedContext context(isolate, isolate->GetCurrentContext());
+
+  accessibility_service_->GetAccessibilitySettings(base::BindOnce(
+      &CastAccessibilityBindings::OnGetAccessibilitySettings,
+      base::Unretained(this), std::move(unique_resolver), std::move(context)));
+  return resolver->GetPromise();
+}
+
+void CastAccessibilityBindings::SetScreenReaderSettingChangedHandler(
+    v8::Local<v8::Function> handler) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  screen_reader_setting_changed_handler_ =
+      v8::UniquePersistent<v8::Function>(isolate, handler);
+}
+
+void CastAccessibilityBindings::SetColorInversionSettingChangedHandler(
+    v8::Local<v8::Function> handler) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  color_inversion_setting_changed_handler_ =
+      v8::UniquePersistent<v8::Function>(isolate, handler);
+}
+
+void CastAccessibilityBindings::SetMagnificationGestureSettingChangedHandler(
+    v8::Local<v8::Function> handler) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  magnification_gesture_setting_changed_handler_ =
+      v8::UniquePersistent<v8::Function>(isolate, handler);
+}
+
+bool CastAccessibilityBindings::BindAccessibility() {
+  if (!accessibility_service_) {
+    render_frame()->GetBrowserInterfaceBroker()->GetInterface(
+        accessibility_service_.BindNewPipeAndPassReceiver());
+    if (!accessibility_service_) {
+      LOG(ERROR)
+          << "Couldn't establish connection to cast window manager service";
+      return false;
+    }
+    accessibility_service_.set_disconnect_handler(
+        base::BindRepeating(&OnAccessibilityServiceConnectionError));
+  }
+  return true;
+}
+
+void CastAccessibilityBindings::OnGetAccessibilitySettings(
+    PersistedResolver resolver,
+    PersistedContext original_context,
+    mojom::AccessibilitySettingsPtr settings) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::MicrotasksScope microtasks_scope(
+      isolate, v8::MicrotasksScope::kDoNotRunMicrotasks);
+  v8::HandleScope handle_scope(isolate);
+  v8::Local<v8::Context> context = original_context.Get(isolate);
+  v8::Context::Scope context_scope(context);
+
+  v8::Local<v8::Object> obj =
+      gin::DataObjectBuilder(isolate)
+          .Set(kIsColorInversionEnabled, settings->color_inversion_enabled)
+          .Set(kIsScreenReaderEnabled, settings->screen_reader_enabled)
+          .Set(kIsMagnificationGestureEnabled,
+               settings->magnification_gesture_enabled)
+          .Build();
+
+  resolver.Get(isolate)->Resolve(context, obj).ToChecked();
+}
+
+void CastAccessibilityBindings::AccessibilitySettingChanged(
+    v8::UniquePersistent<v8::Function>* handler_function,
+    bool new_value) {
+  if (handler_function->IsEmpty()) {
+    return;
+  }
+
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::HandleScope handle_scope(isolate);
+  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
+  v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
+  v8::Context::Scope context_scope(context);
+  v8::Local<v8::Function> handler =
+      v8::Local<v8::Function>::New(isolate, std::move(*handler_function));
+
+  v8::Local<v8::Boolean> arg = v8::Boolean::New(isolate, new_value);
+
+  std::vector<v8::Local<v8::Value>> args{arg};
+
+  v8::MaybeLocal<v8::Value> maybe_result =
+      handler->Call(context, context->Global(), args.size(), args.data());
+
+  *handler_function = v8::UniquePersistent<v8::Function>(isolate, handler);
+
+  v8::Local<v8::Value> result;
+  ignore_result(maybe_result.ToLocal(&result));
+}
+
+void CastAccessibilityBindings::ScreenReaderSettingChanged(bool new_value) {
+  AccessibilitySettingChanged(&screen_reader_setting_changed_handler_,
+                              new_value);
+}
+
+void CastAccessibilityBindings::ColorInversionSettingChanged(bool new_value) {
+  AccessibilitySettingChanged(&color_inversion_setting_changed_handler_,
+                              new_value);
+}
+
+void CastAccessibilityBindings::MagnificationGestureSettingChanged(
+    bool new_value) {
+  AccessibilitySettingChanged(&magnification_gesture_setting_changed_handler_,
+                              new_value);
+}
+
+void CastAccessibilityBindings::OnCastAccessibilityClientRequest(
+    mojo::PendingReceiver<shell::mojom::CastAccessibilityClient> request) {
+  bindings_.Add(this, std::move(request));
+}
+
+}  // namespace shell
+}  // namespace chromecast
diff --git a/chromecast/renderer/cast_accessibility_bindings.h b/chromecast/renderer/cast_accessibility_bindings.h
new file mode 100644
index 0000000..ecace20
--- /dev/null
+++ b/chromecast/renderer/cast_accessibility_bindings.h
@@ -0,0 +1,100 @@
+// 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 CHROMECAST_RENDERER_CAST_ACCESSIBILITY_BINDINGS_H_
+#define CHROMECAST_RENDERER_CAST_ACCESSIBILITY_BINDINGS_H_
+
+#include "base/memory/ptr_util.h"
+#include "chromecast/common/mojom/accessibility.mojom.h"
+#include "chromecast/renderer/native_bindings_helper.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/receiver_set.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "services/service_manager/public/cpp/binder_registry.h"
+#include "v8/include/v8.h"
+
+namespace chromecast {
+
+class FeatureManager;
+
+namespace shell {
+
+// Renderer side implementation of JS available bindings for accessibility
+// related functions.
+class CastAccessibilityBindings : public CastBinding,
+                                  public mojom::CastAccessibilityClient {
+ public:
+  CastAccessibilityBindings(content::RenderFrame* render_frame,
+                            const FeatureManager* feature_manager);
+  ~CastAccessibilityBindings() override;
+  CastAccessibilityBindings(const CastAccessibilityBindings&) = delete;
+  CastAccessibilityBindings& operator=(const CastAccessibilityBindings&) =
+      delete;
+
+ private:
+  // content::RenderFrameObserver implementation:
+  void OnInterfaceRequestForFrame(
+      const std::string& interface_name,
+      mojo::ScopedMessagePipeHandle* interface_pipe) override;
+
+  // Typedefs for v8 objects that need to run between tasks.
+  // v8::Global has move semantics, it does not imply unique ownership over an
+  // v8 object. You can create multiple v8::Globals from the same v8::Local.
+  using PersistedContext = v8::Global<v8::Context>;
+  using PersistedResolver = v8::Global<v8::Promise::Resolver>;
+
+  // CastBinding implementation:
+  void Install(v8::Local<v8::Object> cast_platform,
+               v8::Isolate* isolate) override;
+
+  // Bind incoming accessibility requests with this implementation.
+  void OnCastAccessibilityClientRequest(
+      mojo::PendingReceiver<shell::mojom::CastAccessibilityClient> request);
+
+  void OnConnectionError();
+  bool BindAccessibility();
+
+  void SetColorInversion(bool enable);
+  void SetScreenReader(bool enable);
+  void SetMagnificationGesture(bool enable);
+
+  v8::Local<v8::Value> GetAccessibilitySettings();
+
+  void SetScreenReaderSettingChangedHandler(v8::Local<v8::Function> handler);
+  void SetColorInversionSettingChangedHandler(v8::Local<v8::Function> handler);
+  void SetMagnificationGestureSettingChangedHandler(
+      v8::Local<v8::Function> handler);
+
+  void OnGetAccessibilitySettings(PersistedResolver resolver,
+                                  PersistedContext original_context,
+                                  mojom::AccessibilitySettingsPtr settings);
+
+  void AccessibilitySettingChanged(
+      v8::UniquePersistent<v8::Function>* handler_function,
+      bool new_value);
+
+  // mojom::CastAccessibilityClient implementation
+  void ScreenReaderSettingChanged(bool new_value) override;
+  void ColorInversionSettingChanged(bool new_value) override;
+  void MagnificationGestureSettingChanged(bool new_value) override;
+
+  mojo::Remote<mojom::CastAccessibilityService> accessibility_service_;
+
+  const FeatureManager* feature_manager_;
+
+  v8::UniquePersistent<v8::Function> screen_reader_setting_changed_handler_;
+  v8::UniquePersistent<v8::Function> color_inversion_setting_changed_handler_;
+  v8::UniquePersistent<v8::Function>
+      magnification_gesture_setting_changed_handler_;
+
+  service_manager::BinderRegistry registry_;
+
+  mojo::ReceiverSet<shell::mojom::CastAccessibilityClient> bindings_;
+};
+
+}  // namespace shell
+}  // namespace chromecast
+
+#endif  // CHROMECAST_RENDERER_CAST_ACCESSIBILITY_BINDINGS_H_
diff --git a/chromecast/renderer/cast_demo_bindings.cc b/chromecast/renderer/cast_demo_bindings.cc
new file mode 100644
index 0000000..76d44c0
--- /dev/null
+++ b/chromecast/renderer/cast_demo_bindings.cc
@@ -0,0 +1,373 @@
+// 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 "chromecast/renderer/cast_demo_bindings.h"
+
+#include "base/check.h"
+#include "content/public/renderer/render_frame.h"
+#include "content/public/renderer/v8_value_converter.h"
+#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
+#include "third_party/blink/public/web/blink.h"
+#include "third_party/blink/public/web/web_local_frame.h"
+
+namespace chromecast {
+namespace shell {
+
+namespace {
+const int64_t kDelayBetweenReconnectionInMillis = 100;
+
+const char kDemoObjectName[] = "demo";
+
+const char kRecordEventName[] = "recordEvent";
+const char kSetRetailerName[] = "setRetailer";
+const char kSetStoreIdName[] = "setStoreId";
+const char kGetRetailerName[] = "getRetailer";
+const char kGetStoreIdName[] = "getStoreId";
+const char kSetDefaultVolumeName[] = "setDefaultVolume";
+const char kGetDefaultVolumeName[] = "getDefaultVolume";
+const char kApplyDefaultVolumeName[] = "applyDefaultVolume";
+const char kSetWifiConnectionName[] = "setWifiConnection";
+const char kGetAvailableWifiNetworksName[] = "getAvailableWifiNetworks";
+const char kGetWifiConnectionStateName[] = "getWifiConnectionState";
+const char kRegisterVolumeChangeHandlerName[] = "registerVolumeChangeHandler";
+const char kPersistLocalStorageName[] = "persistLocalStorage";
+
+const char kSetVolumeName[] = "setVolume";
+}  // namespace
+
+CastDemoBindings::CastDemoBindings(content::RenderFrame* render_frame)
+    : CastBinding(render_frame), binding_(this), weak_factory_(this) {}
+
+CastDemoBindings::~CastDemoBindings() {}
+
+void CastDemoBindings::Install(v8::Local<v8::Object> cast_platform,
+                               v8::Isolate* isolate) {
+  v8::Local<v8::Object> demo_object =
+      EnsureObjectExists(isolate, cast_platform, kDemoObjectName);
+
+  InstallBinding(isolate, demo_object, kRecordEventName,
+                 &CastDemoBindings::RecordEvent, base::Unretained(this));
+  InstallBinding(isolate, demo_object, kSetRetailerName,
+                 &CastDemoBindings::SetRetailerName, base::Unretained(this));
+  InstallBinding(isolate, demo_object, kSetStoreIdName,
+                 &CastDemoBindings::SetStoreId, base::Unretained(this));
+  InstallBinding(isolate, demo_object, kGetRetailerName,
+                 &CastDemoBindings::GetRetailerName, base::Unretained(this));
+  InstallBinding(isolate, demo_object, kGetStoreIdName,
+                 &CastDemoBindings::GetStoreId, base::Unretained(this));
+  InstallBinding(isolate, demo_object, kSetDefaultVolumeName,
+                 &CastDemoBindings::SetDefaultVolumeLevel,
+                 base::Unretained(this));
+  InstallBinding(isolate, demo_object, kGetDefaultVolumeName,
+                 &CastDemoBindings::GetDefaultVolumeLevel,
+                 base::Unretained(this));
+  InstallBinding(isolate, demo_object, kApplyDefaultVolumeName,
+                 &CastDemoBindings::ApplyDefaultVolume, base::Unretained(this));
+  InstallBinding(isolate, demo_object, kSetWifiConnectionName,
+                 &CastDemoBindings::SetWifiCredentials, base::Unretained(this));
+  InstallBinding(isolate, demo_object, kGetAvailableWifiNetworksName,
+                 &CastDemoBindings::GetAvailableWifiNetworks,
+                 base::Unretained(this));
+  InstallBinding(isolate, demo_object, kGetWifiConnectionStateName,
+                 &CastDemoBindings::GetConnectionStatus,
+                 base::Unretained(this));
+  InstallBinding(isolate, demo_object, kRegisterVolumeChangeHandlerName,
+                 &CastDemoBindings::SetVolumeChangeHandler,
+                 base::Unretained(this));
+  InstallBinding(isolate, demo_object, kPersistLocalStorageName,
+                 &CastDemoBindings::PersistLocalStorage,
+                 base::Unretained(this));
+
+  InstallBinding(isolate, demo_object, kSetVolumeName,
+                 &CastDemoBindings::SetVolume, base::Unretained(this));
+}
+
+void CastDemoBindings::RecordEvent(const std::string& event_name,
+                                   v8::Local<v8::Value> v8_data) {
+  v8::Isolate* v8_isolate = blink::MainThreadIsolate();
+  v8::HandleScope v8_handle_scope(v8_isolate);
+  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
+  v8::Local<v8::Context> v8_context = web_frame->MainWorldScriptContext();
+  v8::Context::Scope v8_context_scope(v8_context);
+
+  std::unique_ptr<content::V8ValueConverter> v8_converter =
+      content::V8ValueConverter::Create();
+  v8_converter->SetDateAllowed(true);
+  v8_converter->SetRegExpAllowed(true);
+  std::unique_ptr<base::Value> data_ptr =
+      v8_converter->FromV8Value(v8_data, v8_context);
+
+  base::Value data;
+  if (data_ptr) {
+    data = base::Value::FromUniquePtrValue(std::move(data_ptr));
+  }
+  GetCastDemo()->RecordEvent(event_name, std::move(data));
+}
+
+void CastDemoBindings::SetRetailerName(const std::string& retailer_name) {
+  GetCastDemo()->SetRetailerName(retailer_name);
+}
+
+void CastDemoBindings::SetStoreId(const std::string& store_id) {
+  GetCastDemo()->SetStoreId(store_id);
+}
+
+v8::Local<v8::Value> CastDemoBindings::GetRetailerName() {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  auto context = isolate->GetCurrentContext();
+  v8::Local<v8::Promise::Resolver> resolver =
+      v8::Promise::Resolver::New(context).ToLocalChecked();
+  v8::Global<v8::Promise::Resolver> unique_resolver =
+      v8::Global<v8::Promise::Resolver>(isolate, resolver);
+  v8::Global<v8::Context> persisted_context =
+      v8::Global<v8::Context>(isolate, context);
+
+  GetCastDemo()->GetRetailerName(base::BindOnce(
+      &CastDemoBindings::OnGetRetailerName, base::Unretained(this),
+      std::move(unique_resolver), std::move(persisted_context)));
+  return resolver->GetPromise();
+}
+
+void CastDemoBindings::OnGetRetailerName(
+    v8::Global<v8::Promise::Resolver> resolver,
+    v8::Global<v8::Context> original_context,
+    const std::string& retailer_name) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::MicrotasksScope microtasks_scope(
+      isolate, v8::MicrotasksScope::kDoNotRunMicrotasks);
+  v8::HandleScope handle_scope(isolate);
+  v8::Local<v8::Context> context = original_context.Get(isolate);
+  v8::Context::Scope context_scope(context);
+
+  resolver.Get(isolate)
+      ->Resolve(context, gin::ConvertToV8(isolate, retailer_name))
+      .ToChecked();
+}
+
+v8::Local<v8::Value> CastDemoBindings::GetStoreId() {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  auto context = isolate->GetCurrentContext();
+  v8::Local<v8::Promise::Resolver> resolver =
+      v8::Promise::Resolver::New(context).ToLocalChecked();
+  v8::Global<v8::Promise::Resolver> unique_resolver =
+      v8::Global<v8::Promise::Resolver>(isolate, resolver);
+  v8::Global<v8::Context> persisted_context =
+      v8::Global<v8::Context>(isolate, context);
+
+  GetCastDemo()->GetStoreId(
+      base::BindOnce(&CastDemoBindings::OnGetStoreId, base::Unretained(this),
+                     std::move(unique_resolver), std::move(persisted_context)));
+  return resolver->GetPromise();
+}
+
+void CastDemoBindings::OnGetStoreId(v8::Global<v8::Promise::Resolver> resolver,
+                                    v8::Global<v8::Context> original_context,
+                                    const std::string& store_id) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::MicrotasksScope microtasks_scope(
+      isolate, v8::MicrotasksScope::kDoNotRunMicrotasks);
+  v8::HandleScope handle_scope(isolate);
+  v8::Local<v8::Context> context = original_context.Get(isolate);
+  v8::Context::Scope context_scope(context);
+
+  resolver.Get(isolate)
+      ->Resolve(context, gin::ConvertToV8(isolate, store_id))
+      .ToChecked();
+}
+
+void CastDemoBindings::SetVolume(float level) {
+  // This method is deprecated. Provide a workable implementation to support
+  // development using old content.
+  SetDefaultVolumeLevel(level);
+  ApplyDefaultVolume();
+}
+
+void CastDemoBindings::SetDefaultVolumeLevel(float level) {
+  GetCastDemo()->SetDefaultVolumeLevel(level);
+}
+
+v8::Local<v8::Value> CastDemoBindings::GetDefaultVolumeLevel() {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  auto context = isolate->GetCurrentContext();
+  v8::Local<v8::Promise::Resolver> resolver =
+      v8::Promise::Resolver::New(context).ToLocalChecked();
+  v8::Global<v8::Promise::Resolver> unique_resolver =
+      v8::Global<v8::Promise::Resolver>(isolate, resolver);
+  v8::Global<v8::Context> persisted_context =
+      v8::Global<v8::Context>(isolate, context);
+
+  GetCastDemo()->GetDefaultVolumeLevel(base::BindOnce(
+      &CastDemoBindings::OnGetDefaultVolumeLevel, base::Unretained(this),
+      std::move(unique_resolver), std::move(persisted_context)));
+  return resolver->GetPromise();
+}
+
+void CastDemoBindings::OnGetDefaultVolumeLevel(
+    v8::Global<v8::Promise::Resolver> resolver,
+    v8::Global<v8::Context> original_context,
+    float level) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::MicrotasksScope microtasks_scope(
+      isolate, v8::MicrotasksScope::kDoNotRunMicrotasks);
+  v8::HandleScope handle_scope(isolate);
+  v8::Local<v8::Context> context = original_context.Get(isolate);
+  v8::Context::Scope context_scope(context);
+
+  resolver.Get(isolate)
+      ->Resolve(context, gin::ConvertToV8(isolate, level))
+      .ToChecked();
+}
+
+void CastDemoBindings::ApplyDefaultVolume() {
+  GetCastDemo()->ApplyDefaultVolume();
+}
+
+void CastDemoBindings::SetWifiCredentials(const std::string& ssid,
+                                          const std::string& psk) {
+  GetCastDemo()->SetWifiCredentials(ssid, psk);
+}
+
+v8::Local<v8::Value> CastDemoBindings::GetAvailableWifiNetworks() {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  auto context = isolate->GetCurrentContext();
+  v8::Local<v8::Promise::Resolver> resolver =
+      v8::Promise::Resolver::New(context).ToLocalChecked();
+  v8::Global<v8::Promise::Resolver> unique_resolver =
+      v8::Global<v8::Promise::Resolver>(isolate, resolver);
+  v8::Global<v8::Context> persisted_context =
+      v8::Global<v8::Context>(isolate, context);
+
+  GetCastDemo()->GetAvailableWifiNetworks(base::BindOnce(
+      &CastDemoBindings::OnGetAvailableWifiNetworks, base::Unretained(this),
+      std::move(unique_resolver), std::move(persisted_context)));
+  return resolver->GetPromise();
+}
+
+void CastDemoBindings::OnGetAvailableWifiNetworks(
+    v8::Global<v8::Promise::Resolver> resolver,
+    v8::Global<v8::Context> original_context,
+    base::Value network_list) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::MicrotasksScope microtasks_scope(
+      isolate, v8::MicrotasksScope::kDoNotRunMicrotasks);
+  v8::HandleScope handle_scope(isolate);
+  v8::Local<v8::Context> context = original_context.Get(isolate);
+  v8::Context::Scope context_scope(context);
+
+  std::unique_ptr<content::V8ValueConverter> v8_converter =
+      content::V8ValueConverter::Create();
+  v8::Local<v8::Value> v8_value =
+      v8_converter->ToV8Value(&network_list, context);
+
+  resolver.Get(isolate)
+      ->Resolve(context, gin::ConvertToV8(isolate, v8_value))
+      .ToChecked();
+}
+
+v8::Local<v8::Value> CastDemoBindings::GetConnectionStatus() {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  auto context = isolate->GetCurrentContext();
+  v8::Local<v8::Promise::Resolver> resolver =
+      v8::Promise::Resolver::New(context).ToLocalChecked();
+  v8::Global<v8::Promise::Resolver> unique_resolver =
+      v8::Global<v8::Promise::Resolver>(isolate, resolver);
+  v8::Global<v8::Context> persisted_context =
+      v8::Global<v8::Context>(isolate, context);
+
+  GetCastDemo()->GetConnectionStatus(base::BindOnce(
+      &CastDemoBindings::OnGetConnectionStatus, base::Unretained(this),
+      std::move(unique_resolver), std::move(persisted_context)));
+  return resolver->GetPromise();
+}
+
+void CastDemoBindings::OnGetConnectionStatus(
+    v8::Global<v8::Promise::Resolver> resolver,
+    v8::Global<v8::Context> original_context,
+    base::Value status) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::MicrotasksScope microtasks_scope(
+      isolate, v8::MicrotasksScope::kDoNotRunMicrotasks);
+  v8::HandleScope handle_scope(isolate);
+  v8::Local<v8::Context> context = original_context.Get(isolate);
+  v8::Context::Scope context_scope(context);
+
+  std::unique_ptr<content::V8ValueConverter> v8_converter =
+      content::V8ValueConverter::Create();
+  v8::Local<v8::Value> v8_value = v8_converter->ToV8Value(&status, context);
+
+  resolver.Get(isolate)
+      ->Resolve(context, gin::ConvertToV8(isolate, v8_value))
+      .ToChecked();
+}
+
+void CastDemoBindings::SetVolumeChangeHandler(
+    v8::Local<v8::Function> volume_change_handler) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  volume_change_handler_ =
+      v8::UniquePersistent<v8::Function>(isolate, volume_change_handler);
+}
+
+void CastDemoBindings::VolumeChanged(float level) {
+  if (volume_change_handler_.IsEmpty()) {
+    return;
+  }
+
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::HandleScope handle_scope(isolate);
+  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
+  v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
+  v8::Context::Scope context_scope(context);
+  v8::Local<v8::Function> handler =
+      v8::Local<v8::Function>::New(isolate, std::move(volume_change_handler_));
+
+  std::vector<v8::Local<v8::Value>> args{gin::ConvertToV8(isolate, level)};
+
+  v8::MaybeLocal<v8::Value> maybe_result =
+      handler->Call(context, context->Global(), args.size(), args.data());
+
+  volume_change_handler_ = v8::UniquePersistent<v8::Function>(isolate, handler);
+
+  v8::Local<v8::Value> result;
+  ignore_result(maybe_result.ToLocal(&result));
+}
+
+void CastDemoBindings::PersistLocalStorage() {
+  GetCastDemo()->PersistLocalStorage();
+}
+
+void CastDemoBindings::ReconnectMojo() {
+  render_frame()->GetBrowserInterfaceBroker()->GetInterface(
+      cast_demo_.BindNewPipeAndPassReceiver());
+  DCHECK(cast_demo_.is_bound());
+  cast_demo_.set_disconnect_handler(base::BindOnce(
+      &CastDemoBindings::OnMojoConnectionError, base::Unretained(this)));
+
+  if (binding_.is_bound()) {
+    binding_.reset();
+  }
+
+  mojo::PendingRemote<mojom::CastDemoVolumeChangeObserver> pending_remote;
+  binding_.Bind(pending_remote.InitWithNewPipeAndPassReceiver());
+  cast_demo_->AddVolumeChangeObserver(std::move(pending_remote));
+}
+
+void CastDemoBindings::OnMojoConnectionError() {
+  LOG(WARNING) << "Disconnected from Demo Mojo. Will retry every "
+               << kDelayBetweenReconnectionInMillis << " milliseconds.";
+  base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
+      FROM_HERE,
+      base::BindOnce(&CastDemoBindings::ReconnectMojo,
+                     weak_factory_.GetWeakPtr()),
+      base::TimeDelta::FromMilliseconds(kDelayBetweenReconnectionInMillis));
+}
+
+const mojo::Remote<mojom::CastDemo>& CastDemoBindings::GetCastDemo() {
+  if (!cast_demo_.is_bound()) {
+    ReconnectMojo();
+  }
+  return cast_demo_;
+}
+
+}  // namespace shell
+}  // namespace chromecast
diff --git a/chromecast/renderer/cast_demo_bindings.h b/chromecast/renderer/cast_demo_bindings.h
new file mode 100644
index 0000000..2306bfe
--- /dev/null
+++ b/chromecast/renderer/cast_demo_bindings.h
@@ -0,0 +1,97 @@
+// 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 CHROMECAST_RENDERER_CAST_DEMO_BINDINGS_H_
+#define CHROMECAST_RENDERER_CAST_DEMO_BINDINGS_H_
+
+#include <string>
+
+#include "base/memory/weak_ptr.h"
+#include "chromecast/common/mojom/cast_demo.mojom.h"
+#include "chromecast/renderer/native_bindings_helper.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "v8/include/v8.h"
+
+namespace chromecast {
+namespace shell {
+
+// Enabled only when the device is in demonstration mode. This is only enabled
+// for the demonstration JS app.
+class CastDemoBindings : public CastBinding,
+                         public mojom::CastDemoVolumeChangeObserver {
+ public:
+  explicit CastDemoBindings(content::RenderFrame* render_frame);
+  CastDemoBindings(const CastDemoBindings&) = delete;
+  CastDemoBindings& operator=(const CastDemoBindings&) = delete;
+
+ private:
+  friend class CastBinding;
+
+  ~CastDemoBindings() override;
+
+  // CastBinding implementation:
+  void Install(v8::Local<v8::Object> cast_platform,
+               v8::Isolate* isolate) override;
+
+  // Methods to be called from v8: (See mojom for details)
+  void RecordEvent(const std::string& event_name, v8::Local<v8::Value> data);
+  void SetRetailerName(const std::string& retailer_name);
+  void SetStoreId(const std::string& store_id);
+  v8::Local<v8::Value> GetRetailerName();
+  v8::Local<v8::Value> GetStoreId();
+  void SetDefaultVolumeLevel(float level);
+  v8::Local<v8::Value> GetDefaultVolumeLevel();
+  void ApplyDefaultVolume();
+  void SetWifiCredentials(const std::string& ssid, const std::string& psk);
+  v8::Local<v8::Value> GetAvailableWifiNetworks();
+  v8::Local<v8::Value> GetConnectionStatus();
+  void SetVolumeChangeHandler(v8::Local<v8::Function> volume_change_handler);
+  void PersistLocalStorage();
+
+  // Deprecated
+  void SetVolume(float level);
+
+  // Methods to return values to v8:
+  void OnGetRetailerName(v8::Global<v8::Promise::Resolver> resolver,
+                         v8::Global<v8::Context> original_context,
+                         const std::string& retailer_name);
+  void OnGetStoreId(v8::Global<v8::Promise::Resolver> resolver,
+                    v8::Global<v8::Context> original_context,
+                    const std::string& store_id);
+  void OnGetDefaultVolumeLevel(v8::Global<v8::Promise::Resolver> resolver,
+                               v8::Global<v8::Context> original_context,
+                               float level);
+  void OnGetAvailableWifiNetworks(v8::Global<v8::Promise::Resolver> resolver,
+                                  v8::Global<v8::Context> original_context,
+                                  base::Value network_list);
+  void OnGetConnectionStatus(v8::Global<v8::Promise::Resolver> resolver,
+                             v8::Global<v8::Context> original_context,
+                             base::Value status);
+
+  // mojom::CastDemoVolumeChangeObserver implementation:
+  void VolumeChanged(float level) override;
+
+  void ReconnectMojo();
+  void OnMojoConnectionError();
+
+  // Returns a reference to |cast_demo_|, and binds it to a mojo pipe if
+  // necessary.
+  const mojo::Remote<mojom::CastDemo>& GetCastDemo();
+
+  // The pointer to the remote mojom::CastDemo interface.  Do not access this
+  // member directly; instead, use GetCastDemo().
+  mojo::Remote<mojom::CastDemo> cast_demo_;
+
+  mojo::Receiver<mojom::CastDemoVolumeChangeObserver> binding_;
+
+  v8::UniquePersistent<v8::Function> volume_change_handler_;
+
+  base::WeakPtrFactory<CastDemoBindings> weak_factory_;
+};
+
+}  // namespace shell
+}  // namespace chromecast
+
+#endif  // CHROMECAST_RENDERER_CAST_DEMO_BINDINGS_H_
diff --git a/chromecast/renderer/cast_window_manager_bindings.cc b/chromecast/renderer/cast_window_manager_bindings.cc
new file mode 100644
index 0000000..897f8b5
--- /dev/null
+++ b/chromecast/renderer/cast_window_manager_bindings.cc
@@ -0,0 +1,340 @@
+// 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 "chromecast/renderer/cast_window_manager_bindings.h"
+
+#include <vector>
+
+#include "base/check.h"
+#include "build/build_config.h"
+#include "chromecast/base/cast_features.h"
+#include "chromecast/common/feature_constants.h"
+#include "chromecast/common/mojom/gesture.mojom.h"
+#include "chromecast/renderer/feature_manager.h"
+#include "content/public/renderer/render_frame.h"
+#include "content/public/renderer/render_view.h"
+#include "gin/data_object_builder.h"
+#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
+#include "third_party/blink/public/web/blink.h"
+#include "third_party/blink/public/web/web_local_frame.h"
+#include "third_party/blink/public/web/web_view.h"
+
+namespace chromecast {
+namespace shell {
+
+namespace {
+
+const char kBindingsObjectName[] = "windowManager";
+const char kOnBackGestureName[] = "onBackGesture";
+const char kOnBackGestureProgressName[] = "onBackGestureProgress";
+const char kOnBackGestureCancelName[] = "onBackGestureCancel";
+const char kOnTopDragGestureDoneName[] = "onTopDragGestureDone";
+const char kOnTopDragGestureProgressName[] = "onTopDragGestureProgress";
+const char kOnRightDragGestureDoneName[] = "onRightDragGestureDone";
+const char kOnRightDragGestureProgressName[] = "onRightDragGestureProgress";
+const char kOnTapGestureName[] = "onTapGesture";
+const char kOnTapDownGestureName[] = "onTapDownGesture";
+const char kCanGoBackName[] = "canGoBack";
+const char kCanTopDragName[] = "canTopDrag";
+const char kCanRightDragName[] = "canRightDrag";
+const char kMinimize[] = "minimize";
+const char kMaximize[] = "maximize";
+
+#if !defined(OS_ANDROID)
+const char kDisplayControlsName[] = "displayControls";
+#endif
+
+void OnGestureSourceDisconnectionError() {
+  LOG(ERROR) << "Connection error talking to system gesture source.";
+}
+
+void OnWindowDisconnect() {
+  LOG(ERROR) << "Connection error talking to platform activity.";
+}
+
+}  // namespace
+
+CastWindowManagerBindings::CastWindowManagerBindings(
+    content::RenderFrame* render_frame,
+    const FeatureManager* feature_manager)
+    : CastBinding(render_frame),
+      feature_manager_(feature_manager),
+      handler_receiver_(this) {
+  DCHECK(feature_manager_);
+}
+
+CastWindowManagerBindings::~CastWindowManagerBindings() {}
+
+void CastWindowManagerBindings::Install(v8::Local<v8::Object> cast_platform,
+                                        v8::Isolate* isolate) {
+  if (feature_manager_->FeatureEnabled(feature::kEnableSystemGestures)) {
+    v8::Local<v8::Object> windowManagerObject =
+        EnsureObjectExists(isolate, cast_platform, kBindingsObjectName);
+
+    // On back bindings.
+    InstallBinding(isolate, windowManagerObject, kCanGoBackName,
+                   &CastWindowManagerBindings::SetCanGoBack,
+                   base::Unretained(this));
+    InstallBinding(isolate, windowManagerObject, kOnBackGestureName,
+                   &CastWindowManagerBindings::SetV8Callback,
+                   base::Unretained(this), &on_back_gesture_callback_);
+    InstallBinding(isolate, windowManagerObject, kOnBackGestureProgressName,
+                   &CastWindowManagerBindings::SetV8Callback,
+                   base::Unretained(this), &on_back_gesture_progress_callback_);
+    InstallBinding(isolate, windowManagerObject, kOnBackGestureCancelName,
+                   &CastWindowManagerBindings::SetV8Callback,
+                   base::Unretained(this), &on_back_gesture_cancel_callback_);
+
+    // Top drag bindings.
+    InstallBinding(isolate, windowManagerObject, kCanTopDragName,
+                   &CastWindowManagerBindings::SetCanTopDrag,
+                   base::Unretained(this));
+    InstallBinding(isolate, windowManagerObject, kOnTopDragGestureDoneName,
+                   &CastWindowManagerBindings::SetV8Callback,
+                   base::Unretained(this), &on_top_drag_gesture_done_callback_);
+    InstallBinding(isolate, windowManagerObject, kOnTopDragGestureProgressName,
+                   &CastWindowManagerBindings::SetV8Callback,
+                   base::Unretained(this),
+                   &on_top_drag_gesture_progress_callback_);
+
+    // Right drag bindings.
+    InstallBinding(isolate, windowManagerObject, kCanRightDragName,
+                   &CastWindowManagerBindings::SetCanRightDrag,
+                   base::Unretained(this));
+    InstallBinding(isolate, windowManagerObject, kOnRightDragGestureDoneName,
+                   &CastWindowManagerBindings::SetV8Callback,
+                   base::Unretained(this),
+                   &on_right_drag_gesture_done_callback_);
+    InstallBinding(
+        isolate, windowManagerObject, kOnRightDragGestureProgressName,
+        &CastWindowManagerBindings::SetV8Callback, base::Unretained(this),
+        &on_right_drag_gesture_progress_callback_);
+
+    // 'Tap' bindings.
+    InstallBinding(isolate, windowManagerObject, kOnTapGestureName,
+                   &CastWindowManagerBindings::SetV8Callback,
+                   base::Unretained(this), &on_tap_gesture_callback_);
+    InstallBinding(isolate, windowManagerObject, kOnTapDownGestureName,
+                   &CastWindowManagerBindings::SetV8Callback,
+                   base::Unretained(this), &on_tap_down_gesture_callback_);
+  }
+  if (feature_manager_->FeatureEnabled(feature::kEnableWindowControls)) {
+    v8::Local<v8::Object> windowManagerObject =
+        EnsureObjectExists(isolate, cast_platform, kBindingsObjectName);
+
+    InstallBinding(isolate, windowManagerObject, kMaximize,
+                   &CastWindowManagerBindings::Show, base::Unretained(this));
+    InstallBinding(isolate, windowManagerObject, kMinimize,
+                   &CastWindowManagerBindings::Hide, base::Unretained(this));
+  }
+}
+
+v8::Local<v8::Value> CastWindowManagerBindings::SetV8Callback(
+    v8::UniquePersistent<v8::Function>* callback_function,
+    v8::Local<v8::Function> callback) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+
+  *callback_function = v8::UniquePersistent<v8::Function>(isolate, callback);
+
+  return v8::Undefined(isolate);
+}
+
+void CastWindowManagerBindings::Show() {
+  BindWindow();
+  window_->Show();
+}
+
+void CastWindowManagerBindings::Hide() {
+  BindWindow();
+  window_->Hide();
+}
+
+void CastWindowManagerBindings::SetCanGoBack(bool can_go_back) {
+  BindGestureSource();
+  gesture_source_->SetCanGoBack(can_go_back);
+}
+
+void CastWindowManagerBindings::SetCanTopDrag(bool can_top_drag) {
+  BindGestureSource();
+  gesture_source_->SetCanTopDrag(can_top_drag);
+}
+
+void CastWindowManagerBindings::SetCanRightDrag(bool can_right_drag) {
+  BindGestureSource();
+  gesture_source_->SetCanRightDrag(can_right_drag);
+}
+
+void CastWindowManagerBindings::OnTouchInputSupportSet(
+    PersistedResolver resolver,
+    PersistedContext original_context,
+    bool resolve_promise,
+    bool display_controls) {
+  DVLOG(2) << __FUNCTION__;
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::MicrotasksScope microtasks_scope(
+      isolate, v8::MicrotasksScope::kDoNotRunMicrotasks);
+  v8::HandleScope handle_scope(isolate);
+  v8::Local<v8::Context> context = original_context.Get(isolate);
+  v8::Context::Scope context_scope(context);
+
+  if (resolve_promise) {
+    resolver.Get(isolate)
+        ->Resolve(context,
+
+#if !defined(OS_ANDROID)
+                  gin::DataObjectBuilder(isolate)
+                      .Set(kDisplayControlsName, display_controls)
+                      .Build()
+#else
+                  v8::Undefined(isolate)
+#endif
+                      )
+        .ToChecked();
+  } else {
+    resolver.Get(isolate)->Reject(context, v8::Undefined(isolate)).ToChecked();
+  }
+}
+
+void CastWindowManagerBindings::BindGestureSource() {
+  if (gesture_source_.is_bound() && gesture_source_.is_connected())
+    return;
+  gesture_source_.reset();
+  render_frame()->GetBrowserInterfaceBroker()->GetInterface(
+      gesture_source_.BindNewPipeAndPassReceiver());
+  gesture_source_.set_disconnect_handler(
+      base::BindRepeating(&OnGestureSourceDisconnectionError));
+  handler_receiver_.reset();
+  gesture_source_->Subscribe(handler_receiver_.BindNewPipeAndPassRemote());
+}
+
+void CastWindowManagerBindings::BindWindow() {
+  if (window_.is_bound() && window_.is_connected())
+    return;
+  window_.reset();
+  render_frame()->GetBrowserInterfaceBroker()->GetInterface(
+      window_.BindNewPipeAndPassReceiver());
+  window_.set_disconnect_handler(base::BindRepeating(&OnWindowDisconnect));
+}
+
+void CastWindowManagerBindings::InvokeV8Callback(
+    v8::UniquePersistent<v8::Function>* callback_function) {
+  if (callback_function->IsEmpty()) {
+    return;
+  }
+
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::HandleScope handle_scope(isolate);
+  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
+  v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
+  v8::Context::Scope context_scope(context);
+  v8::Local<v8::Function> handler =
+      v8::Local<v8::Function>::New(isolate, std::move(*callback_function));
+
+  v8::MaybeLocal<v8::Value> maybe_result =
+      handler->Call(context, context->Global(), 0, nullptr);
+
+  *callback_function = v8::UniquePersistent<v8::Function>(isolate, handler);
+
+  v8::Local<v8::Value> result;
+  ignore_result(maybe_result.ToLocal(&result));
+}
+
+void CastWindowManagerBindings::InvokeV8Callback(
+    v8::UniquePersistent<v8::Function>* callback_function,
+    const gfx::Point& touch_location) {
+  if (callback_function->IsEmpty()) {
+    return;
+  }
+
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::HandleScope handle_scope(isolate);
+  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
+  v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
+  v8::Context::Scope context_scope(context);
+  v8::Local<v8::Function> handler =
+      v8::Local<v8::Function>::New(isolate, std::move(*callback_function));
+
+  v8::Local<v8::Number> touch_x = v8::Integer::New(isolate, touch_location.x());
+  v8::Local<v8::Number> touch_y = v8::Integer::New(isolate, touch_location.y());
+
+  std::vector<v8::Local<v8::Value>> args{touch_x, touch_y};
+
+  v8::MaybeLocal<v8::Value> maybe_result =
+      handler->Call(context, context->Global(), args.size(), args.data());
+
+  *callback_function = v8::UniquePersistent<v8::Function>(isolate, handler);
+
+  v8::Local<v8::Value> result;
+  ignore_result(maybe_result.ToLocal(&result));
+}
+
+void CastWindowManagerBindings::OnBackGesture(
+    ::chromecast::mojom::GestureHandler::OnBackGestureCallback callback) {
+  // Note: Can't use InvokeV8Callback here because of the OnBackGestureCallback
+  // argument. So we have this boilerplate here until we can get rid of the
+  // unused callback argument.
+  if (on_back_gesture_callback_.IsEmpty()) {
+    std::move(callback).Run(false);
+    return;
+  }
+
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::HandleScope handle_scope(isolate);
+  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
+  v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
+  v8::Context::Scope context_scope(context);
+  v8::Local<v8::Function> handler = v8::Local<v8::Function>::New(
+      isolate, std::move(on_back_gesture_callback_));
+  auto result = handler->Call(context, context->Global(), 0, nullptr);
+
+  on_back_gesture_callback_ =
+      v8::UniquePersistent<v8::Function>(isolate, handler);
+  if (result.IsEmpty()) {
+    LOG(ERROR) << "No value from callback execution; ";
+    std::move(callback).Run(false);
+    return;
+  }
+
+  auto callback_return_value = result.ToLocalChecked();
+  bool return_boolean = callback_return_value->BooleanValue(isolate);
+  std::move(callback).Run(return_boolean);
+}
+
+void CastWindowManagerBindings::OnBackGestureProgress(
+    const gfx::Point& touch_location) {
+  InvokeV8Callback(&on_back_gesture_progress_callback_, touch_location);
+}
+
+void CastWindowManagerBindings::OnBackGestureCancel() {
+  InvokeV8Callback(&on_back_gesture_cancel_callback_);
+}
+
+void CastWindowManagerBindings::OnTopDragGestureDone() {
+  InvokeV8Callback(&on_top_drag_gesture_done_callback_);
+}
+
+void CastWindowManagerBindings::OnTopDragGestureProgress(
+    const gfx::Point& touch_location) {
+  InvokeV8Callback(&on_top_drag_gesture_progress_callback_, touch_location);
+}
+
+void CastWindowManagerBindings::OnRightDragGestureDone() {
+  InvokeV8Callback(&on_right_drag_gesture_done_callback_);
+}
+
+void CastWindowManagerBindings::OnRightDragGestureProgress(
+    const gfx::Point& touch_location) {
+  InvokeV8Callback(&on_right_drag_gesture_progress_callback_, touch_location);
+}
+
+void CastWindowManagerBindings::OnTapGesture() {
+  InvokeV8Callback(&on_tap_gesture_callback_);
+}
+
+void CastWindowManagerBindings::OnTapDownGesture() {
+  InvokeV8Callback(&on_tap_down_gesture_callback_);
+}
+
+}  // namespace shell
+}  // namespace chromecast
diff --git a/chromecast/renderer/cast_window_manager_bindings.h b/chromecast/renderer/cast_window_manager_bindings.h
new file mode 100644
index 0000000..dfb4731
--- /dev/null
+++ b/chromecast/renderer/cast_window_manager_bindings.h
@@ -0,0 +1,108 @@
+// 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 CHROMECAST_RENDERER_CAST_WINDOW_MANAGER_BINDINGS_H_
+#define CHROMECAST_RENDERER_CAST_WINDOW_MANAGER_BINDINGS_H_
+
+#include "base/memory/ptr_util.h"
+#include "chromecast/browser/mojom/cast_content_window.mojom.h"
+#include "chromecast/common/mojom/activity_window.mojom.h"
+#include "chromecast/common/mojom/gesture.mojom.h"
+#include "chromecast/renderer/native_bindings_helper.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "v8/include/v8.h"
+
+namespace chromecast {
+
+class FeatureManager;
+
+namespace shell {
+
+// Renderer side implementation of cast system gesture.
+// Implements the mojo service which receives notification of gesture events,
+// and also sets up the JS callback bindings to application code.
+class CastWindowManagerBindings : public CastBinding,
+                                  public ::chromecast::mojom::GestureHandler {
+ public:
+  CastWindowManagerBindings(content::RenderFrame* render_frame,
+                            const FeatureManager* feature_manager);
+  CastWindowManagerBindings(const CastWindowManagerBindings&) = delete;
+  CastWindowManagerBindings& operator=(const CastWindowManagerBindings&) =
+      delete;
+
+  // ::chromecast::mojom::GestureHandler implementation:
+  void OnBackGesture(OnBackGestureCallback callback) override;
+  void OnBackGestureProgress(const gfx::Point& touch_location) override;
+  void OnBackGestureCancel() override;
+  void OnTopDragGestureProgress(const gfx::Point& touch_location) override;
+  void OnTopDragGestureDone() override;
+  void OnRightDragGestureProgress(const gfx::Point& touch_location) override;
+  void OnRightDragGestureDone() override;
+  void OnTapGesture() override;
+  void OnTapDownGesture() override;
+
+ private:
+  friend class CastBinding;
+
+  ~CastWindowManagerBindings() override;
+
+  // Typedefs for v8 objects that need to run between tasks.
+  // v8::Global has move semantics, it does not imply unique ownership over an
+  // v8 object. You can create multiple v8::Globals from the same v8::Local.
+  using PersistedContext = v8::Global<v8::Context>;
+  using PersistedResolver = v8::Global<v8::Promise::Resolver>;
+
+  v8::Local<v8::Value> SetV8Callback(
+      v8::UniquePersistent<v8::Function>* callback_function,
+      v8::Local<v8::Function> callback);
+  void InvokeV8Callback(v8::UniquePersistent<v8::Function>* callback_function);
+  void InvokeV8Callback(v8::UniquePersistent<v8::Function>* callback_function,
+                        const gfx::Point& touch_location);
+
+  // CastBinding implementation:
+  void Install(v8::Local<v8::Object> cast_platform,
+               v8::Isolate* isolate) override;
+
+  void BindGestureSource();
+  void BindWindow();
+
+  void Show();
+  void Hide();
+  void SetCanGoBack(bool can_go_back);
+  void SetCanTopDrag(bool can_top_drag);
+  void SetCanRightDrag(bool can_right_drag);
+
+  void OnTouchInputSupportSet(PersistedResolver resolver,
+                              PersistedContext original_context,
+                              bool resolve_promise,
+                              bool display_controls);
+
+  const FeatureManager* feature_manager_;
+
+  mojo::Remote<::chromecast::mojom::GestureSource> gesture_source_;
+  mojo::Remote<::chromecast::mojom::ActivityWindow> window_;
+
+  // Receiver handle bound to self.
+  mojo::Receiver<::chromecast::mojom::GestureHandler> handler_receiver_;
+
+  v8::UniquePersistent<v8::Function> on_back_gesture_callback_;
+  v8::UniquePersistent<v8::Function> on_back_gesture_progress_callback_;
+  v8::UniquePersistent<v8::Function> on_back_gesture_cancel_callback_;
+
+  v8::UniquePersistent<v8::Function> on_top_drag_gesture_done_callback_;
+  v8::UniquePersistent<v8::Function> on_top_drag_gesture_progress_callback_;
+
+  v8::UniquePersistent<v8::Function> on_right_drag_gesture_done_callback_;
+  v8::UniquePersistent<v8::Function> on_right_drag_gesture_progress_callback_;
+
+  v8::UniquePersistent<v8::Function> on_tap_gesture_callback_;
+
+  v8::UniquePersistent<v8::Function> on_tap_down_gesture_callback_;
+};
+
+}  // namespace shell
+}  // namespace chromecast
+
+#endif  // CHROMECAST_RENDERER_CAST_WINDOW_MANAGER_BINDINGS_H_
diff --git a/chromecast/renderer/extensions/extension_hooks_delegate.cc b/chromecast/renderer/extensions/extension_hooks_delegate.cc
index d9cdb57..3e7b878 100644
--- a/chromecast/renderer/extensions/extension_hooks_delegate.cc
+++ b/chromecast/renderer/extensions/extension_hooks_delegate.cc
@@ -218,7 +218,8 @@
 
   v8::Local<v8::Value> v8_message = arguments[1];
   std::unique_ptr<Message> message = messaging_util::MessageFromV8(
-      script_context->v8_context(), v8_message, &error);
+      script_context->v8_context(), v8_message,
+      messaging_util::GetSerializationFormat(*script_context), &error);
   if (!message) {
     RequestResult result(RequestResult::INVALID_INVOCATION);
     result.error = std::move(error);
diff --git a/chromecast/renderer/extensions/tabs_hooks_delegate.cc b/chromecast/renderer/extensions/tabs_hooks_delegate.cc
index c8e7663..ffcbbeab 100644
--- a/chromecast/renderer/extensions/tabs_hooks_delegate.cc
+++ b/chromecast/renderer/extensions/tabs_hooks_delegate.cc
@@ -89,7 +89,8 @@
   v8::Local<v8::Value> v8_message = arguments[1];
   std::string error;
   std::unique_ptr<Message> message = messaging_util::MessageFromV8(
-      script_context->v8_context(), v8_message, &error);
+      script_context->v8_context(), v8_message,
+      messaging_util::GetSerializationFormat(*script_context), &error);
   if (!message) {
     RequestResult result(RequestResult::INVALID_INVOCATION);
     result.error = std::move(error);
@@ -124,7 +125,8 @@
   DCHECK(!v8_message.IsEmpty());
   std::string error;
   std::unique_ptr<Message> message = messaging_util::MessageFromV8(
-      script_context->v8_context(), v8_message, &error);
+      script_context->v8_context(), v8_message,
+      messaging_util::GetSerializationFormat(*script_context), &error);
   if (!message) {
     RequestResult result(RequestResult::INVALID_INVOCATION);
     result.error = std::move(error);
@@ -158,7 +160,8 @@
 
   gin::Handle<GinPort> port = messaging_service_->Connect(
       script_context, MessageTarget::ForTab(tab_id, options.frame_id),
-      options.channel_name);
+      options.channel_name,
+      messaging_util::GetSerializationFormat(*script_context));
   DCHECK(!port.IsEmpty());
 
   RequestResult result(RequestResult::HANDLED);
diff --git a/chromecast/renderer/feature_manager.cc b/chromecast/renderer/feature_manager.cc
new file mode 100644
index 0000000..734847b
--- /dev/null
+++ b/chromecast/renderer/feature_manager.cc
@@ -0,0 +1,185 @@
+// 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 "chromecast/renderer/feature_manager.h"
+
+#include <iostream>
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/check.h"
+#include "base/logging.h"
+#include "base/values.h"
+#include "chromecast/base/cast_features.h"
+#include "chromecast/common/feature_constants.h"
+#include "chromecast/renderer/assistant_bindings.h"
+#include "chromecast/renderer/cast_accessibility_bindings.h"
+#include "chromecast/renderer/cast_demo_bindings.h"
+#include "chromecast/renderer/cast_window_manager_bindings.h"
+#include "chromecast/renderer/settings_ui_bindings.h"
+#include "content/public/renderer/render_frame.h"
+#include "content/public/renderer/render_frame_media_playback_options.h"
+#include "services/network/public/cpp/is_potentially_trustworthy.h"
+#include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h"
+#include "third_party/blink/public/platform/web_runtime_features.h"
+#include "third_party/blink/public/platform/web_security_origin.h"
+#include "third_party/blink/public/platform/web_string.h"
+#include "third_party/blink/public/web/web_local_frame.h"
+#include "third_party/blink/public/web/web_security_policy.h"
+
+namespace chromecast {
+
+FeatureManager::FeatureManager(content::RenderFrame* render_frame)
+    : content::RenderFrameObserver(render_frame),
+      configured_(false),
+      can_install_bindings_(false),
+      dev_origin_(GURL::EmptyGURL()),
+      secure_origin_set_(false) {
+  registry_.AddInterface(base::BindRepeating(
+      &FeatureManager::OnFeatureManagerRequest, base::Unretained(this)));
+}
+
+FeatureManager::~FeatureManager() {}
+
+void FeatureManager::OnInterfaceRequestForFrame(
+    const std::string& interface_name,
+    mojo::ScopedMessagePipeHandle* interface_pipe) {
+  registry_.TryBindInterface(interface_name, interface_pipe);
+}
+
+void FeatureManager::OnDestruct() {
+  delete this;
+}
+
+void FeatureManager::DidClearWindowObject() {
+  can_install_bindings_ = true;
+  if (!configured_)
+    return;
+
+  EnableBindings();
+}
+
+void FeatureManager::ConfigureFeatures(
+    std::vector<chromecast::shell::mojom::FeaturePtr> features) {
+  if (configured_)
+    return;
+  configured_ = true;
+  for (auto& feature : features) {
+    // If we want to add enabled/disabled status to FeaturePtr, we can overlap
+    // previous setting via [] operator
+    features_map_[feature->name] = std::move(feature);
+  }
+
+  ConfigureFeaturesInternal();
+
+  if (!can_install_bindings_)
+    return;
+  EnableBindings();
+}
+
+void FeatureManager::ConfigureFeaturesInternal() {
+  if (FeatureEnabled(feature::kEnableDevMode)) {
+    base::Value& dev_mode_config =
+        (features_map_.find(feature::kEnableDevMode)->second)->config;
+    base::Value* dev_mode_origin =
+        dev_mode_config.FindKey(feature::kDevModeOrigin);
+    DCHECK(dev_mode_origin);
+    dev_origin_ = GURL(dev_mode_origin->GetString());
+    DCHECK(dev_origin_.is_valid());
+  }
+
+  if (FeatureEnabled(feature::kDisableBackgroundSuspend)) {
+    auto options = render_frame()->GetRenderFrameMediaPlaybackOptions();
+    options.is_background_suspend_enabled = false;
+    render_frame()->SetRenderFrameMediaPlaybackOptions(options);
+  }
+
+  // Call feature-specific functions.
+  SetupAdditionalSecureOrigin();
+
+  // Disable timer throttling for background tabs before the frame is painted.
+  if (FeatureEnabled(feature::kDisableBackgroundTabTimerThrottle)) {
+    blink::WebRuntimeFeatures::EnableTimerThrottlingForBackgroundTabs(false);
+  }
+  if (FeatureEnabled(feature::kEnableSettingsUiMojo)) {
+    v8_bindings_.insert(new shell::SettingsUiBindings(render_frame()));
+  }
+
+  // Window manager bindings will install themselves depending on the specific
+  // feature flags enabled, so we pass the feature manager through to let it
+  // decide.
+  v8_bindings_.insert(
+      new shell::CastWindowManagerBindings(render_frame(), this));
+  // Accessibility bindings will install themselves depending on the specific
+  // feature flags enabled, so we pass the feature manager through to let it
+  // decide.
+  v8_bindings_.insert(
+      new shell::CastAccessibilityBindings(render_frame(), this));
+
+  if (FeatureEnabled(feature::kEnableDemoStandaloneMode)) {
+    v8_bindings_.insert(new shell::CastDemoBindings(render_frame()));
+  }
+
+  if (FeatureEnabled(feature::kEnableAssistantMessagePipe)) {
+    auto& feature = GetFeature(feature::kEnableAssistantMessagePipe);
+    v8_bindings_.insert(
+        new shell::AssistantBindings(render_frame(), feature->config));
+  }
+}
+
+void FeatureManager::EnableBindings() {
+  LOG(INFO) << "Enabling bindings: " << *this;
+  for (auto* binding : v8_bindings_) {
+    binding->TryInstall();
+  }
+}
+
+void FeatureManager::OnFeatureManagerRequest(
+    mojo::PendingReceiver<shell::mojom::FeatureManager> request) {
+  bindings_.Add(this, std::move(request));
+}
+
+bool FeatureManager::FeatureEnabled(const std::string& feature) const {
+  return features_map_.find(feature) != features_map_.end();
+}
+
+const chromecast::shell::mojom::FeaturePtr& FeatureManager::GetFeature(
+    const std::string& feature) const {
+  auto itor = features_map_.find(feature);
+  DCHECK(itor != features_map_.end());
+  return itor->second;
+}
+
+void FeatureManager::SetupAdditionalSecureOrigin() {
+  if (!dev_origin_.is_valid()) {
+    return;
+  }
+
+  // Secure origin should be only set once, otherwise it will cause CHECK
+  // failure when race between origin safelist changing and thread creation
+  // happens (b/63583734).
+  if (secure_origin_set_) {
+    return;
+  }
+
+  secure_origin_set_ = true;
+
+  LOG(INFO) << "Treat origin " << dev_origin_ << " as secure origin";
+
+  blink::WebSecurityPolicy::AddSchemeToSecureContextSafelist(
+      blink::WebString::FromASCII(dev_origin_.scheme()));
+
+  network::SecureOriginAllowlist::GetInstance().SetAuxiliaryAllowlist(
+      dev_origin_.spec(), nullptr);
+}
+
+std::ostream& operator<<(std::ostream& os, const FeatureManager& features) {
+  for (auto& feature : features.features_map_) {
+    os << feature.first << " ";
+  }
+  return os;
+}
+
+}  // namespace chromecast
diff --git a/chromecast/renderer/feature_manager.h b/chromecast/renderer/feature_manager.h
new file mode 100644
index 0000000..fb05dd3
--- /dev/null
+++ b/chromecast/renderer/feature_manager.h
@@ -0,0 +1,94 @@
+// 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 CHROMECAST_RENDERER_FEATURE_MANAGER_H_
+#define CHROMECAST_RENDERER_FEATURE_MANAGER_H_
+
+#include <iosfwd>
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/containers/flat_set.h"
+#include "base/values.h"
+#include "chromecast/common/mojom/feature_manager.mojom.h"
+#include "chromecast/renderer/native_bindings_helper.h"
+#include "content/public/renderer/render_frame_observer.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/receiver_set.h"
+#include "services/service_manager/public/cpp/binder_registry.h"
+#include "url/gurl.h"
+
+namespace chromecast {
+
+// Receives messages from the browser process to enable/disable Cast
+// application-facing features. Features usually have an associated V8 binding
+// which exposes a platform capability to the app.
+class FeatureManager : public content::RenderFrameObserver,
+                       public shell::mojom::FeatureManager {
+ public:
+  explicit FeatureManager(content::RenderFrame* render_frame);
+  FeatureManager(const FeatureManager&) = delete;
+  FeatureManager& operator=(const FeatureManager&) = delete;
+  ~FeatureManager() override;
+
+  const GURL& dev_origin() const { return dev_origin_; }
+  bool configured() const { return configured_; }
+
+  bool FeatureEnabled(const std::string& feature) const;
+
+  const chromecast::shell::mojom::FeaturePtr& GetFeature(
+      const std::string& feature) const;
+
+  friend std::ostream& operator<<(std::ostream& os,
+                                  const FeatureManager& features);
+
+ protected:
+  // Allows a derived class to add its own features at the end of
+  // mojom::FeatureManager::ConfigureFeatures().
+  virtual void ConfigureFeaturesInternal();
+
+  // Map for storing enabled features, name -> FeaturePtr.
+  using FeaturesMap =
+      std::map<std::string, chromecast::shell::mojom::FeaturePtr>;
+  FeaturesMap features_map_;
+
+  base::flat_set<CastBinding*> v8_bindings_;
+
+ private:
+  // content::RenderFrameObserver implementation:
+  void OnInterfaceRequestForFrame(
+      const std::string& interface_name,
+      mojo::ScopedMessagePipeHandle* interface_pipe) override;
+  void DidClearWindowObject() override;
+  void OnDestruct() override;
+
+  // shell::mojom::FeatureManager implementation
+  void ConfigureFeatures(
+      std::vector<chromecast::shell::mojom::FeaturePtr> features) override;
+
+  // Bind the incoming request with this implementation
+  void OnFeatureManagerRequest(
+      mojo::PendingReceiver<shell::mojom::FeatureManager> request);
+
+  void EnableBindings();
+  void SetupAdditionalSecureOrigin();
+
+  // Flag for when the configuration message is received from the browser.
+  bool configured_;
+  bool can_install_bindings_;
+
+  // Origin enabled for development
+  GURL dev_origin_;
+  bool secure_origin_set_;
+
+  service_manager::BinderRegistry registry_;
+
+  mojo::ReceiverSet<shell::mojom::FeatureManager> bindings_;
+};
+
+}  // namespace chromecast
+
+#endif  // CHROMECAST_RENDERER_FEATURE_MANAGER_H_
diff --git a/chromecast/renderer/settings_ui_bindings.cc b/chromecast/renderer/settings_ui_bindings.cc
new file mode 100644
index 0000000..740aaca
--- /dev/null
+++ b/chromecast/renderer/settings_ui_bindings.cc
@@ -0,0 +1,171 @@
+// 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 "chromecast/renderer/settings_ui_bindings.h"
+
+#include <vector>
+
+#include "base/bind.h"
+#include "base/check.h"
+#include "base/location.h"
+#include "content/public/renderer/render_frame.h"
+#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
+#include "third_party/blink/public/web/blink.h"
+#include "third_party/blink/public/web/web_local_frame.h"
+
+namespace chromecast {
+namespace shell {
+
+namespace {
+
+const int64_t kDelayBetweenReconnectionInMillis = 100;
+
+const char kSettingsUiName[] = "settings_ui";
+const char kSetSideSwipeHandler[] = "setSideSwipeHandler";
+const char kSetPlatformInfoHandler[] = "setPlatformInfoHandler";
+const char kRequestVisible[] = "requestVisible";
+
+}  // namespace
+
+SettingsUiBindings::SettingsUiBindings(content::RenderFrame* frame)
+    : CastBinding(frame), binding_(this), weak_factory_(this) {
+  ReconnectMojo();
+}
+
+SettingsUiBindings::~SettingsUiBindings() {}
+
+void SettingsUiBindings::HandleSideSwipe(
+    chromecast::mojom::SideSwipeEvent event,
+    chromecast::mojom::SideSwipeOrigin origin,
+    const gfx::Point& touch_location) {
+  if (side_swipe_handler_.IsEmpty()) {
+    return;
+  }
+
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::MicrotasksScope microtasks_scope(
+      isolate, v8::MicrotasksScope::kDoNotRunMicrotasks);
+  v8::HandleScope handle_scope(isolate);
+  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
+  v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
+  v8::Context::Scope context_scope(context);
+  v8::Local<v8::Function> handler =
+      v8::Local<v8::Function>::New(isolate, std::move(side_swipe_handler_));
+
+  v8::Local<v8::Number> touch_event =
+      v8::Integer::New(isolate, static_cast<int>(event));
+  v8::Local<v8::Number> touch_origin =
+      v8::Integer::New(isolate, static_cast<int>(origin));
+  v8::Local<v8::Number> touch_x = v8::Integer::New(isolate, touch_location.x());
+  v8::Local<v8::Number> touch_y = v8::Integer::New(isolate, touch_location.y());
+
+  std::vector<v8::Local<v8::Value>> args{touch_event, touch_origin, touch_x,
+                                         touch_y};
+
+  v8::MaybeLocal<v8::Value> maybe_result =
+      handler->Call(context, context->Global(), args.size(), args.data());
+
+  side_swipe_handler_ = v8::UniquePersistent<v8::Function>(isolate, handler);
+
+  v8::Local<v8::Value> result;
+  ignore_result(maybe_result.ToLocal(&result));
+}
+
+void SettingsUiBindings::SendPlatformInfo(
+    const std::string& platform_info_json) {
+  if (platform_info_handler_.IsEmpty()) {
+    pending_platform_info_json_ = platform_info_json;
+    return;
+  }
+
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  v8::MicrotasksScope microtasks_scope(
+      isolate, v8::MicrotasksScope::kDoNotRunMicrotasks);
+  v8::HandleScope handle_scope(isolate);
+  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
+  v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
+  v8::Context::Scope context_scope(context);
+  v8::Local<v8::Function> handler =
+      v8::Local<v8::Function>::New(isolate, std::move(platform_info_handler_));
+
+  v8::Local<v8::String> platform_info =
+      v8::String::NewFromUtf8(isolate, platform_info_json.data(),
+                              v8::NewStringType::kInternalized)
+          .ToLocalChecked();
+
+  std::vector<v8::Local<v8::Value>> args{platform_info};
+
+  v8::MaybeLocal<v8::Value> maybe_result =
+      handler->Call(context, context->Global(), args.size(), args.data());
+
+  platform_info_handler_ = v8::UniquePersistent<v8::Function>(isolate, handler);
+
+  v8::Local<v8::Value> result;
+  ignore_result(maybe_result.ToLocal(&result));
+}
+
+void SettingsUiBindings::Install(v8::Local<v8::Object> cast_platform,
+                                 v8::Isolate* isolate) {
+  v8::Local<v8::Object> settings_ui =
+      EnsureObjectExists(isolate, cast_platform, kSettingsUiName);
+
+  InstallBinding(isolate, settings_ui, kSetSideSwipeHandler,
+                 &SettingsUiBindings::SetSideSwipeHandler,
+                 base::Unretained(this));
+  InstallBinding(isolate, settings_ui, kSetPlatformInfoHandler,
+                 &SettingsUiBindings::SetPlatformInfoHandler,
+                 base::Unretained(this));
+  InstallBinding(isolate, settings_ui, kRequestVisible,
+                 &SettingsUiBindings::RequestVisible, base::Unretained(this));
+}
+
+void SettingsUiBindings::SetSideSwipeHandler(
+    v8::Local<v8::Function> side_swipe_handler) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  side_swipe_handler_ =
+      v8::UniquePersistent<v8::Function>(isolate, side_swipe_handler);
+}
+
+void SettingsUiBindings::SetPlatformInfoHandler(
+    v8::Local<v8::Function> platform_info_handler) {
+  v8::Isolate* isolate = blink::MainThreadIsolate();
+  platform_info_handler_ =
+      v8::UniquePersistent<v8::Function>(isolate, platform_info_handler);
+  if (!pending_platform_info_json_.empty()) {
+    SendPlatformInfo(pending_platform_info_json_);
+    pending_platform_info_json_.clear();
+  }
+}
+
+void SettingsUiBindings::RequestVisible(bool visible) {
+  if (settings_platform_ptr_) {
+    settings_platform_ptr_->RequestVisible(visible);
+  }
+}
+
+void SettingsUiBindings::ReconnectMojo() {
+  if (binding_.is_bound())
+    binding_.reset();
+  if (settings_platform_ptr_.is_bound())
+    settings_platform_ptr_.reset();
+
+  render_frame()->GetBrowserInterfaceBroker()->GetInterface(
+      settings_platform_ptr_.BindNewPipeAndPassReceiver());
+  settings_platform_ptr_.set_disconnect_handler(base::BindOnce(
+      &SettingsUiBindings::OnMojoConnectionError, weak_factory_.GetWeakPtr()));
+  settings_platform_ptr_->Connect(binding_.BindNewPipeAndPassRemote());
+  mojo_reconnect_timer_.Stop();
+}
+
+void SettingsUiBindings::OnMojoConnectionError() {
+  LOG(WARNING) << "Disconnected from settings UI MOJO. Will reconnect every "
+               << kDelayBetweenReconnectionInMillis << " milliseconds.";
+  mojo_reconnect_timer_.Start(
+      FROM_HERE,
+      base::TimeDelta::FromMilliseconds(kDelayBetweenReconnectionInMillis),
+      this, &SettingsUiBindings::ReconnectMojo);
+}
+
+}  // namespace shell
+}  // namespace chromecast
diff --git a/chromecast/renderer/settings_ui_bindings.h b/chromecast/renderer/settings_ui_bindings.h
new file mode 100644
index 0000000..b5b75d9d
--- /dev/null
+++ b/chromecast/renderer/settings_ui_bindings.h
@@ -0,0 +1,69 @@
+// 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 CHROMECAST_RENDERER_SETTINGS_UI_BINDINGS_H_
+#define CHROMECAST_RENDERER_SETTINGS_UI_BINDINGS_H_
+
+#include <string>
+
+#include "base/memory/weak_ptr.h"
+#include "base/timer/timer.h"
+#include "chromecast/common/mojom/settings_ui.mojom.h"
+#include "chromecast/renderer/native_bindings_helper.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "v8/include/v8.h"
+
+namespace chromecast {
+namespace shell {
+
+// Exposed to the platform settings UI app on display assistants. Handles
+// certain swipe events to show/hide the settings menu. This also receives
+// platform information periodically to update the device properties in the
+// settings UI.
+class SettingsUiBindings : public CastBinding,
+                           public chromecast::mojom::SettingsClient {
+ public:
+  explicit SettingsUiBindings(content::RenderFrame* frame);
+  ~SettingsUiBindings() override;
+  SettingsUiBindings(const SettingsUiBindings&) = delete;
+  SettingsUiBindings& operator=(const SettingsUiBindings&) = delete;
+
+ private:
+  friend class ::chromecast::CastBinding;
+
+  // mojom::SettingsClient implementation:
+  void HandleSideSwipe(chromecast::mojom::SideSwipeEvent event,
+                       chromecast::mojom::SideSwipeOrigin origin,
+                       const gfx::Point& touch_location) override;
+  void SendPlatformInfo(const std::string& platform_info_json) override;
+
+  // CastBinding implementation:
+  void Install(v8::Local<v8::Object> cast_platform,
+               v8::Isolate* isolate) override;
+
+  // Binding methods
+  void SetSideSwipeHandler(v8::Local<v8::Function> side_swipe_handler);
+  void SetPlatformInfoHandler(v8::Local<v8::Function> platform_info_handler);
+  void RequestVisible(bool visible);
+
+  void ReconnectMojo();
+  void OnMojoConnectionError();
+
+  std::string pending_platform_info_json_;
+
+  v8::UniquePersistent<v8::Function> side_swipe_handler_;
+  v8::UniquePersistent<v8::Function> platform_info_handler_;
+
+  mojo::Remote<chromecast::mojom::SettingsPlatform> settings_platform_ptr_;
+  mojo::Receiver<chromecast::mojom::SettingsClient> binding_;
+  base::RepeatingTimer mojo_reconnect_timer_;
+
+  base::WeakPtrFactory<SettingsUiBindings> weak_factory_;
+};
+
+}  // namespace shell
+}  // namespace chromecast
+
+#endif  // CHROMECAST_RENDERER_SETTINGS_UI_BINDINGS_H_
diff --git a/chromeos/chromeos_strings.grd b/chromeos/chromeos_strings.grd
index 5500cf9..604e047 100644
--- a/chromeos/chromeos_strings.grd
+++ b/chromeos/chromeos_strings.grd
@@ -1065,13 +1065,13 @@
         Can't resolve DNS from Android apps
       </message>
       <message name="IDS_NETWORK_DIAGNOSTICS_NO_IP_ADDRESS_TEXT" desc="The text shown when a network's IP Address is missing.">
-        Unable to obtain IP address
+        IP address is unavailable
       </message>
       <message name="IDS_NETWORK_DIAGNOSTICS_VISIT_SETTINGS_TO_CONFIGURE_LINK_TEXT" desc="Help text that directs users to settings to configure their network.">
-        Visit settings to configure
+        To set up, go to Settings
       </message>
       <message name="IDS_NETWORK_DIAGNOSTICS_MISSING_NAME_SERVERS_TEXT" desc="The text shown when no name servers are configured.">
-        No name servers are configured
+        DNS isn't set up
       </message>
       <message name="IDS_DIAGNOSTICS_NETWORK_SIGNAL_STRENGTH_WEAK" desc="Label used to describe the signal strength of a network connection when the signal is weak.">
         Weak (<ph name="SIGNAL_STRENGTH">$1<ex>(20)</ex></ph>)
diff --git a/chromeos/chromeos_strings_grd/IDS_NETWORK_DIAGNOSTICS_MISSING_NAME_SERVERS_TEXT.png.sha1 b/chromeos/chromeos_strings_grd/IDS_NETWORK_DIAGNOSTICS_MISSING_NAME_SERVERS_TEXT.png.sha1
index e88da440..0072e72 100644
--- a/chromeos/chromeos_strings_grd/IDS_NETWORK_DIAGNOSTICS_MISSING_NAME_SERVERS_TEXT.png.sha1
+++ b/chromeos/chromeos_strings_grd/IDS_NETWORK_DIAGNOSTICS_MISSING_NAME_SERVERS_TEXT.png.sha1
@@ -1 +1 @@
-73746946099c4c974852e63234501effa4f798f1
\ No newline at end of file
+2f4cd218cecf2d8eb157f7bd3351bf17ebc14542
\ No newline at end of file
diff --git a/chromeos/chromeos_strings_grd/IDS_NETWORK_DIAGNOSTICS_NO_IP_ADDRESS_TEXT.png.sha1 b/chromeos/chromeos_strings_grd/IDS_NETWORK_DIAGNOSTICS_NO_IP_ADDRESS_TEXT.png.sha1
index 6a08a142..235cdd0 100644
--- a/chromeos/chromeos_strings_grd/IDS_NETWORK_DIAGNOSTICS_NO_IP_ADDRESS_TEXT.png.sha1
+++ b/chromeos/chromeos_strings_grd/IDS_NETWORK_DIAGNOSTICS_NO_IP_ADDRESS_TEXT.png.sha1
@@ -1 +1 @@
-56b002c1ffe8572c90dc532fd18de231a2823ae5
\ No newline at end of file
+d4521a2b262d7345cf7c470069a9dc378f5918ef
\ No newline at end of file
diff --git a/chromeos/chromeos_strings_grd/IDS_NETWORK_DIAGNOSTICS_VISIT_SETTINGS_TO_CONFIGURE_LINK_TEXT.png.sha1 b/chromeos/chromeos_strings_grd/IDS_NETWORK_DIAGNOSTICS_VISIT_SETTINGS_TO_CONFIGURE_LINK_TEXT.png.sha1
index 6a08a142..235cdd0 100644
--- a/chromeos/chromeos_strings_grd/IDS_NETWORK_DIAGNOSTICS_VISIT_SETTINGS_TO_CONFIGURE_LINK_TEXT.png.sha1
+++ b/chromeos/chromeos_strings_grd/IDS_NETWORK_DIAGNOSTICS_VISIT_SETTINGS_TO_CONFIGURE_LINK_TEXT.png.sha1
@@ -1 +1 @@
-56b002c1ffe8572c90dc532fd18de231a2823ae5
\ No newline at end of file
+d4521a2b262d7345cf7c470069a9dc378f5918ef
\ No newline at end of file
diff --git a/chromeos/components/eche_app_ui/BUILD.gn b/chromeos/components/eche_app_ui/BUILD.gn
index 03e52027..16e1b22 100644
--- a/chromeos/components/eche_app_ui/BUILD.gn
+++ b/chromeos/components/eche_app_ui/BUILD.gn
@@ -32,6 +32,8 @@
     "eche_notification_click_handler.h",
     "eche_notification_generator.cc",
     "eche_notification_generator.h",
+    "eche_presence_manager.cc",
+    "eche_presence_manager.h",
     "eche_recent_app_click_handler.cc",
     "eche_recent_app_click_handler.h",
     "eche_signaler.cc",
@@ -67,7 +69,8 @@
     "//chromeos/resources:eche_bundle_resources",
     "//chromeos/services/device_sync/public/cpp:cpp",
     "//chromeos/services/multidevice_setup/public/cpp:cpp",
-    "//chromeos/services/secure_channel/public/cpp/client:client",
+    "//chromeos/services/secure_channel/public/cpp/client",
+    "//chromeos/services/secure_channel/public/cpp/shared",
     "//components/prefs",
     "//content/public/browser",
     "//mojo/public/js:resources",
diff --git a/chromeos/components/eche_app_ui/eche_app_manager.cc b/chromeos/components/eche_app_ui/eche_app_manager.cc
index 3b1bc3d..96803be 100644
--- a/chromeos/components/eche_app_ui/eche_app_manager.cc
+++ b/chromeos/components/eche_app_ui/eche_app_manager.cc
@@ -8,6 +8,7 @@
 #include "base/system/sys_info.h"
 #include "chromeos/components/eche_app_ui/eche_message_receiver.h"
 #include "chromeos/components/eche_app_ui/eche_notification_generator.h"
+#include "chromeos/components/eche_app_ui/eche_presence_manager.h"
 #include "chromeos/components/eche_app_ui/eche_signaler.h"
 #include "chromeos/components/eche_app_ui/eche_uid_provider.h"
 #include "chromeos/components/eche_app_ui/launch_app_helper.h"
@@ -32,6 +33,8 @@
     device_sync::DeviceSyncClient* device_sync_client,
     multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client,
     secure_channel::SecureChannelClient* secure_channel_client,
+    std::unique_ptr<secure_channel::PresenceMonitorClient>
+        presence_monitor_client,
     LaunchAppHelper::LaunchEcheAppFunction launch_eche_app_function,
     LaunchAppHelper::CloseEcheAppFunction close_eche_app_function,
     LaunchAppHelper::LaunchNotificationFunction launch_notification_function)
@@ -64,6 +67,12 @@
                                           connection_manager_.get())),
       signaler_(std::make_unique<EcheSignaler>(eche_connector_.get(),
                                                connection_manager_.get())),
+      eche_presence_manager_(std::make_unique<EchePresenceManager>(
+          feature_status_provider_.get(),
+          device_sync_client,
+          multidevice_setup_client,
+          std::move(presence_monitor_client),
+          eche_connector_.get())),
       uid_(std::make_unique<EcheUidProvider>(pref_service)),
       eche_recent_app_click_handler_(
           std::make_unique<EcheRecentAppClickHandler>(
@@ -105,16 +114,17 @@
 // NOTE: These should be destroyed in the opposite order of how these objects
 // are initialized in the constructor.
 void EcheAppManager::Shutdown() {
+  system_info_provider_.reset();
+  message_receiver_.reset();
   notification_generator_.reset();
   eche_recent_app_click_handler_.reset();
   uid_.reset();
-  system_info_provider_.reset();
+  eche_presence_manager_.reset();
   signaler_.reset();
   eche_connector_.reset();
   eche_notification_click_handler_.reset();
   launch_app_helper_.reset();
   feature_status_provider_.reset();
-  message_receiver_.reset();
   connection_manager_.reset();
 }
 
diff --git a/chromeos/components/eche_app_ui/eche_app_manager.h b/chromeos/components/eche_app_ui/eche_app_manager.h
index 28d7312..0895615 100644
--- a/chromeos/components/eche_app_ui/eche_app_manager.h
+++ b/chromeos/components/eche_app_ui/eche_app_manager.h
@@ -6,6 +6,7 @@
 #define CHROMEOS_COMPONENTS_ECHE_APP_UI_ECHE_APP_MANAGER_H_
 
 #include <stdint.h>
+#include <memory>
 
 #include "chromeos/components/eche_app_ui/eche_connector.h"
 #include "chromeos/components/eche_app_ui/eche_feature_status_provider.h"
@@ -15,6 +16,7 @@
 #include "chromeos/components/eche_app_ui/mojom/eche_app.mojom.h"
 #include "chromeos/components/phonehub/phone_hub_manager.h"
 #include "chromeos/services/network_config/public/mojom/cros_network_config.mojom.h"
+#include "chromeos/services/secure_channel/public/cpp/client/presence_monitor_client_impl.h"
 #include "components/keyed_service/core/keyed_service.h"
 #include "mojo/public/cpp/bindings/remote.h"
 
@@ -37,12 +39,13 @@
 
 namespace eche_app {
 
-class SystemInfo;
-class EcheSignaler;
-class SystemInfoProvider;
-class EcheUidProvider;
-class EcheNotificationGenerator;
 class EcheMessageReceiver;
+class EcheNotificationGenerator;
+class EchePresenceManager;
+class EcheSignaler;
+class EcheUidProvider;
+class SystemInfo;
+class SystemInfoProvider;
 
 // Implements the core logic of the EcheApp and exposes interfaces via its
 // public API. Implemented as a KeyedService since it depends on other
@@ -55,6 +58,8 @@
                  device_sync::DeviceSyncClient*,
                  multidevice_setup::MultiDeviceSetupClient*,
                  secure_channel::SecureChannelClient*,
+                 std::unique_ptr<secure_channel::PresenceMonitorClient>
+                     presence_monitor_client,
                  LaunchAppHelper::LaunchEcheAppFunction,
                  LaunchAppHelper::CloseEcheAppFunction,
                  LaunchAppHelper::LaunchNotificationFunction);
@@ -86,6 +91,7 @@
       eche_notification_click_handler_;
   std::unique_ptr<EcheConnector> eche_connector_;
   std::unique_ptr<EcheSignaler> signaler_;
+  std::unique_ptr<EchePresenceManager> eche_presence_manager_;
   std::unique_ptr<EcheUidProvider> uid_;
   std::unique_ptr<EcheRecentAppClickHandler> eche_recent_app_click_handler_;
   std::unique_ptr<EcheNotificationGenerator> notification_generator_;
diff --git a/chromeos/components/eche_app_ui/eche_connector.cc b/chromeos/components/eche_app_ui/eche_connector.cc
index 51099673..aa2aff72 100644
--- a/chromeos/components/eche_app_ui/eche_connector.cc
+++ b/chromeos/components/eche_app_ui/eche_connector.cc
@@ -10,7 +10,6 @@
 #include "chromeos/components/multidevice/software_feature.h"
 #include "chromeos/components/multidevice/software_feature_state.h"
 #include "chromeos/components/phonehub/phone_hub_manager.h"
-#include "chromeos/services/device_sync/public/cpp/device_sync_client.h"
 #include "chromeos/services/secure_channel/public/cpp/client/connection_manager.h"
 
 namespace chromeos {
@@ -84,12 +83,12 @@
   const FeatureStatus feature_status =
       eche_feature_status_provider_->GetStatus();
   if (feature_status == FeatureStatus::kConnected && !queue_.empty()) {
+    PA_LOG(INFO) << "Flushing message queue";
     FlushQueue();
   }
 }
 
 void EcheConnector::FlushQueue() {
-  PA_LOG(INFO) << "Flushing message queue";
   const int size = queue_.size();
   for (int i = 0; i < size; i++) {
     connection_manager_->SendMessage(queue_.front());
diff --git a/chromeos/components/eche_app_ui/eche_presence_manager.cc b/chromeos/components/eche_app_ui/eche_presence_manager.cc
new file mode 100644
index 0000000..57f31cd
--- /dev/null
+++ b/chromeos/components/eche_app_ui/eche_presence_manager.cc
@@ -0,0 +1,146 @@
+// 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 "chromeos/components/eche_app_ui/eche_presence_manager.h"
+
+#include "chromeos/components/eche_app_ui/eche_connector.h"
+#include "chromeos/components/eche_app_ui/proto/exo_messages.pb.h"
+#include "chromeos/components/multidevice/logging/logging.h"
+#include "chromeos/components/multidevice/remote_device_ref.h"
+#include "chromeos/services/device_sync/public/cpp/device_sync_client.h"
+#include "chromeos/services/secure_channel/public/cpp/client/presence_monitor_client.h"
+
+namespace chromeos {
+namespace eche_app {
+
+namespace {
+
+// How often to check whether the last seen time is below the maximum age.
+constexpr base::TimeDelta kTimerInterval = base::Seconds(30);
+// The maximum age of the last seen time before the connection must be
+// terminated.
+constexpr base::TimeDelta kMaximumLastSeenAge = base::Minutes(5);
+
+}  // namespace
+
+EchePresenceManager::EchePresenceManager(
+    EcheFeatureStatusProvider* eche_feature_status_provider,
+    device_sync::DeviceSyncClient* device_sync_client,
+    multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client,
+    std::unique_ptr<secure_channel::PresenceMonitorClient>
+        presence_monitor_client,
+    EcheConnector* eche_connector)
+    : eche_feature_status_provider_(eche_feature_status_provider),
+      device_sync_client_(device_sync_client),
+      multidevice_setup_client_(multidevice_setup_client),
+      presence_monitor_client_(std::move(presence_monitor_client)),
+      eche_connector_(eche_connector) {
+  eche_feature_status_provider_->AddObserver(this);
+  presence_monitor_client_->SetPresenceMonitorCallbacks(
+      base::BindRepeating(&EchePresenceManager::OnReady,
+                          weak_ptr_factory_.GetWeakPtr()),
+      base::BindRepeating(&EchePresenceManager::OnDeviceSeen,
+                          weak_ptr_factory_.GetWeakPtr()));
+}
+
+EchePresenceManager::~EchePresenceManager() {
+  eche_feature_status_provider_->RemoveObserver(this);
+}
+
+void EchePresenceManager::OnFeatureStatusChanged() {
+  UpdateMonitoringStatus();
+}
+
+void EchePresenceManager::OnReady() {
+  UpdateMonitoringStatus();
+}
+
+void EchePresenceManager::UpdateMonitoringStatus() {
+  const FeatureStatus feature_status =
+      eche_feature_status_provider_->GetStatus();
+  switch (feature_status) {
+    case FeatureStatus::kIneligible:
+      ABSL_FALLTHROUGH_INTENDED;
+    case FeatureStatus::kDisabled:
+      ABSL_FALLTHROUGH_INTENDED;
+    case FeatureStatus::kDependentFeature:
+      ABSL_FALLTHROUGH_INTENDED;
+    case FeatureStatus::kDependentFeaturePending:
+      ABSL_FALLTHROUGH_INTENDED;
+    case FeatureStatus::kDisconnected:
+      ABSL_FALLTHROUGH_INTENDED;
+    case FeatureStatus::kConnecting:
+      StopMonitoring();
+      break;
+
+    case FeatureStatus::kConnected:
+      StartMonitoring();
+      break;
+  }
+}
+
+void EchePresenceManager::StartMonitoring() {
+  if (is_monitoring_) {
+    return;
+  }
+
+  const absl::optional<multidevice::RemoteDeviceRef> remote_device_ref =
+      multidevice_setup_client_->GetHostStatus().second;
+  const absl::optional<multidevice::RemoteDeviceRef> local_device_ref =
+      device_sync_client_->GetLocalDeviceMetadata();
+  if (!remote_device_ref || !local_device_ref) {
+    return;
+  }
+
+  // Before we start monitoring we don't know when the device was last seen, so
+  // set it to something far out of the expected time window to start. This will
+  // be properly set once we see the first advertisement, and if one is not seen
+  // before the first check, will return that the devices are out of proximity.
+  device_last_seen_time_ = base::TimeTicks::UnixEpoch();
+  is_monitoring_ = true;
+
+  if (timer_.IsRunning()) {
+    timer_.Reset();
+  } else {
+    timer_.Start(FROM_HERE, kTimerInterval,
+                 base::BindRepeating(&EchePresenceManager::OnTimerExpired,
+                                     weak_ptr_factory_.GetWeakPtr()));
+  }
+
+  presence_monitor_client_->StartMonitoring(remote_device_ref.value(),
+                                            local_device_ref.value());
+}
+
+void EchePresenceManager::StopMonitoring() {
+  if (!is_monitoring_) {
+    return;
+  }
+
+  timer_.Stop();
+  presence_monitor_client_->StopMonitoring();
+  is_monitoring_ = false;
+}
+
+void EchePresenceManager::OnTimerExpired() {
+  if ((base::TimeTicks::Now() - device_last_seen_time_) > kMaximumLastSeenAge) {
+    PA_LOG(INFO) << "Proximity has not been maintained; stopping monitoring";
+    StopMonitoring();
+  } else {
+    PA_LOG(INFO) << "Proximity has been maintained; sending ping";
+    proto::ProximityPing ping;
+    proto::ExoMessage message;
+    *message.mutable_proximity_ping() = std::move(ping);
+    eche_connector_->SendMessage(message.SerializeAsString());
+  }
+}
+
+void EchePresenceManager::OnDeviceSeen() {
+  // It is the responsibility of the scanner to ensure outdated advertisements
+  // are not forwarded through, so we will treat all received advertisements as
+  // valid.
+  device_last_seen_time_ = base::TimeTicks::Now();
+}
+
+}  // namespace eche_app
+}  // namespace chromeos
diff --git a/chromeos/components/eche_app_ui/eche_presence_manager.h b/chromeos/components/eche_app_ui/eche_presence_manager.h
new file mode 100644
index 0000000..4935e88
--- /dev/null
+++ b/chromeos/components/eche_app_ui/eche_presence_manager.h
@@ -0,0 +1,83 @@
+// 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 CHROMEOS_COMPONENTS_ECHE_APP_UI_ECHE_PRESENCE_MANAGER_H_
+#define CHROMEOS_COMPONENTS_ECHE_APP_UI_ECHE_PRESENCE_MANAGER_H_
+
+#include <memory>
+
+#include "base/memory/weak_ptr.h"
+#include "base/timer/timer.h"
+#include "chromeos/components/eche_app_ui/eche_feature_status_provider.h"
+#include "chromeos/components/eche_app_ui/feature_status_provider.h"
+
+namespace chromeos {
+
+namespace device_sync {
+
+class DeviceSyncClient;
+
+}  // namespace device_sync
+
+namespace multidevice_setup {
+
+class MultiDeviceSetupClient;
+
+}  // namespace multidevice_setup
+
+namespace secure_channel {
+
+class PresenceMonitorClient;
+
+}  // namespace secure_channel
+
+namespace eche_app {
+
+class EcheConnector;
+
+// Control presence monitoring and the sending of keepalives.
+class EchePresenceManager : public FeatureStatusProvider::Observer {
+ public:
+  EchePresenceManager(
+      EcheFeatureStatusProvider* eche_feature_status_provider,
+      device_sync::DeviceSyncClient* device_sync_client,
+      multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client,
+      std::unique_ptr<secure_channel::PresenceMonitorClient>
+          presence_monitor_client,
+      EcheConnector* eche_connector);
+  ~EchePresenceManager() override;
+
+  EchePresenceManager(const EchePresenceManager&) = delete;
+  EchePresenceManager& operator=(const EchePresenceManager&) = delete;
+
+ private:
+  // FeatureStatusProvider::Observer:
+  void OnFeatureStatusChanged() override;
+
+  void OnReady();
+  void OnDeviceSeen();
+
+  void UpdateMonitoringStatus();
+  void StartMonitoring();
+  void StopMonitoring();
+  void OnTimerExpired();
+
+  EcheFeatureStatusProvider* eche_feature_status_provider_;
+  device_sync::DeviceSyncClient* device_sync_client_;
+  multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client_;
+  std::unique_ptr<secure_channel::PresenceMonitorClient>
+      presence_monitor_client_;
+  EcheConnector* eche_connector_;
+  base::RepeatingTimer timer_;
+
+  bool is_monitoring_ = false;
+  base::TimeTicks device_last_seen_time_;
+
+  base::WeakPtrFactory<EchePresenceManager> weak_ptr_factory_{this};
+};
+
+}  // namespace eche_app
+}  // namespace chromeos
+
+#endif  // CHROMEOS_COMPONENTS_ECHE_APP_UI_ECHE_PRESENCE_MANAGER_H_
diff --git a/chromeos/components/multidevice/remote_device_ref.h b/chromeos/components/multidevice/remote_device_ref.h
index d943418..65047f36 100644
--- a/chromeos/components/multidevice/remote_device_ref.h
+++ b/chromeos/components/multidevice/remote_device_ref.h
@@ -23,6 +23,7 @@
 }  // namespace multidevice_setup
 
 namespace secure_channel {
+class PresenceMonitorClientImpl;
 class SecureChannelClientImpl;
 }  // namespace secure_channel
 
@@ -92,6 +93,7 @@
  private:
   friend class multidevice_setup::MultiDeviceSetupImpl;
   friend class secure_channel::SecureChannelClientImpl;
+  friend class secure_channel::PresenceMonitorClientImpl;
   friend class RemoteDeviceCache;
   friend class RemoteDeviceRefBuilder;
   friend class RemoteDeviceRefTest;
diff --git a/chromeos/services/bluetooth_config/OWNERS b/chromeos/services/bluetooth_config/OWNERS
index 5189b388..33cd4c86 100644
--- a/chromeos/services/bluetooth_config/OWNERS
+++ b/chromeos/services/bluetooth_config/OWNERS
@@ -1 +1,2 @@
-khorimoto@chromium.org
+gordonseto@google.com
+khorimoto@chromium.org
\ No newline at end of file
diff --git a/chromeos/services/libassistant/conversation_controller.cc b/chromeos/services/libassistant/conversation_controller.cc
index d24f753..8bb637a 100644
--- a/chromeos/services/libassistant/conversation_controller.cc
+++ b/chromeos/services/libassistant/conversation_controller.cc
@@ -11,9 +11,11 @@
 #include "base/thread_annotations.h"
 #include "base/threading/sequenced_task_runner_handle.h"
 #include "chromeos/assistant/internal/internal_util.h"
+#include "chromeos/assistant/internal/proto/shared/proto/v2/internal_options.pb.h"
 #include "chromeos/services/assistant/public/cpp/features.h"
 #include "chromeos/services/libassistant/grpc/assistant_client.h"
 #include "chromeos/services/libassistant/public/mojom/conversation_controller.mojom.h"
+#include "chromeos/services/libassistant/util.h"
 #include "chromeos/strings/grit/chromeos_strings.h"
 #include "libassistant/shared/internal_api/assistant_manager_delegate.h"
 #include "libassistant/shared/internal_api/assistant_manager_internal.h"
@@ -257,15 +259,13 @@
 void ConversationController::OnAssistantClientRunning(
     AssistantClient* assistant_client) {
   // Only when Libassistant is running we can start sending queries.
-  assistant_manager_ = assistant_client->assistant_manager();
-  assistant_manager_internal_ = assistant_client->assistant_manager_internal();
+  assistant_client_ = assistant_client;
   requests_are_allowed_ = true;
 }
 
 void ConversationController::OnDestroyingAssistantClient(
     AssistantClient* assistant_client) {
-  assistant_manager_ = nullptr;
-  assistant_manager_internal_ = nullptr;
+  assistant_client_ = nullptr;
 }
 
 void ConversationController::SendTextQuery(const std::string& query,
@@ -275,28 +275,27 @@
 
   DCHECK(requests_are_allowed_)
       << "Should not receive requests before Libassistant is running";
-  if (!assistant_manager_internal_)
+  if (!assistant_client_)
     return;
 
   MaybeStopPreviousInteraction();
 
   // Configs |VoicelessOptions|.
-  assistant_client::VoicelessOptions options;
-  options.is_user_initiated = true;
+  ::assistant::api::VoicelessOptions options;
+  options.set_is_user_initiated(true);
   if (!allow_tts) {
-    options.modality =
-        assistant_client::VoicelessOptions::Modality::TYPING_MODALITY;
+    options.set_modality(::assistant::api::VoicelessOptions::TYPING_MODALITY);
   }
   // Remember the interaction metadata, and pass the generated conversation id
   // to LibAssistant.
-  options.conversation_turn_id =
-      assistant_manager_delegate_->AddPendingTextInteraction(query, source);
+  options.set_conversation_turn_id(
+      assistant_manager_delegate_->AddPendingTextInteraction(query, source));
 
   // Builds text interaction.
-  std::string interaction = assistant::CreateTextQueryInteraction(query);
+  auto interaction = CreateTextQueryInteraction(query);
 
-  assistant_manager_internal_->SendVoicelessInteraction(
-      interaction, /*description=*/"text_query", options, [](auto) {});
+  assistant_client_->SendVoicelessInteraction(
+      interaction, /*description=*/"text_query", options, base::DoNothing());
 }
 
 void ConversationController::StartVoiceInteraction() {
@@ -304,21 +303,21 @@
 
   DCHECK(requests_are_allowed_)
       << "Should not receive requests before Libassistant is running";
-  if (!assistant_manager_) {
+  if (!assistant_client_) {
     VLOG(1) << "Starting voice interaction without assistant manager.";
     return;
   }
 
   MaybeStopPreviousInteraction();
 
-  assistant_manager_->StartAssistantInteraction();
+  assistant_client_->StartVoiceInteraction();
 }
 
 void ConversationController::StartEditReminderInteraction(
     const std::string& client_id) {
   DCHECK(requests_are_allowed_)
       << "Should not receive requests before Libassistant is running";
-  if (!assistant_manager_internal_)
+  if (!assistant_client_)
     return;
 
   // Cancels any ongoing StopInteraction posted by StopActiveInteraction()
@@ -328,9 +327,12 @@
   // See b/182948180.
   MaybeStopPreviousInteraction();
 
-  SendVoicelessInteraction(assistant::CreateEditReminderInteraction(client_id),
-                           /*description=*/std::string(),
-                           /*is_user_initiated=*/true);
+  ::assistant::api::VoicelessOptions options;
+  options.set_is_user_initiated(true);
+
+  assistant_client_->SendVoicelessInteraction(
+      CreateEditReminderInteraction(client_id),
+      /*description=*/std::string(), options, base::DoNothing());
 }
 
 void ConversationController::StartScreenContextInteraction(
@@ -338,7 +340,7 @@
     const std::vector<uint8_t>& screenshot) {
   DCHECK(requests_are_allowed_)
       << "Should not receive requests before Libassistant is running";
-  if (!assistant_manager_internal_)
+  if (!assistant_client_)
     return;
 
   MaybeStopPreviousInteraction();
@@ -364,11 +366,11 @@
   context_protos.emplace_back(
       chromeos::assistant::CreateContextProto(screenshot,
                                               /*is_first_query=*/true));
-  assistant_manager_internal_->SendScreenContextRequest(context_protos);
+  assistant_client_->SendScreenContextRequest(context_protos);
 }
 
 void ConversationController::StopActiveInteraction(bool cancel_conversation) {
-  if (!assistant_manager_internal_) {
+  if (!assistant_client_) {
     VLOG(1) << "Stopping interaction without assistant manager.";
     return;
   }
@@ -378,12 +380,11 @@
   // stability as Libassistant might misbehave when it's forcefully stopped.
   auto stop_callback = [](base::WeakPtr<ConversationController> weak_this,
                           bool cancel_conversation) {
-    if (!weak_this || !weak_this->assistant_manager_internal_) {
+    if (!weak_this || !weak_this->assistant_client_) {
       return;
     }
     VLOG(1) << "Stopping Assistant interaction.";
-    weak_this->assistant_manager_internal_->StopAssistantInteractionInternal(
-        cancel_conversation);
+    weak_this->assistant_client_->StopAssistantInteraction(cancel_conversation);
   };
 
   stop_interaction_closure_ =
@@ -400,55 +401,59 @@
     int32_t action_index) {
   DCHECK(requests_are_allowed_)
       << "Should not receive requests before Libassistant is running";
-  if (!assistant_manager_internal_)
+  if (!assistant_client_)
     return;
 
-  const std::string request_interaction =
-      assistant::SerializeNotificationRequestInteraction(
-          notification.server_id, notification.consistency_token,
-          notification.opaque_token, action_index);
+  auto request_interaction = CreateNotificationRequestInteraction(
+      notification.server_id, notification.consistency_token,
+      notification.opaque_token, action_index);
 
-  SendVoicelessInteraction(request_interaction,
-                           /*description=*/"RequestNotification",
-                           /*is_user_initiated=*/true);
+  ::assistant::api::VoicelessOptions options;
+  options.set_is_user_initiated(true);
+
+  assistant_client_->SendVoicelessInteraction(
+      request_interaction,
+      /*description=*/"RequestNotification", options, base::DoNothing());
 }
 
 void ConversationController::DismissNotification(
     AssistantNotification notification) {
   DCHECK(requests_are_allowed_)
       << "Should not receive requests before Libassistant is running";
-  if (!assistant_manager_internal_)
+  if (!assistant_client_)
     return;
 
-  const std::string dismissed_interaction =
-      assistant::SerializeNotificationDismissedInteraction(
-          notification.server_id, notification.consistency_token,
-          notification.opaque_token, {notification.grouping_key});
+  auto dismissed_interaction = CreateNotificationDismissedInteraction(
+      notification.server_id, notification.consistency_token,
+      notification.opaque_token, {notification.grouping_key});
 
-  assistant_client::VoicelessOptions options;
-  options.obfuscated_gaia_id = notification.obfuscated_gaia_id;
+  ::assistant::api::VoicelessOptions options;
+  options.set_obfuscated_gaia_id(notification.obfuscated_gaia_id);
 
-  assistant_manager_internal_->SendVoicelessInteraction(
+  assistant_client_->SendVoicelessInteraction(
       dismissed_interaction, /*description=*/"DismissNotification", options,
-      [](auto) {});
+      base::DoNothing());
 }
 
 void ConversationController::SendAssistantFeedback(
     const AssistantFeedback& feedback) {
   DCHECK(requests_are_allowed_)
       << "Should not receive requests before Libassistant is running";
-  if (!assistant_manager_internal_)
+  if (!assistant_client_)
     return;
 
   std::string raw_image_data(feedback.screenshot_png.begin(),
                              feedback.screenshot_png.end());
-  const std::string interaction = assistant::CreateSendFeedbackInteraction(
-      feedback.assistant_debug_info_allowed, feedback.description,
-      raw_image_data);
+  auto interaction =
+      CreateSendFeedbackInteraction(feedback.assistant_debug_info_allowed,
+                                    feedback.description, raw_image_data);
 
-  SendVoicelessInteraction(interaction,
-                           /*description=*/"send feedback with details",
-                           /*is_user_initiated=*/false);
+  ::assistant::api::VoicelessOptions options;
+  options.set_is_user_initiated(false);
+
+  assistant_client_->SendVoicelessInteraction(
+      interaction, /*description=*/"send feedback with details", options,
+      base::DoNothing());
 }
 
 void ConversationController::AddRemoteObserver(
@@ -516,15 +521,14 @@
   // Note that we will always set |provider_found| to true since the preceding
   // OnVerifyAndroidApp() should already confirm that the requested provider is
   // available on the device.
-  std::string interaction_proto =
-      assistant::CreateOpenProviderResponseInteraction(
-          interaction.interaction_id, /*provider_found=*/true);
-  assistant_client::VoicelessOptions options;
-  options.obfuscated_gaia_id = interaction.user_id;
+  auto interaction_proto = CreateOpenProviderResponseInteraction(
+      interaction.interaction_id, /*provider_found=*/true);
+  ::assistant::api::VoicelessOptions options;
+  options.set_obfuscated_gaia_id(interaction.user_id);
 
-  assistant_manager_internal_->SendVoicelessInteraction(
+  assistant_client_->SendVoicelessInteraction(
       interaction_proto, /*description=*/"open_provider_response", options,
-      [](auto) {});
+      base::DoNothing());
 }
 
 // Called from Libassistant thread.
@@ -582,16 +586,5 @@
   stop_interaction_closure_->callback().Run();
 }
 
-void ConversationController::SendVoicelessInteraction(
-    const std::string& interaction,
-    const std::string& description,
-    bool is_user_initiated) {
-  assistant_client::VoicelessOptions voiceless_options;
-  voiceless_options.is_user_initiated = is_user_initiated;
-
-  assistant_manager_internal_->SendVoicelessInteraction(
-      interaction, description, voiceless_options, [](auto) {});
-}
-
 }  // namespace libassistant
 }  // namespace chromeos
diff --git a/chromeos/services/libassistant/conversation_controller.h b/chromeos/services/libassistant/conversation_controller.h
index 51f5ac0..80843e5 100644
--- a/chromeos/services/libassistant/conversation_controller.h
+++ b/chromeos/services/libassistant/conversation_controller.h
@@ -21,11 +21,6 @@
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote_set.h"
 
-namespace assistant_client {
-class AssistantManager;
-class AssistantManagerInternal;
-}  // namespace assistant_client
-
 namespace chromeos {
 namespace assistant {
 namespace action {
@@ -35,6 +30,8 @@
 
 namespace libassistant {
 
+class AssistantClient;
+
 class COMPONENT_EXPORT(LIBASSISTANT_SERVICE) ConversationController
     : public mojom::ConversationController,
       public AssistantClientObserver,
@@ -117,19 +114,17 @@
 
   void MaybeStopPreviousInteraction();
 
-  void SendVoicelessInteraction(const std::string& interaction,
-                                const std::string& description,
-                                bool is_user_initiated);
-
   mojo::Receiver<mojom::ConversationController> receiver_;
   mojo::RemoteSet<mojom::ConversationObserver> observers_;
   mojo::RemoteSet<mojom::AuthenticationStateObserver>
       authentication_state_observers_;
   mojo::Remote<mojom::NotificationDelegate> notification_delegate_;
 
-  assistant_client::AssistantManager* assistant_manager_ = nullptr;
-  assistant_client::AssistantManagerInternal* assistant_manager_internal_ =
-      nullptr;
+  // Owned by ServiceController.
+  // Set in `OnAssistantClientCreated()` and unset in
+  // `OnDestroyingAssistantClient()`.
+  AssistantClient* assistant_client_ = nullptr;
+
   // False until libassistant is running for the first time.
   // Any request that comes in before that is an error and will be DCHECK'ed.
   bool requests_are_allowed_ = false;
diff --git a/chromeos/services/libassistant/conversation_controller_unittest.cc b/chromeos/services/libassistant/conversation_controller_unittest.cc
index f8349d5..6fcc419 100644
--- a/chromeos/services/libassistant/conversation_controller_unittest.cc
+++ b/chromeos/services/libassistant/conversation_controller_unittest.cc
@@ -5,8 +5,6 @@
 #include "chromeos/services/libassistant/conversation_controller.h"
 #include "base/run_loop.h"
 #include "base/test/task_environment.h"
-#include "chromeos/assistant/internal/test_support/fake_assistant_manager.h"
-#include "chromeos/assistant/internal/test_support/fake_assistant_manager_internal.h"
 #include "chromeos/assistant/test_support/expect_utils.h"
 #include "chromeos/services/libassistant/test_support/fake_assistant_client.h"
 #include "testing/gmock/include/gmock/gmock.h"
@@ -17,79 +15,54 @@
 
 namespace {
 
-class AssistantManagerInternalMock
-    : public assistant::FakeAssistantManagerInternal {
+class AssistantClientMock : public FakeAssistantClient {
  public:
-  AssistantManagerInternalMock() = default;
-  AssistantManagerInternalMock(const AssistantManagerInternalMock&) = delete;
-  AssistantManagerInternalMock& operator=(const AssistantManagerInternalMock&) =
-      delete;
-  ~AssistantManagerInternalMock() override = default;
+  AssistantClientMock(
+      std::unique_ptr<assistant::FakeAssistantManager> assistant_manager,
+      assistant::FakeAssistantManagerInternal* assistant_manager_internal)
+      : FakeAssistantClient(std::move(assistant_manager),
+                            assistant_manager_internal) {}
+  ~AssistantClientMock() override = default;
 
-  // assistant::FakeAssistantManagerInternal implementation:
-  MOCK_METHOD(void,
-              StopAssistantInteractionInternal,
-              (bool cancel_conversation));
+  // AssistantClient:
+  MOCK_METHOD(void, StartVoiceInteraction, ());
+  MOCK_METHOD(void, StopAssistantInteraction, (bool cancel_conversation));
   MOCK_METHOD(void,
               SendVoicelessInteraction,
-              (const std::string&,
-               const std::string&,
-               const assistant_client::VoicelessOptions& options,
-               assistant_client::SuccessCallbackInternal on_done));
-};
-
-class AssistantManagerMock : public assistant::FakeAssistantManager {
- public:
-  AssistantManagerMock() = default;
-  AssistantManagerMock(const AssistantManagerMock&) = delete;
-  AssistantManagerMock& operator=(const AssistantManagerMock&) = delete;
-  ~AssistantManagerMock() override = default;
-
-  // assistant::FakeAssistantManager implementation:
-  MOCK_METHOD(void, StartAssistantInteraction, ());
+              (const ::assistant::api::Interaction& interaction,
+               const std::string& description,
+               const ::assistant::api::VoicelessOptions& options,
+               base::OnceCallback<void(bool)> on_done));
 };
 
 }  // namespace
 
 class ConversationControllerTest : public ::testing::Test {
  public:
-  ConversationControllerTest() {
-    auto fake_assistant_manager = std::make_unique<AssistantManagerMock>();
-    assistant_client_ = std::make_unique<FakeAssistantClient>(
-        std::move(fake_assistant_manager), &assistant_manager_internal_);
-  }
+  ConversationControllerTest() = default;
   ConversationControllerTest(const ConversationControllerTest&) = delete;
   ConversationControllerTest& operator=(const ConversationControllerTest&) =
       delete;
   ~ConversationControllerTest() override = default;
 
   void StartLibassistant() {
-    controller_.OnAssistantClientRunning(assistant_client_.get());
+    controller_.OnAssistantClientRunning(&assistant_client_);
   }
 
   ConversationController& controller() { return controller_; }
 
-  AssistantManagerMock& assistant_manager_mock() {
-    return *(reinterpret_cast<AssistantManagerMock*>(
-        assistant_client_->assistant_manager()));
-  }
-
-  AssistantManagerInternalMock& assistant_manager_internal_mock() {
-    return *(reinterpret_cast<AssistantManagerInternalMock*>(
-        assistant_client_->assistant_manager_internal()));
-  }
+  AssistantClientMock& assistant_client_mock() { return assistant_client_; }
 
  private:
   base::test::SingleThreadTaskEnvironment environment_;
   ConversationController controller_;
-  AssistantManagerInternalMock assistant_manager_internal_;
-  std::unique_ptr<FakeAssistantClient> assistant_client_;
+  AssistantClientMock assistant_client_{nullptr, nullptr};
 };
 
 TEST_F(ConversationControllerTest, ShouldStartVoiceInteraction) {
   StartLibassistant();
 
-  EXPECT_CALL(assistant_manager_mock(), StartAssistantInteraction());
+  EXPECT_CALL(assistant_client_mock(), StartVoiceInteraction());
 
   controller().StartVoiceInteraction();
 }
@@ -97,32 +70,26 @@
 TEST_F(ConversationControllerTest, ShouldStopInteractionAfterDelay) {
   StartLibassistant();
 
-  EXPECT_CALL(assistant_manager_internal_mock(),
-              StopAssistantInteractionInternal)
-      .Times(0);
+  EXPECT_CALL(assistant_client_mock(), StopAssistantInteraction).Times(0);
 
   controller().StopActiveInteraction(true);
-  testing::Mock::VerifyAndClearExpectations(&assistant_manager_internal_mock());
+  testing::Mock::VerifyAndClearExpectations(&assistant_client_mock());
 
-  WAIT_FOR_CALL(assistant_manager_internal_mock(),
-                StopAssistantInteractionInternal);
+  WAIT_FOR_CALL(assistant_client_mock(), StopAssistantInteraction);
 }
 
 TEST_F(ConversationControllerTest,
        ShouldStopInteractionImmediatelyBeforeNewVoiceInteraction) {
   StartLibassistant();
 
-  EXPECT_CALL(assistant_manager_internal_mock(),
-              StopAssistantInteractionInternal)
-      .Times(0);
+  EXPECT_CALL(assistant_client_mock(), StopAssistantInteraction).Times(0);
 
   controller().StopActiveInteraction(true);
-  testing::Mock::VerifyAndClearExpectations(&assistant_manager_internal_mock());
+  testing::Mock::VerifyAndClearExpectations(&assistant_client_mock());
 
-  ::testing::Expectation stop = EXPECT_CALL(assistant_manager_internal_mock(),
-                                            StopAssistantInteractionInternal)
-                                    .Times(1);
-  EXPECT_CALL(assistant_manager_mock(), StartAssistantInteraction)
+  ::testing::Expectation stop =
+      EXPECT_CALL(assistant_client_mock(), StopAssistantInteraction).Times(1);
+  EXPECT_CALL(assistant_client_mock(), StartVoiceInteraction)
       .Times(1)
       .After(stop);
   controller().StartVoiceInteraction();
@@ -132,17 +99,14 @@
        ShouldStopInteractionImmediatelyBeforeNewEditReminderInteraction) {
   StartLibassistant();
 
-  EXPECT_CALL(assistant_manager_internal_mock(),
-              StopAssistantInteractionInternal)
-      .Times(0);
+  EXPECT_CALL(assistant_client_mock(), StopAssistantInteraction).Times(0);
 
   controller().StopActiveInteraction(true);
-  testing::Mock::VerifyAndClearExpectations(&assistant_manager_internal_mock());
+  testing::Mock::VerifyAndClearExpectations(&assistant_client_mock());
 
-  ::testing::Expectation stop = EXPECT_CALL(assistant_manager_internal_mock(),
-                                            StopAssistantInteractionInternal)
-                                    .Times(1);
-  EXPECT_CALL(assistant_manager_internal_mock(), SendVoicelessInteraction)
+  ::testing::Expectation stop =
+      EXPECT_CALL(assistant_client_mock(), StopAssistantInteraction).Times(1);
+  EXPECT_CALL(assistant_client_mock(), SendVoicelessInteraction)
       .Times(1)
       .After(stop);
   controller().StartEditReminderInteraction("client-id");
diff --git a/chromeos/services/libassistant/grpc/assistant_client.h b/chromeos/services/libassistant/grpc/assistant_client.h
index acfc75b..6b8fa6f6 100644
--- a/chromeos/services/libassistant/grpc/assistant_client.h
+++ b/chromeos/services/libassistant/grpc/assistant_client.h
@@ -105,11 +105,6 @@
 
   virtual void AddExperimentIds(const std::vector<std::string>& exp_ids) = 0;
 
-  virtual void SendVoicelessInteraction(
-      const ::assistant::api::Interaction& interaction,
-      const std::string& description,
-      const ::assistant::api::VoicelessOptions& options,
-      base::OnceCallback<void(bool)> on_done) = 0;
 
   // Speaker Id Enrollment methods.
   virtual void AddSpeakerIdEnrollmentEventObserver(
@@ -142,8 +137,17 @@
       GrpcServicesObserver<OnDeviceStateEventRequest>* observer) = 0;
 
   // Conversation methods.
+  virtual void SendVoicelessInteraction(
+      const ::assistant::api::Interaction& interaction,
+      const std::string& description,
+      const ::assistant::api::VoicelessOptions& options,
+      base::OnceCallback<void(bool)> on_done) = 0;
   virtual void RegisterActionModule(
       assistant_client::ActionModule* action_module) = 0;
+  virtual void SendScreenContextRequest(
+      const std::vector<std::string>& context_protos) = 0;
+  virtual void StartVoiceInteraction() = 0;
+  virtual void StopAssistantInteraction(bool cancel_conversation) = 0;
 
   // Settings-related functionality during bootup:
   virtual void SetAuthenticationInfo(const AuthTokens& tokens) = 0;
diff --git a/chromeos/services/libassistant/grpc/assistant_client_v1.cc b/chromeos/services/libassistant/grpc/assistant_client_v1.cc
index 7f7090ad..c82ef0d 100644
--- a/chromeos/services/libassistant/grpc/assistant_client_v1.cc
+++ b/chromeos/services/libassistant/grpc/assistant_client_v1.cc
@@ -266,7 +266,7 @@
 }
 
 AssistantClientV1::~AssistantClientV1() {
-  // Some listeners (e.g. MediaManagerListener) require that they outlive 
+  // Some listeners (e.g. MediaManagerListener) require that they outlive
   // `assistant_manager_`. Reset `assistant_manager_` in the parent class first
   // before any listener in this class gets destructed.
   ResetAssistantManager();
@@ -295,20 +295,6 @@
   assistant_manager_internal()->AddExtraExperimentIds(exp_ids);
 }
 
-void AssistantClientV1::SendVoicelessInteraction(
-    const ::assistant::api::Interaction& interaction,
-    const std::string& description,
-    const ::assistant::api::VoicelessOptions& options,
-    base::OnceCallback<void(bool)> on_done) {
-  assistant_client::VoicelessOptions voiceless_options;
-  PopulateVoicelessOptionsFromProto(options, &voiceless_options);
-  assistant_manager_internal()->SendVoicelessInteraction(
-      interaction.SerializeAsString(), description, voiceless_options,
-      [callback = std::move(on_done)](bool result) mutable {
-        std::move(callback).Run(result);
-      });
-}
-
 void AssistantClientV1::AddSpeakerIdEnrollmentEventObserver(
     GrpcServicesObserver<OnSpeakerIdEnrollmentEventRequest>* observer) {
   speaker_event_observer_list_.AddObserver(observer);
@@ -389,11 +375,39 @@
   device_state_event_observer_list_.AddObserver(observer);
 }
 
+void AssistantClientV1::SendVoicelessInteraction(
+    const ::assistant::api::Interaction& interaction,
+    const std::string& description,
+    const ::assistant::api::VoicelessOptions& options,
+    base::OnceCallback<void(bool)> on_done) {
+  assistant_client::VoicelessOptions voiceless_options;
+  PopulateVoicelessOptionsFromProto(options, &voiceless_options);
+  assistant_manager_internal()->SendVoicelessInteraction(
+      interaction.SerializeAsString(), description, voiceless_options,
+      [callback = std::move(on_done)](bool result) mutable {
+        std::move(callback).Run(result);
+      });
+}
+
 void AssistantClientV1::RegisterActionModule(
     assistant_client::ActionModule* action_module) {
   assistant_manager_internal()->RegisterActionModule(action_module);
 }
 
+void AssistantClientV1::SendScreenContextRequest(
+    const std::vector<std::string>& context_protos) {
+  assistant_manager_internal()->SendScreenContextRequest(context_protos);
+}
+
+void AssistantClientV1::StartVoiceInteraction() {
+  assistant_manager()->StartAssistantInteraction();
+}
+
+void AssistantClientV1::StopAssistantInteraction(bool cancel_conversation) {
+  assistant_manager_internal()->StopAssistantInteractionInternal(
+      cancel_conversation);
+}
+
 void AssistantClientV1::SetAuthenticationInfo(const AuthTokens& tokens) {
   assistant_manager()->SetAuthTokens(tokens);
 }
diff --git a/chromeos/services/libassistant/grpc/assistant_client_v1.h b/chromeos/services/libassistant/grpc/assistant_client_v1.h
index fcf587c4..7e1963d7 100644
--- a/chromeos/services/libassistant/grpc/assistant_client_v1.h
+++ b/chromeos/services/libassistant/grpc/assistant_client_v1.h
@@ -33,11 +33,6 @@
       assistant_client::ChromeOSApiDelegate* delegate) override;
   bool StartGrpcServices() override;
   void AddExperimentIds(const std::vector<std::string>& exp_ids) override;
-  void SendVoicelessInteraction(
-      const ::assistant::api::Interaction& interaction,
-      const std::string& description,
-      const ::assistant::api::VoicelessOptions& options,
-      base::OnceCallback<void(bool)> on_done) override;
   void AddSpeakerIdEnrollmentEventObserver(
       GrpcServicesObserver<OnSpeakerIdEnrollmentEventRequest>* observer)
       override;
@@ -60,8 +55,17 @@
   void SetExternalPlaybackState(const MediaStatus& status_proto) override;
   void AddDeviceStateEventObserver(
       GrpcServicesObserver<OnDeviceStateEventRequest>* observer) override;
+  void SendVoicelessInteraction(
+      const ::assistant::api::Interaction& interaction,
+      const std::string& description,
+      const ::assistant::api::VoicelessOptions& options,
+      base::OnceCallback<void(bool)> on_done) override;
   void RegisterActionModule(
       assistant_client::ActionModule* action_module) override;
+  void SendScreenContextRequest(
+      const std::vector<std::string>& context_protos) override;
+  void StartVoiceInteraction() override;
+  void StopAssistantInteraction(bool cancel_conversation) override;
   void SetAuthenticationInfo(const AuthTokens& tokens) override;
   void SetInternalOptions(const std::string& locale,
                           bool spoken_feedback_enabled) override;
diff --git a/chromeos/services/libassistant/test_support/fake_assistant_client.cc b/chromeos/services/libassistant/test_support/fake_assistant_client.cc
index b27a4b5f..3c7e773c 100644
--- a/chromeos/services/libassistant/test_support/fake_assistant_client.cc
+++ b/chromeos/services/libassistant/test_support/fake_assistant_client.cc
@@ -34,12 +34,6 @@
 void FakeAssistantClient::AddExperimentIds(
     const std::vector<std::string>& exp_ids) {}
 
-void FakeAssistantClient::SendVoicelessInteraction(
-    const ::assistant::api::Interaction& interaction,
-    const std::string& description,
-    const ::assistant::api::VoicelessOptions& options,
-    base::OnceCallback<void(bool)> on_done) {}
-
 void FakeAssistantClient::AddSpeakerIdEnrollmentEventObserver(
     GrpcServicesObserver<OnSpeakerIdEnrollmentEventRequest>* observer) {}
 
@@ -74,9 +68,22 @@
 void FakeAssistantClient::AddDeviceStateEventObserver(
     GrpcServicesObserver<OnDeviceStateEventRequest>* observer) {}
 
+void FakeAssistantClient::SendVoicelessInteraction(
+    const ::assistant::api::Interaction& interaction,
+    const std::string& description,
+    const ::assistant::api::VoicelessOptions& options,
+    base::OnceCallback<void(bool)> on_done) {}
+
 void FakeAssistantClient::RegisterActionModule(
     assistant_client::ActionModule* action_module) {}
 
+void FakeAssistantClient::SendScreenContextRequest(
+    const std::vector<std::string>& context_protos) {}
+
+void FakeAssistantClient::StartVoiceInteraction() {}
+
+void FakeAssistantClient::StopAssistantInteraction(bool cancel_conversation) {}
+
 void FakeAssistantClient::SetInternalOptions(const std::string& locale,
                                              bool spoken_feedback_enabled) {}
 
diff --git a/chromeos/services/libassistant/test_support/fake_assistant_client.h b/chromeos/services/libassistant/test_support/fake_assistant_client.h
index b683d32..77452816 100644
--- a/chromeos/services/libassistant/test_support/fake_assistant_client.h
+++ b/chromeos/services/libassistant/test_support/fake_assistant_client.h
@@ -34,11 +34,6 @@
       assistant_client::ChromeOSApiDelegate* delegate) override;
   bool StartGrpcServices() override;
   void AddExperimentIds(const std::vector<std::string>& exp_ids) override;
-  void SendVoicelessInteraction(
-      const ::assistant::api::Interaction& interaction,
-      const std::string& description,
-      const ::assistant::api::VoicelessOptions& options,
-      base::OnceCallback<void(bool)> on_done) override;
   void AddSpeakerIdEnrollmentEventObserver(
       GrpcServicesObserver<OnSpeakerIdEnrollmentEventRequest>* observer)
       override;
@@ -61,8 +56,17 @@
   void SetExternalPlaybackState(const MediaStatus& status_proto) override;
   void AddDeviceStateEventObserver(
       GrpcServicesObserver<OnDeviceStateEventRequest>* observer) override;
+  void SendVoicelessInteraction(
+      const ::assistant::api::Interaction& interaction,
+      const std::string& description,
+      const ::assistant::api::VoicelessOptions& options,
+      base::OnceCallback<void(bool)> on_done) override;
   void RegisterActionModule(
       assistant_client::ActionModule* action_module) override;
+  void SendScreenContextRequest(
+      const std::vector<std::string>& context_protos) override;
+  void StartVoiceInteraction() override;
+  void StopAssistantInteraction(bool cancel_conversation) override;
   void SetInternalOptions(const std::string& locale,
                           bool spoken_feedback_enabled) override;
   void SetAuthenticationInfo(const AuthTokens& tokens) override;
diff --git a/chromeos/services/libassistant/util.cc b/chromeos/services/libassistant/util.cc
index 1f840ef..fcdfb3f 100644
--- a/chromeos/services/libassistant/util.cc
+++ b/chromeos/services/libassistant/util.cc
@@ -349,5 +349,76 @@
       .Proto();
 }
 
+Interaction CreateNotificationRequestInteraction(
+    const std::string& notification_id,
+    const std::string& consistent_token,
+    const std::string& opaque_token,
+    const int action_index) {
+  auto request_param = assistant::CreateNotificationRequestParam(
+      notification_id, consistent_token, opaque_token, action_index);
+
+  return V1InteractionBuilder()
+      .SetClientInputName(assistant::kClientInputRequestNotification)
+      .AddClientInputParams(assistant::kNotificationRequestParamsKey,
+                            request_param)
+      .Proto();
+}
+
+Interaction CreateNotificationDismissedInteraction(
+    const std::string& notification_id,
+    const std::string& consistent_token,
+    const std::string& opaque_token,
+    const std::vector<std::string>& grouping_keys) {
+  auto dismiss_param = assistant::CreateNotificationDismissedParam(
+      notification_id, consistent_token, opaque_token, grouping_keys);
+
+  return V1InteractionBuilder()
+      .SetClientInputName(assistant::kClientInputDismissNotification)
+      .AddClientInputParams(assistant::kNotificationDismissParamsKey,
+                            dismiss_param)
+      .Proto();
+}
+
+Interaction CreateEditReminderInteraction(const std::string& reminder_id) {
+  auto intent_input = assistant::CreateEditReminderParam(reminder_id);
+
+  return V1InteractionBuilder()
+      .SetClientInputName(assistant::kClientInputEditReminder)
+      .AddClientInputParams(assistant::kEditReminderParamsKey, intent_input)
+      .Proto();
+}
+
+Interaction CreateOpenProviderResponseInteraction(const int interaction_id,
+                                                  const bool provider_found) {
+  return V1InteractionBuilder()
+      .SetInResponseTo(interaction_id)
+      .SetStatusCodeFromEntityFound(provider_found)
+      .Proto();
+}
+
+Interaction CreateSendFeedbackInteraction(
+    bool assistant_debug_info_allowed,
+    const std::string& feedback_description,
+    const std::string& screenshot_png) {
+  auto feedback_arg = assistant::CreateFeedbackParam(
+      assistant_debug_info_allowed, feedback_description, screenshot_png);
+
+  return V1InteractionBuilder()
+      .SetClientInputName(assistant::kClientInputText)
+      .AddClientInputParams(
+          assistant::kTextParamsKey,
+          assistant::CreateTextParam(assistant::kFeedbackText))
+      .AddClientInputParams(assistant::kFeedbackParamsKey, feedback_arg)
+      .Proto();
+}
+
+Interaction CreateTextQueryInteraction(const std::string& query) {
+  return V1InteractionBuilder()
+      .SetClientInputName(assistant::kClientInputText)
+      .AddClientInputParams(assistant::kTextParamsKey,
+                            assistant::CreateTextParam(query))
+      .Proto();
+}
+
 }  // namespace libassistant
 }  // namespace chromeos
diff --git a/chromeos/services/libassistant/util.h b/chromeos/services/libassistant/util.h
index ce795004..61ab09c 100644
--- a/chromeos/services/libassistant/util.h
+++ b/chromeos/services/libassistant/util.h
@@ -45,6 +45,35 @@
     int interaction_id,
     const std::vector<chromeos::assistant::DeviceSetting>& device_settings);
 
+// `action_index` is the index of the actions and buttons.
+::assistant::api::Interaction CreateNotificationRequestInteraction(
+    const std::string& notification_id,
+    const std::string& consistent_token,
+    const std::string& opaque_token,
+    const int action_index);
+
+// `grouping_keys` are the keys to group multiple notifications together.
+::assistant::api::Interaction CreateNotificationDismissedInteraction(
+    const std::string& notification_id,
+    const std::string& consistent_token,
+    const std::string& opaque_token,
+    const std::vector<std::string>& grouping_keys);
+
+::assistant::api::Interaction CreateEditReminderInteraction(
+    const std::string& reminder_id);
+
+::assistant::api::Interaction CreateOpenProviderResponseInteraction(
+    const int interaction_id,
+    const bool provider_found);
+
+::assistant::api::Interaction CreateSendFeedbackInteraction(
+    bool assistant_debug_info_allowed,
+    const std::string& feedback_description,
+    const std::string& screenshot_png = std::string());
+
+::assistant::api::Interaction CreateTextQueryInteraction(
+    const std::string& query);
+
 }  // namespace libassistant
 }  // namespace chromeos
 
diff --git a/chromeos/services/secure_channel/BUILD.gn b/chromeos/services/secure_channel/BUILD.gn
index 85cf6e8b8..ca8faab 100644
--- a/chromeos/services/secure_channel/BUILD.gn
+++ b/chromeos/services/secure_channel/BUILD.gn
@@ -134,6 +134,10 @@
     "pending_connection_request_delegate.h",
     "pending_nearby_initiator_connection_request.cc",
     "pending_nearby_initiator_connection_request.h",
+    "presence_monitor_delegate.cc",
+    "presence_monitor_delegate.h",
+    "presence_monitor_impl.cc",
+    "presence_monitor_impl.h",
     "raw_eid_generator.h",
     "raw_eid_generator_impl.cc",
     "raw_eid_generator_impl.h",
diff --git a/chromeos/services/secure_channel/presence_monitor_delegate.cc b/chromeos/services/secure_channel/presence_monitor_delegate.cc
new file mode 100644
index 0000000..fe802ea
--- /dev/null
+++ b/chromeos/services/secure_channel/presence_monitor_delegate.cc
@@ -0,0 +1,74 @@
+// 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 "chromeos/services/secure_channel/presence_monitor_delegate.h"
+
+#include "chromeos/components/multidevice/logging/logging.h"
+#include "chromeos/components/multidevice/remote_device_cache.h"
+#include "chromeos/services/secure_channel/ble_scanner_impl.h"
+#include "chromeos/services/secure_channel/ble_synchronizer.h"
+#include "chromeos/services/secure_channel/bluetooth_helper_impl.h"
+#include "chromeos/services/secure_channel/connection_role.h"
+#include "chromeos/services/secure_channel/public/cpp/shared/connection_priority.h"
+#include "device/bluetooth/bluetooth_adapter.h"
+
+namespace chromeos {
+namespace secure_channel {
+
+PresenceMonitorDelegate::PresenceMonitorDelegate(
+    scoped_refptr<device::BluetoothAdapter> bluetooth_adapter,
+    PresenceMonitor::DeviceSeenCallback device_seen_callback)
+    : bluetooth_adapter_(bluetooth_adapter),
+      device_seen_callback_(std::move(device_seen_callback)),
+      remote_device_cache_(multidevice::RemoteDeviceCache::Factory::Create()),
+      bluetooth_helper_(
+          BluetoothHelperImpl::Factory::Create(remote_device_cache_.get())),
+      ble_synchronizer_(BleSynchronizer::Factory::Create(bluetooth_adapter_)),
+      ble_scanner_(BleScannerImpl::Factory::Create(bluetooth_helper_.get(),
+                                                   ble_synchronizer_.get(),
+                                                   bluetooth_adapter_)) {
+  ble_scanner_->AddObserver(this);
+}
+
+PresenceMonitorDelegate::~PresenceMonitorDelegate() {
+  ble_scanner_->RemoveObserver(this);
+}
+
+void PresenceMonitorDelegate::StartMonitoring(
+    const multidevice::RemoteDevice& remote_device,
+    const multidevice::RemoteDevice& local_device) {
+  PA_LOG(INFO) << "Starting monitoring proximity";
+  remote_device_id_ = remote_device.GetDeviceId();
+  local_device_id_ = local_device.GetDeviceId();
+
+  remote_device_cache_->SetRemoteDevices({remote_device, local_device});
+
+  ble_scanner_->AddScanRequest(ConnectionAttemptDetails(
+      remote_device_id_, local_device_id_,
+      ConnectionMedium::kBluetoothLowEnergy, ConnectionRole::kListenerRole));
+}
+
+void PresenceMonitorDelegate::StopMonitoring() {
+  PA_LOG(INFO) << "Stopping monitoring proximity";
+  ble_scanner_->RemoveScanRequest(ConnectionAttemptDetails(
+      remote_device_id_, local_device_id_,
+      ConnectionMedium::kBluetoothLowEnergy, ConnectionRole::kListenerRole));
+  remote_device_id_.clear();
+  local_device_id_.clear();
+}
+
+void PresenceMonitorDelegate::OnReceivedAdvertisement(
+    multidevice::RemoteDeviceRef remote_device_ref,
+    device::BluetoothDevice* bluetooth_device,
+    ConnectionMedium connection_medium,
+    ConnectionRole connection_role,
+    const std::vector<uint8_t>& eid) {
+  // It is the responsibility of the scanner to ensure outdated advertisements
+  // are not forwarded through, so we will treat all received advertisements as
+  // valid.
+  device_seen_callback_.Run();
+}
+
+}  // namespace secure_channel
+}  // namespace chromeos
diff --git a/chromeos/services/secure_channel/presence_monitor_delegate.h b/chromeos/services/secure_channel/presence_monitor_delegate.h
new file mode 100644
index 0000000..5389ee4
--- /dev/null
+++ b/chromeos/services/secure_channel/presence_monitor_delegate.h
@@ -0,0 +1,65 @@
+// 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 CHROMEOS_SERVICES_SECURE_CHANNEL_PRESENCE_MONITOR_DELEGATE_H_
+#define CHROMEOS_SERVICES_SECURE_CHANNEL_PRESENCE_MONITOR_DELEGATE_H_
+
+#include "chromeos/services/secure_channel/ble_scanner.h"
+#include "chromeos/services/secure_channel/public/cpp/shared/presence_monitor.h"
+
+namespace device {
+class BluetoothAdapter;
+class BluetoothDevice;
+}  // namespace device
+
+namespace chromeos {
+
+namespace multidevice {
+class RemoteDeviceCache;
+class RemoteDeviceRef;
+}  // namespace multidevice
+
+namespace secure_channel {
+
+class BleSynchronizerBase;
+class Bluet