diff --git a/DEPS b/DEPS
index ccbc679..4015fb75 100644
--- a/DEPS
+++ b/DEPS
@@ -325,7 +325,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': 'd5b2b322948ba9779c01eb2ea76aad4542a7da45',
+  'skia_revision': 'af3cd504644942bf0062461be22fec7785be55b3',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
@@ -333,7 +333,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': '36111b25fe13a46896c18d8614191889fbd2a032',
+  'angle_revision': '377216093693e092dd886d12d654f30605165cbf',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -341,7 +341,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
-  'pdfium_revision': '7bac6ae44fef54e80c086f95d0a074022051bd57',
+  'pdfium_revision': 'ce0337c0880cb6c3d7bf213b828aee57a3020530',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling BoringSSL
   # and whatever else without interference from each other.
@@ -396,7 +396,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling catapult
   # and whatever else without interference from each other.
-  'catapult_revision': '2da767c6c13e331107f4c8026fd4584a7a85a214',
+  'catapult_revision': 'c5ac2a64a6688f72cd857b2f6f3a01d7379ae83d',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling CrossBench
   # and whatever else without interference from each other.
@@ -412,7 +412,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': '847d9112cf89902d5ee7f37b80887abdc582adf4',
+  'devtools_frontend_revision': 'd1a36c004c4ab384ef857a71dc3413d50a7653ee',
   # 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.
@@ -452,7 +452,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': '9543f74739118a853dd5e5a46297f5442c3352f8',
+  'dawn_revision': '7972a10aa8f3686c9b9d7f69fc9b18d9db53d3e7',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -800,7 +800,7 @@
 
   'src/clank': {
     'url': Var('chrome_git') + '/clank/internal/apps.git' + '@' +
-    '0a29c4d421df48939cd95bf6ef7581dc2bcace0a',
+    '39571ae0a7fbb42f9baba19fa85a7409ccf2cd93',
     'condition': 'checkout_android and checkout_src_internal',
   },
 
@@ -989,7 +989,7 @@
     'packages': [
       {
           'package': 'chromium/third_party/androidx',
-          'version': 'naNt8xoIXXo_9dIBIcVTLD4u9Qq-edLsdJ3hzMAy1IQC',
+          'version': '7Pq-5ILyMsBaCykhDCAcLI9KkdBnuD9ugOF6Q03YVpUC',
       },
     ],
     'condition': 'checkout_android',
@@ -1246,7 +1246,7 @@
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
 
   'src/third_party/devtools-frontend-internal': {
-      'url': Var('chrome_git') + '/devtools/devtools-internal.git' + '@' + 'e588d2222bdb1dbc05a8f293a18604e7421f1ea4',
+      'url': Var('chrome_git') + '/devtools/devtools-internal.git' + '@' + 'f250c7663bc0220d167c537a233c15a72630fe30',
     'condition': 'checkout_src_internal',
   },
 
@@ -1701,7 +1701,7 @@
     Var('chromium_git') + '/external/github.com/cisco/openh264' + '@' + 'db956674bbdfbaab5acdd3fdb4117c2fef5527e9',
 
   'src/third_party/openscreen/src':
-    Var('chromium_git') + '/openscreen' + '@' + 'a3d689dae21d8d4b1f2c9846370bbf74f6d054e0',
+    Var('chromium_git') + '/openscreen' + '@' + '9fa061bbb71807041927af9952aae249014160f1',
 
   'src/third_party/openxr/src': {
     'url': Var('chromium_git') + '/external/github.com/KhronosGroup/OpenXR-SDK' + '@' + 'bf21ccb1007bb531b45d9978919a56ea5059c245',
@@ -1718,7 +1718,7 @@
   },
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' + 'd26e33e5ce7dcb680b43053be9453a6a3da19642',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' + 'f90d61c166b857309dcbc12e5a7c328578e5bd51',
 
   'src/third_party/perl': {
       'url': Var('chromium_git') + '/chromium/deps/perl.git' + '@' + '6f3e5028eb65d0b4c5fdd792106ac4c84eee1eb3',
@@ -1903,7 +1903,7 @@
     Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + '6c8361e98f1daba65902f5e2fc1297893ac14b67',
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + 'e1298afaa66e1fa88bd1e257d93f6af9db6e3e76',
+    Var('webrtc_git') + '/src.git' + '@' + 'd877589e16f2ad93563d91f4dc54c67d9a17a920',
 
   # Wuffs' canonical repository is at github.com/google/wuffs, but we use
   # Skia's mirror of Wuffs, the same as in upstream Skia's DEPS file.
@@ -1915,6 +1915,13 @@
       'condition': 'checkout_linux',
   },
 
+  # A conformance-suite developed by canonical for the mir wayland server.
+  # Required to compile exo_wlcs on chromeos checkouts.
+  'src/third_party/wlcs/src': {
+      'url': Var('chromium_git') + '/external/github.com/MirServer/wlcs.git' + '@' + '2930ad4b5ca602446ad211b49fb1827303ce9f4b',
+      'condition': 'checkout_chromeos',
+  },
+
   'src/third_party/xdg-utils': {
       'url': Var('chromium_git') + '/chromium/deps/xdg-utils.git' + '@' + 'd80274d5869b17b8c9067a1022e4416ee7ed5e0d',
       'condition': 'checkout_linux',
@@ -1930,7 +1937,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/linux-amd64',
-          'version': 'uu72l9rwYReKqlKTHTUE26N6gMAt6YHvdysfn6rcAvgC',
+          'version': 'iQ7zKud-gha6r9hEdwqYWRdOpeAs6gFfDxnviDUt4FQC',
         },
       ],
       'dep_type': 'cipd',
@@ -1940,7 +1947,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/windows-amd64',
-          'version': 'DxsfuVWQrwfkUxTCKXzhO_Wh4OYOLWM-sSQpfx92DxwC',
+          'version': 'we56UJIWxJJ2GkQ_ne0o3oGAr7FBJa5T5Jr1xguLn-gC',
         },
       ],
       'dep_type': 'cipd',
@@ -1951,7 +1958,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/mac-amd64',
-          'version': 'EPIsKRgwINCn8DCzCmRC1ZH3EivSehuq2ymx_qN6MhMC',
+          'version': '9Wfje1bt82IO9pJokAt9lboy59X_Pe-s0b4EpmH7RT4C',
         },
       ],
       'dep_type': 'cipd',
@@ -1962,7 +1969,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/mac-arm64',
-          'version': 'NcrJgtTlI-mdqmPTl4LBprsY9nhx_5nzK08RLKJG9CAC',
+          'version': 'zihT2Lk2afg0XzIZozuGcZXWv7RJujaDEi_6q7QL4DgC',
         },
       ],
       'dep_type': 'cipd',
@@ -1973,7 +1980,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': Var('chrome_git') + '/chrome/src-internal.git@02aabbc7ffc30397435d88760e7831cd81f4af95',
+    'url': Var('chrome_git') + '/chrome/src-internal.git@0b25bff5936a5effb5ad49ad7a0e3117fc7d3046',
     'condition': 'checkout_src_internal',
   },
 
@@ -4178,7 +4185,7 @@
 
   'src/ios_internal':  {
       'url': '{chrome_git}/chrome/ios_internal.git' + '@' +
-        'c4c13e9e59ee894857c7e836810922482e29704b',
+        '6b1f4ed31be2e8831073e1fa54e2f7b8402dfa48',
       'condition': 'checkout_src_internal and checkout_ios',
     },
 
diff --git a/WATCHLISTS b/WATCHLISTS
index cde3c87d..7320bb97 100644
--- a/WATCHLISTS
+++ b/WATCHLISTS
@@ -1242,8 +1242,8 @@
     'input_device_settings': {
       'filepath': 'ash/system/input_device_settings/|'\
                   'chrome/browser/resources/settings/chromeos/device_page/|'\
-                  'chrome/browser/ui/webui/settings/ash/device_section.*'\
-                  'chrome/browser/ui/webui/settings/ash/input_device_settings/|',
+                  'chrome/browser/ui/webui/settings/ash/device_section.*|'\
+                  'chrome/browser/ui/webui/settings/ash/input_device_settings/',
     },
     'input_devices': {
       'filepath': 'ui/events/devices/',
diff --git a/ash/accelerators/accelerator_commands.cc b/ash/accelerators/accelerator_commands.cc
index eca6137..2359cb4 100644
--- a/ash/accelerators/accelerator_commands.cc
+++ b/ash/accelerators/accelerator_commands.cc
@@ -489,10 +489,6 @@
   return display::Screen::GetScreen()->GetNumDisplays() > 1;
 }
 
-bool CanToggleCalendar() {
-  return features::IsCalendarViewEnabled();
-}
-
 bool CanToggleDictation() {
   return Shell::Get()->accessibility_controller()->dictation().enabled();
 }
diff --git a/ash/accelerators/accelerator_controller_impl.cc b/ash/accelerators/accelerator_controller_impl.cc
index e95a50cd..2a4a4b8 100644
--- a/ash/accelerators/accelerator_controller_impl.cc
+++ b/ash/accelerators/accelerator_controller_impl.cc
@@ -680,7 +680,7 @@
           accelerator, previous_accelerator,
           accelerator_history_->currently_pressed_keys());
     case TOGGLE_CALENDAR:
-      return accelerators::CanToggleCalendar();
+      return true;
     case TOGGLE_CAPS_LOCK:
       return CanHandleToggleCapsLock(
           accelerator, previous_accelerator,
diff --git a/ash/accelerators/ash_accelerator_configuration.cc b/ash/accelerators/ash_accelerator_configuration.cc
index 642c562..053c2570 100644
--- a/ash/accelerators/ash_accelerator_configuration.cc
+++ b/ash/accelerators/ash_accelerator_configuration.cc
@@ -183,7 +183,17 @@
 }
 
 AcceleratorConfigResult AshAcceleratorConfiguration::RestoreAllDefaults() {
-  return AcceleratorConfigResult::kActionLocked;
+  accelerators_.clear();
+  id_to_accelerators_.clear();
+  accelerator_to_id_.Clear();
+
+  // TODO(jimmyxgong): Reset the prefs here too.
+  id_to_accelerators_ = default_id_to_accelerators_cache_;
+  accelerator_to_id_ = default_accelerators_to_id_cache_;
+
+  UpdateAndNotifyAccelerators();
+
+  return AcceleratorConfigResult::kSuccess;
 }
 
 void AshAcceleratorConfiguration::Initialize() {
diff --git a/ash/accelerators/ash_accelerator_configuration_unittest.cc b/ash/accelerators/ash_accelerator_configuration_unittest.cc
index ce0312a..ced38cd2 100644
--- a/ash/accelerators/ash_accelerator_configuration_unittest.cc
+++ b/ash/accelerators/ash_accelerator_configuration_unittest.cc
@@ -494,4 +494,87 @@
   ExpectAllAcceleratorsEqual(updated_test_data, config_->GetAllAccelerators());
   EXPECT_EQ(2, observer_.num_times_accelerator_updated_called());
 }
+
+TEST_F(AshAcceleratorConfigurationTest, RemoveAndRestoreDefault) {
+  EXPECT_EQ(0, observer_.num_times_accelerator_updated_called());
+  const AcceleratorData test_data[] = {
+      {/*trigger_on_press=*/true, ui::VKEY_SPACE, ui::EF_CONTROL_DOWN,
+       SWITCH_TO_LAST_USED_IME},
+      {/*trigger_on_press=*/true, ui::VKEY_SPACE,
+       ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN, SWITCH_TO_LAST_USED_IME},
+      {/*trigger_on_press=*/true, ui::VKEY_TAB, ui::EF_ALT_DOWN,
+       CYCLE_FORWARD_MRU},
+      {/*trigger_on_press=*/true, ui::VKEY_TAB,
+       ui::EF_SHIFT_DOWN | ui::EF_ALT_DOWN, CYCLE_BACKWARD_MRU},
+  };
+
+  config_->Initialize(test_data);
+
+  ExpectAllAcceleratorsEqual(test_data, config_->GetAllAccelerators());
+  EXPECT_EQ(1, observer_.num_times_accelerator_updated_called());
+
+  // Remove `SWITCH_TO_LAST_USE_IME`.
+  const AcceleratorData updated_test_data[] = {
+      {/*trigger_on_press=*/true, ui::VKEY_SPACE,
+       ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN, SWITCH_TO_LAST_USED_IME},
+      {/*trigger_on_press=*/true, ui::VKEY_TAB, ui::EF_ALT_DOWN,
+       CYCLE_FORWARD_MRU},
+      {/*trigger_on_press=*/true, ui::VKEY_TAB,
+       ui::EF_SHIFT_DOWN | ui::EF_ALT_DOWN, CYCLE_BACKWARD_MRU},
+  };
+  AcceleratorConfigResult result = config_->RemoveAccelerator(
+      SWITCH_TO_LAST_USED_IME,
+      ui::Accelerator(ui::VKEY_SPACE, ui::EF_CONTROL_DOWN));
+  EXPECT_EQ(AcceleratorConfigResult::kSuccess, result);
+
+  // Compare expected accelerators and that the observer was fired after
+  // removing an accelerator.
+  ExpectAllAcceleratorsEqual(updated_test_data, config_->GetAllAccelerators());
+  EXPECT_EQ(2, observer_.num_times_accelerator_updated_called());
+
+  // Restore all defaults.
+  result = config_->RestoreAllDefaults();
+  EXPECT_EQ(AcceleratorConfigResult::kSuccess, result);
+
+  // Expect accelerators to revert back to the default state and observer
+  // is called.
+  ExpectAllAcceleratorsEqual(test_data, config_->GetAllAccelerators());
+  EXPECT_EQ(3, observer_.num_times_accelerator_updated_called());
+}
+
+TEST_F(AshAcceleratorConfigurationTest, RestoreAllConsecutively) {
+  EXPECT_EQ(0, observer_.num_times_accelerator_updated_called());
+  const AcceleratorData test_data[] = {
+      {/*trigger_on_press=*/true, ui::VKEY_SPACE, ui::EF_CONTROL_DOWN,
+       SWITCH_TO_LAST_USED_IME},
+      {/*trigger_on_press=*/true, ui::VKEY_SPACE,
+       ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN, SWITCH_TO_LAST_USED_IME},
+      {/*trigger_on_press=*/true, ui::VKEY_TAB, ui::EF_ALT_DOWN,
+       CYCLE_FORWARD_MRU},
+      {/*trigger_on_press=*/true, ui::VKEY_TAB,
+       ui::EF_SHIFT_DOWN | ui::EF_ALT_DOWN, CYCLE_BACKWARD_MRU},
+  };
+
+  config_->Initialize(test_data);
+
+  ExpectAllAcceleratorsEqual(test_data, config_->GetAllAccelerators());
+  EXPECT_EQ(1, observer_.num_times_accelerator_updated_called());
+
+  // Restore all defaults, even though no change was made.
+  AcceleratorConfigResult reset_result = config_->RestoreAllDefaults();
+  EXPECT_EQ(AcceleratorConfigResult::kSuccess, reset_result);
+
+  // Nothing should have changed, but the observer is called.
+  ExpectAllAcceleratorsEqual(test_data, config_->GetAllAccelerators());
+  EXPECT_EQ(2, observer_.num_times_accelerator_updated_called());
+
+  // Restore all defaults again, even though no change was made.
+  reset_result = config_->RestoreAllDefaults();
+  EXPECT_EQ(AcceleratorConfigResult::kSuccess, reset_result);
+
+  // Nothing should have changed, but the observer is called.
+  ExpectAllAcceleratorsEqual(test_data, config_->GetAllAccelerators());
+  EXPECT_EQ(3, observer_.num_times_accelerator_updated_called());
+}
+
 }  // namespace ash
diff --git a/ash/ambient/metrics/ambient_multi_screen_metrics_recorder.cc b/ash/ambient/metrics/ambient_multi_screen_metrics_recorder.cc
index 790bf40..d39bd1a 100644
--- a/ash/ambient/metrics/ambient_multi_screen_metrics_recorder.cc
+++ b/ash/ambient/metrics/ambient_multi_screen_metrics_recorder.cc
@@ -86,8 +86,7 @@
                           *largest_timestamp_offset);
       break;
     case AmbientTheme::kSlideshow:
-    case AmbientTheme::kVideoNewMexico:
-    case AmbientTheme::kVideoClouds:
+    case AmbientTheme::kVideo:
       LOG(DFATAL) << "Should not be recording animation metrics for "
                   << ToString(theme_);
       break;
diff --git a/ash/app_list/app_list_item_util.cc b/ash/app_list/app_list_item_util.cc
index bb40d1b..f85bd944 100644
--- a/ash/app_list/app_list_item_util.cc
+++ b/ash/app_list/app_list_item_util.cc
@@ -4,6 +4,11 @@
 
 #include "ash/app_list/app_list_item_util.h"
 
+#include <string>
+
+#include "base/no_destructor.h"
+#include "base/pickle.h"
+
 namespace ash {
 
 const ui::ClipboardFormatType& GetAppItemFormatType() {
@@ -13,4 +18,20 @@
   return *format;
 }
 
+absl::optional<std::string> GetAppIdFromDropData(
+    const ui::OSExchangeData& data) {
+  base::Pickle data_pickle;
+  if (!data.GetPickledData(GetAppItemFormatType(), &data_pickle)) {
+    return absl::nullopt;
+  }
+
+  std::string app_id;
+  base::PickleIterator iter(data_pickle);
+  if (!iter.ReadString(&app_id)) {
+    return absl::nullopt;
+  }
+
+  return app_id;
+}
+
 }  // namespace ash
diff --git a/ash/app_list/app_list_item_util.h b/ash/app_list/app_list_item_util.h
index 63fd6d4..7770e53e 100644
--- a/ash/app_list/app_list_item_util.h
+++ b/ash/app_list/app_list_item_util.h
@@ -5,13 +5,22 @@
 #ifndef ASH_APP_LIST_APP_LIST_ITEM_UTIL_H_
 #define ASH_APP_LIST_APP_LIST_ITEM_UTIL_H_
 
+#include <string>
+
+#include "third_party/abseil-cpp/absl/types/optional.h"
 #include "ui/base/clipboard/clipboard_format_type.h"
 #include "ui/base/dragdrop/drag_drop_types.h"
+#include "ui/base/dragdrop/os_exchange_data.h"
 
 namespace ash {
 
 const ui::ClipboardFormatType& GetAppItemFormatType();
 
+// Retrieve and app id carried in a OSExchangeData object during app list drag
+// and drop actions.
+absl::optional<std::string> GetAppIdFromDropData(
+    const ui::OSExchangeData& data);
+
 }  // namespace ash
 
 #endif  // ASH_APP_LIST_APP_LIST_ITEM_UTIL_H_
diff --git a/ash/app_list/views/apps_grid_view.cc b/ash/app_list/views/apps_grid_view.cc
index 205eceb..7b6c335 100644
--- a/ash/app_list/views/apps_grid_view.cc
+++ b/ash/app_list/views/apps_grid_view.cc
@@ -44,7 +44,6 @@
 #include "base/metrics/histogram_macros.h"
 #include "base/metrics/user_metrics.h"
 #include "base/metrics/user_metrics_action.h"
-#include "base/pickle.h"
 #include "base/ranges/algorithm.h"
 #include "base/time/time.h"
 #include "ui/aura/window.h"
@@ -746,14 +745,16 @@
       // An EndDrag can be received during a reparent via a model change. This
       // is always a cancel and needs to be forwarded to the folder.
       if (cancel) {
+        DCHECK_EQ(!reparent_drag_cancellation_, is_drag_drop_refactor_enabled);
         if (reparent_drag_cancellation_) {
           std::move(reparent_drag_cancellation_).Run();
+          return;
         }
       } else {
         UpdateDropTargetRegion();
         EndDragFromReparentItemInRootLevel(nullptr, false, false, nullptr);
+        return;
       }
-      return;
     }
 
     if (!cancel && was_dragging) {
@@ -1097,6 +1098,11 @@
     return false;
   }
 
+  auto app_id = GetAppIdFromDropData(data);
+  if (app_id->empty()) {
+    return false;
+  }
+
   return data.HasCustomFormat(GetAppItemFormatType());
 }
 
@@ -1127,7 +1133,7 @@
     dragging_for_reparent_item_ = true;
     folder_delegate_->Close();
   }
-  drag_item_ = nullptr;
+  CancelDragWithNoDropAnimation();
 }
 
 void AppsGridView::OnDragEntered(const ui::DropTargetEvent& event) {
@@ -1141,19 +1147,12 @@
     return;
   }
 
-  std::string drag_item_id;
-
-  base::Pickle data_pickle;
-  if (!event.data().GetPickledData(GetAppItemFormatType(), &data_pickle)) {
+  auto app_id = GetAppIdFromDropData(event.data());
+  if (app_id->empty()) {
     return;
   }
 
-  base::PickleIterator iter(data_pickle);
-  if (!iter.ReadString(&drag_item_id)) {
-    return;
-  }
-
-  drag_item_ = AppListModelProvider::Get()->model()->FindItem(drag_item_id);
+  drag_item_ = AppListModelProvider::Get()->model()->FindItem(app_id.value());
   if (!drag_item_) {
     return;
   }
diff --git a/ash/app_list/views/apps_grid_view_unittest.cc b/ash/app_list/views/apps_grid_view_unittest.cc
index bfa3d6a..bc746a72 100644
--- a/ash/app_list/views/apps_grid_view_unittest.cc
+++ b/ash/app_list/views/apps_grid_view_unittest.cc
@@ -572,6 +572,14 @@
     const int selected_page = GetSelectedPage(apps_grid_view);
     GridIndex index(selected_page, row * apps_grid_view->cols() + column);
     AppListItemView* view = test_api.GetViewAtIndex(index);
+
+    InitiateDragForView(pointer, view, apps_grid_view);
+    return view;
+  }
+
+  void InitiateDragForView(AppsGridView::Pointer pointer,
+                           AppListItemView* view,
+                           AppsGridView* apps_grid_view) {
     DCHECK(view);
 
     gfx::Point from = view->GetBoundsInScreen().CenterPoint();
@@ -602,7 +610,6 @@
     // target OnDragUpdate().
     current_drag_location_ = from + gfx::Vector2d(10, 10);
     UpdateDragInScreen(pointer, current_drag_location_.value(), 2);
-    return view;
   }
 
   void UpdateDragInScreen(AppsGridView::Pointer pointer,
@@ -620,6 +627,7 @@
         generator->MoveMouseTo(drag_increment_point);
       }
     }
+    current_drag_location_ = to_in_screen;
   }
 
   // Updates the drag from the current drag location to the destination point
@@ -4255,19 +4263,13 @@
   EXPECT_FALSE(item_view->HasFocus());
 }
 
-TEST_P(AppsGridViewDragLegacyTest, DragAndPinItemToShelf) {
+TEST_P(AppsGridViewDragTest, DragAndPinItemToShelf) {
   model_->PopulateApps(2);
   UpdateLayout();
 
-  AppListItemView* const item_view = GetItemViewInTopLevelGrid(1);
+  AppListItemView* const item_view = InitiateDragForItemAtCurrentPageAt(
+      AppsGridView::MOUSE, 0, 1, apps_grid_view_);
 
-  auto* generator = GetEventGenerator();
-  generator->MoveMouseTo(item_view->GetBoundsInScreen().CenterPoint());
-  generator->PressLeftButton();
-  if (!use_drag_drop_refactor()) {
-    item_view->FireMouseDragTimerForTest();
-  }
-  generator->MoveMouseBy(10, 10);
   MaybeCheckHaptickEventsCount(1);
 
   // Verify that item drag has started.
@@ -4277,19 +4279,24 @@
 
   // Shelf should start handling the drag if it moves within its bounds.
   auto* shelf_view = GetPrimaryShelf()->GetShelfViewForTesting();
-  generator->MoveMouseTo(shelf_view->GetBoundsInScreen().left_center());
-  ASSERT_TRUE(apps_grid_view_->FireDragToShelfTimerForTest());
+  UpdateDragInScreen(
+      AppsGridView::MOUSE,
+      shelf_view->GetBoundsInScreen().left_center() + gfx::Vector2d(5, 5),
+      /*steps=*/1);
+  if (!use_drag_drop_refactor()) {
+    ASSERT_TRUE(apps_grid_view_->FireDragToShelfTimerForTest());
+  }
 
   EXPECT_EQ("Item 1", shelf_view->drag_and_drop_shelf_id().app_id);
 
   // Releasing drag over shelf should pin the dragged app.
-  generator->ReleaseLeftButton();
+  EndDrag();
   EXPECT_TRUE(ShelfModel::Get()->IsAppPinned("Item 1"));
   EXPECT_EQ("Item 1", ShelfModel::Get()->items()[0].id.app_id);
   MaybeCheckHaptickEventsCount(1);
 }
 
-TEST_P(AppsGridViewDragLegacyTest, DragAndPinNotInitiallyVisibleItemToShelf) {
+TEST_P(AppsGridViewDragTest, DragAndPinNotInitiallyVisibleItemToShelf) {
   // Add more apps to the root apps grid.
   model_->PopulateApps(50);
   UpdateLayout();
@@ -4307,13 +4314,7 @@
   ASSERT_TRUE(apps_grid_view_->GetWidget()->GetWindowBoundsInScreen().Contains(
       item_view->GetBoundsInScreen()));
 
-  auto* generator = GetEventGenerator();
-  generator->MoveMouseTo(item_view->GetBoundsInScreen().CenterPoint());
-  generator->PressLeftButton();
-  if (!use_drag_drop_refactor()) {
-    item_view->FireMouseDragTimerForTest();
-  }
-  generator->MoveMouseBy(10, 10);
+  InitiateDragForView(AppsGridView::MOUSE, item_view, apps_grid_view_);
   MaybeCheckHaptickEventsCount(1);
 
   // Verify app list item drag has started.
@@ -4323,19 +4324,24 @@
 
   // Shelf should start handling the drag if it moves within its bounds.
   auto* shelf_view = GetPrimaryShelf()->GetShelfViewForTesting();
-  generator->MoveMouseTo(shelf_view->GetBoundsInScreen().left_center());
-  ASSERT_TRUE(apps_grid_view_->FireDragToShelfTimerForTest());
+  UpdateDragInScreen(
+      AppsGridView::MOUSE,
+      shelf_view->GetBoundsInScreen().left_center() + gfx::Vector2d(5, 5),
+      /*steps=*/1);
+  if (!use_drag_drop_refactor()) {
+    ASSERT_TRUE(apps_grid_view_->FireDragToShelfTimerForTest());
+  }
 
   EXPECT_EQ("Item 40", shelf_view->drag_and_drop_shelf_id().app_id);
 
   // Releasing drag over shelf should pin the dragged app.
-  generator->ReleaseLeftButton();
+  EndDrag();
   MaybeCheckHaptickEventsCount(1);
   EXPECT_TRUE(ShelfModel::Get()->IsAppPinned("Item 40"));
   EXPECT_EQ("Item 40", ShelfModel::Get()->items()[0].id.app_id);
 }
 
-TEST_P(AppsGridViewDragLegacyTest, DragItemToAndFromShelf) {
+TEST_P(AppsGridViewDragTest, DragItemToAndFromShelf) {
   model_->PopulateApps(2);
   UpdateLayout();
 
@@ -4351,9 +4357,13 @@
 
   // Shelf should start handling the drag if it moves within its bounds.
   auto* shelf_view = GetPrimaryShelf()->GetShelfViewForTesting();
-  UpdateDragInScreen(AppsGridView::MOUSE,
-                     shelf_view->GetBoundsInScreen().left_center());
-  ASSERT_TRUE(apps_grid_view_->FireDragToShelfTimerForTest());
+  UpdateDragInScreen(
+      AppsGridView::MOUSE,
+      shelf_view->GetBoundsInScreen().left_center() + gfx::Vector2d(5, 5),
+      /*steps=*/1);
+  if (!use_drag_drop_refactor()) {
+    ASSERT_TRUE(apps_grid_view_->FireDragToShelfTimerForTest());
+  }
 
   EXPECT_EQ("Item 1", shelf_view->drag_and_drop_shelf_id().app_id);
 
@@ -4368,7 +4378,7 @@
   EXPECT_TRUE(ShelfModel::Get()->items().empty());
 }
 
-TEST_P(AppsGridViewDragLegacyTest, DragAndPinItemFromFolderToShelf) {
+TEST_P(AppsGridViewDragTest, DragAndPinItemFromFolderToShelf) {
   // Creates a folder item - the folder size was chosen arbitrarily.
   model_->CreateAndPopulateFolderWithApps(5);
   // Add more apps to the root apps grid.
@@ -4378,16 +4388,9 @@
   // Open the folder.
   test_api_->PressItemAt(0);
 
-  AppListItemView* const item_view =
-      GetItemViewInAppsGridAt(1, folder_apps_grid_view());
+  AppListItemView* const item_view = InitiateDragForItemAtCurrentPageAt(
+      AppsGridView::MOUSE, 0, 1, folder_apps_grid_view());
 
-  auto* generator = GetEventGenerator();
-  generator->MoveMouseTo(item_view->GetBoundsInScreen().CenterPoint());
-  generator->PressLeftButton();
-  if (!use_drag_drop_refactor()) {
-    item_view->FireMouseDragTimerForTest();
-  }
-  generator->MoveMouseBy(10, 10);
   MaybeCheckHaptickEventsCount(1);
 
   // Verify app list item drag has started.
@@ -4395,9 +4398,11 @@
   ASSERT_TRUE(folder_apps_grid_view()->IsDragging());
   ASSERT_EQ(item_view->item(), folder_apps_grid_view()->drag_item());
 
-  generator->MoveMouseTo(
+  UpdateDragInScreen(
+      AppsGridView::MOUSE,
       app_list_folder_view()->GetBoundsInScreen().right_center() +
-      gfx::Vector2d(20, 0));
+          gfx::Vector2d(20, 0),
+      /*steps=*/1);
 
   // Fire the reparent timer that should be started when an item is dragged out
   // of folder bounds.
@@ -4405,20 +4410,24 @@
 
   // Shelf should start handling the drag if it moves within its bounds.
   auto* shelf_view = GetPrimaryShelf()->GetShelfViewForTesting();
-  generator->MoveMouseTo(shelf_view->GetBoundsInScreen().left_center());
-  ASSERT_TRUE(folder_apps_grid_view()->FireDragToShelfTimerForTest());
+  UpdateDragInScreen(
+      AppsGridView::MOUSE,
+      shelf_view->GetBoundsInScreen().left_center() + gfx::Vector2d(5, 5),
+      /*steps=*/1);
+  if (!use_drag_drop_refactor()) {
+    ASSERT_TRUE(folder_apps_grid_view()->FireDragToShelfTimerForTest());
+  }
 
   EXPECT_EQ("Item 1", shelf_view->drag_and_drop_shelf_id().app_id);
 
   // Releasing drag over shelf should pin the dragged app.
-  generator->ReleaseLeftButton();
+  EndDrag();
   MaybeCheckHaptickEventsCount(1);
   EXPECT_TRUE(ShelfModel::Get()->IsAppPinned("Item 1"));
   EXPECT_EQ("Item 1", ShelfModel::Get()->items()[0].id.app_id);
 }
 
-TEST_P(AppsGridViewDragLegacyTest,
-       DragAndPinNotInitiallyVisibleFolderItemToShelf) {
+TEST_P(AppsGridViewDragTest, DragAndPinNotInitiallyVisibleFolderItemToShelf) {
   model_->CreateAndPopulateFolderWithApps(kMaxItemsInFolder);
   UpdateLayout();
 
@@ -4440,13 +4449,7 @@
   ASSERT_TRUE(app_list_folder_view()->GetBoundsInScreen().Contains(
       item_view->GetBoundsInScreen()));
 
-  auto* generator = GetEventGenerator();
-  generator->MoveMouseTo(item_view->GetBoundsInScreen().CenterPoint());
-  generator->PressLeftButton();
-  if (!use_drag_drop_refactor()) {
-    item_view->FireMouseDragTimerForTest();
-  }
-  generator->MoveMouseBy(10, 10);
+  InitiateDragForView(AppsGridView::MOUSE, item_view, apps_grid_view_);
   MaybeCheckHaptickEventsCount(1);
 
   // Verify app list item drag has started.
@@ -4454,9 +4457,11 @@
   ASSERT_TRUE(folder_apps_grid_view()->IsDragging());
   ASSERT_EQ(item_view->item(), folder_apps_grid_view()->drag_item());
 
-  generator->MoveMouseTo(
+  UpdateDragInScreen(
+      AppsGridView::MOUSE,
       app_list_folder_view()->GetBoundsInScreen().right_center() +
-      gfx::Vector2d(20, 0));
+          gfx::Vector2d(20, 0),
+      /*steps=*/1);
 
   // Fire the reparent timer that should be started when an item is dragged out
   // of folder bounds.
@@ -4464,20 +4469,25 @@
 
   // Shelf should start handling the drag if it moves within its bounds.
   auto* shelf_view = GetPrimaryShelf()->GetShelfViewForTesting();
-  generator->MoveMouseTo(shelf_view->GetBoundsInScreen().left_center());
-  ASSERT_TRUE(folder_apps_grid_view()->FireDragToShelfTimerForTest());
+  UpdateDragInScreen(
+      AppsGridView::MOUSE,
+      shelf_view->GetBoundsInScreen().left_center() + gfx::Vector2d(5, 5),
+      /*steps=*/1);
+  if (!use_drag_drop_refactor()) {
+    ASSERT_TRUE(folder_apps_grid_view()->FireDragToShelfTimerForTest());
+  }
 
   EXPECT_EQ("Item 30", shelf_view->drag_and_drop_shelf_id().app_id);
 
   // Releasing drag over shelf should pin the dragged app.
-  generator->ReleaseLeftButton();
+  EndDrag();
   MaybeCheckHaptickEventsCount(1);
 
   EXPECT_TRUE(ShelfModel::Get()->IsAppPinned("Item 30"));
   EXPECT_EQ("Item 30", ShelfModel::Get()->items()[0].id.app_id);
 }
 
-TEST_P(AppsGridViewDragLegacyTest, DragAnItemFromFolderToAndFromShelf) {
+TEST_P(AppsGridViewDragTest, DragAnItemFromFolderToAndFromShelf) {
   // Creates a folder item - the folder size was chosen arbitrarily.
   model_->CreateAndPopulateFolderWithApps(5);
   // Add more apps to the root apps grid.
@@ -4487,16 +4497,8 @@
   // Open the folder.
   test_api_->PressItemAt(0);
 
-  AppListItemView* const item_view =
-      GetItemViewInAppsGridAt(1, folder_apps_grid_view());
-
-  auto* generator = GetEventGenerator();
-  generator->MoveMouseTo(item_view->GetBoundsInScreen().CenterPoint());
-  generator->PressLeftButton();
-  if (!use_drag_drop_refactor()) {
-    item_view->FireMouseDragTimerForTest();
-  }
-  generator->MoveMouseBy(10, 10);
+  AppListItemView* const item_view = InitiateDragForItemAtCurrentPageAt(
+      AppsGridView::MOUSE, 0, 1, folder_apps_grid_view());
   MaybeCheckHaptickEventsCount(1);
 
   // Verify app list item drag has started.
@@ -4504,9 +4506,11 @@
   ASSERT_TRUE(folder_apps_grid_view()->IsDragging());
   ASSERT_EQ(item_view->item(), folder_apps_grid_view()->drag_item());
 
-  generator->MoveMouseTo(
+  UpdateDragInScreen(
+      AppsGridView::MOUSE,
       app_list_folder_view()->GetBoundsInScreen().right_center() +
-      gfx::Vector2d(20, 0));
+          gfx::Vector2d(20, 0),
+      /*steps=*/1);
 
   // Fire the reparent timer that should be started when an item is dragged out
   // of folder bounds.
@@ -4514,22 +4518,29 @@
 
   // Shelf should start handling the drag if it moves within its bounds.
   auto* shelf_view = GetPrimaryShelf()->GetShelfViewForTesting();
-  generator->MoveMouseTo(shelf_view->GetBoundsInScreen().left_center());
-  ASSERT_TRUE(folder_apps_grid_view()->FireDragToShelfTimerForTest());
+  UpdateDragInScreen(
+      AppsGridView::MOUSE,
+      shelf_view->GetBoundsInScreen().left_center() + gfx::Vector2d(5, 5),
+      /*steps=*/1);
+  if (!use_drag_drop_refactor()) {
+    ASSERT_TRUE(folder_apps_grid_view()->FireDragToShelfTimerForTest());
+  }
 
   EXPECT_EQ("Item 1", shelf_view->drag_and_drop_shelf_id().app_id);
 
   // Move the app away from shelf, and verify the app doesn't get pinned when
   // the drag ends.
-  generator->MoveMouseTo(apps_grid_view_->GetBoundsInScreen().origin());
-  generator->ReleaseLeftButton();
+  UpdateDragInScreen(AppsGridView::MOUSE,
+                     apps_grid_view_->GetBoundsInScreen().origin(),
+                     /*steps=*/1);
+  EndDrag();
   MaybeCheckHaptickEventsCount(1);
 
   EXPECT_FALSE(ShelfModel::Get()->IsAppPinned("Item 1"));
   EXPECT_TRUE(ShelfModel::Get()->items().empty());
 }
 
-TEST_P(AppsGridViewDragLegacyTest, RemoveDisplayWhileDraggingItemOntoShelf) {
+TEST_P(AppsGridViewDragTest, RemoveDisplayWhileDraggingItemOntoShelf) {
   UpdateDisplay("1024x768,1024x768");
   model_->PopulateApps(3);
 
@@ -4539,13 +4550,7 @@
 
   AppListItemView* const item_view = GetItemViewInTopLevelGrid(1);
 
-  auto* generator = GetEventGenerator();
-  generator->MoveMouseTo(item_view->GetBoundsInScreen().CenterPoint());
-  generator->PressLeftButton();
-  if (!use_drag_drop_refactor()) {
-    item_view->FireMouseDragTimerForTest();
-  }
-  generator->MoveMouseBy(10, 10);
+  InitiateDragForView(AppsGridView::MOUSE, item_view, apps_grid_view_);
   MaybeCheckHaptickEventsCount(1);
 
   // Verify that item drag has started.
@@ -4559,8 +4564,13 @@
 
   // Shelf should start handling the drag if it moves within its bounds.
   ShelfView* shelf_view = secondary_shelf->GetShelfViewForTesting();
-  generator->MoveMouseTo(shelf_view->GetBoundsInScreen().left_center());
-  ASSERT_TRUE(apps_grid_view_->FireDragToShelfTimerForTest());
+  UpdateDragInScreen(
+      AppsGridView::MOUSE,
+      shelf_view->GetBoundsInScreen().left_center() + gfx::Vector2d(5, 5),
+      /*steps=*/1);
+  if (!use_drag_drop_refactor()) {
+    ASSERT_TRUE(apps_grid_view_->FireDragToShelfTimerForTest());
+  }
 
   EXPECT_EQ("Item 1", shelf_view->drag_and_drop_shelf_id().app_id);
 
@@ -4575,8 +4585,7 @@
   EXPECT_TRUE(ShelfModel::Get()->items().empty());
 }
 
-TEST_P(AppsGridViewDragLegacyTest,
-       RemoveDisplayWhileDraggingFolderItemOntoShelf) {
+TEST_P(AppsGridViewDragTest, RemoveDisplayWhileDraggingFolderItemOntoShelf) {
   UpdateDisplay("1024x768,1024x768");
 
   // Creates a folder item - the folder size was chosen arbitrarily.
@@ -4593,14 +4602,7 @@
 
   AppListItemView* const item_view =
       GetItemViewInAppsGridAt(1, folder_apps_grid_view());
-
-  auto* generator = GetEventGenerator();
-  generator->MoveMouseTo(item_view->GetBoundsInScreen().CenterPoint());
-  generator->PressLeftButton();
-  if (!use_drag_drop_refactor()) {
-    item_view->FireMouseDragTimerForTest();
-  }
-  generator->MoveMouseBy(10, 10);
+  InitiateDragForView(AppsGridView::MOUSE, item_view, folder_apps_grid_view());
   MaybeCheckHaptickEventsCount(1);
 
   // Verify app list item drag has started.
@@ -4608,9 +4610,11 @@
   ASSERT_TRUE(folder_apps_grid_view()->IsDragging());
   ASSERT_EQ(item_view->item(), folder_apps_grid_view()->drag_item());
 
-  generator->MoveMouseTo(
+  UpdateDragInScreen(
+      AppsGridView::MOUSE,
       app_list_folder_view()->GetBoundsInScreen().right_center() +
-      gfx::Vector2d(20, 0));
+          gfx::Vector2d(20, 0),
+      /*steps=*/1);
 
   // Fire the reparent timer that should be started when an item is dragged out
   // of folder bounds.
@@ -4622,8 +4626,13 @@
 
   // Shelf should start handling the drag if it moves within its bounds.
   ShelfView* shelf_view = secondary_shelf->GetShelfViewForTesting();
-  generator->MoveMouseTo(shelf_view->GetBoundsInScreen().left_center());
-  ASSERT_TRUE(folder_apps_grid_view()->FireDragToShelfTimerForTest());
+  UpdateDragInScreen(
+      AppsGridView::MOUSE,
+      shelf_view->GetBoundsInScreen().left_center() + gfx::Vector2d(5, 5),
+      /*steps=*/1);
+  if (!use_drag_drop_refactor()) {
+    ASSERT_TRUE(folder_apps_grid_view()->FireDragToShelfTimerForTest());
+  }
 
   EXPECT_EQ("Item 1", shelf_view->drag_and_drop_shelf_id().app_id);
 
diff --git a/ash/app_list/views/search_box_view.cc b/ash/app_list/views/search_box_view.cc
index 1865e398..5ac4eba 100644
--- a/ash/app_list/views/search_box_view.cc
+++ b/ash/app_list/views/search_box_view.cc
@@ -909,6 +909,9 @@
   if (!ShouldProcessAutocomplete())
     return;
 
+  // Clear existing autocomplete text and reset the highlight range.
+  ClearAutocompleteText();
+
   const std::u16string& current_text = search_box()->GetText();
   // Currrent text is a prefix of autocomplete text.
   DCHECK(base::StartsWith(autocomplete_text, current_text,
@@ -916,6 +919,7 @@
   // Autocomplete text should not be the same as current search box text.
   DCHECK(autocomplete_text != current_text);
   // Autocomplete text should not be the same as highlighted text.
+
   const std::u16string& highlighted_text =
       autocomplete_text.substr(highlight_range_.start());
   DCHECK(highlighted_text != current_text);
diff --git a/ash/components/arc/arc_features.cc b/ash/components/arc/arc_features.cc
index 7ce32e6..9e541cb 100644
--- a/ash/components/arc/arc_features.cc
+++ b/ash/components/arc/arc_features.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "ash/components/arc/arc_features.h"
+
 #include "base/feature_list.h"
 
 namespace arc {
@@ -41,13 +42,11 @@
              "ArcIdleManager",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
-
 // For test purposes, ignore battery status changes, allowing Doze mode to
 // kick in even if we do not receive powerd changes related to battery.
 const base::FeatureParam<bool> kEnableArcIdleManagerIgnoreBatteryForPLT{
     &kEnableArcIdleManager, "ignore_battery_for_test", false};
 
-
 // Controls whether files shared to ARC Nearby Share are shared through the
 // FuseBox filesystem, instead of the default method (through a temporary path
 // managed by file manager).
@@ -83,17 +82,6 @@
              "ArcEnablePerVmCoreScheduling",
              base::FEATURE_ENABLED_BY_DEFAULT);
 
-// Controls whether to use ARC TTS caching to optimize ARC boot.
-BASE_FEATURE(kEnableTTSCaching,
-             "ArcEnableTTSCaching",
-             base::FEATURE_DISABLED_BY_DEFAULT);
-
-// Controls whether to use pregenerated ARC TTS cache to optimize ARC boot and
-// also whether or not TTS cache is used.
-BASE_FEATURE(kEnableTTSCacheSetup,
-             "ArcEnableTTSCacheSetup",
-             base::FEATURE_ENABLED_BY_DEFAULT);
-
 // Controls whether we should delegate audio focus requests from ARC to Chrome.
 BASE_FEATURE(kEnableUnifiedAudioFocusFeature,
              "ArcEnableUnifiedAudioFocus",
diff --git a/ash/components/arc/arc_features.h b/ash/components/arc/arc_features.h
index f0808fa..87fb5515 100644
--- a/ash/components/arc/arc_features.h
+++ b/ash/components/arc/arc_features.h
@@ -24,8 +24,6 @@
 BASE_DECLARE_FEATURE(kEnableArcVmDataMigration);
 BASE_DECLARE_FEATURE(kEnableLazyWebViewInit);
 BASE_DECLARE_FEATURE(kEnablePerVmCoreScheduling);
-BASE_DECLARE_FEATURE(kEnableTTSCaching);
-BASE_DECLARE_FEATURE(kEnableTTSCacheSetup);
 BASE_DECLARE_FEATURE(kEnableUnifiedAudioFocusFeature);
 BASE_DECLARE_FEATURE(kEnableUnmanagedToManagedTransitionFeature);
 BASE_DECLARE_FEATURE(kEnableUsap);
diff --git a/ash/components/arc/session/arc_session_impl.cc b/ash/components/arc/session/arc_session_impl.cc
index d340981..227eb1ee 100644
--- a/ash/components/arc/session/arc_session_impl.cc
+++ b/ash/components/arc/session/arc_session_impl.cc
@@ -467,12 +467,13 @@
   params.num_cores_disabled = num_cores_disabled;
   params.enable_notifications_refresh =
       ash::features::IsNotificationsRefreshEnabled();
-  params.enable_tts_caching =
-      base::FeatureList::IsEnabled(kEnableTTSCacheSetup);
+  params.enable_tts_caching = true;
   params.enable_consumer_auto_update_toggle = base::FeatureList::IsEnabled(
       ash::features::kConsumerAutoUpdateToggleAllowed);
   params.enable_privacy_hub_for_chrome =
       base::FeatureList::IsEnabled(ash::features::kCrosPrivacyHub);
+  params.arc_switch_to_keymint =
+      base::FeatureList::IsEnabled(kSwitchToKeyMintOnT);
   params.use_virtio_blk_data = use_virtio_blk_data_;
 
   // TODO (b/196460968): Remove after CTS run is complete.
diff --git a/ash/components/arc/session/arc_session_impl_unittest.cc b/ash/components/arc/session/arc_session_impl_unittest.cc
index 107e22ece..e8ee8e5 100644
--- a/ash/components/arc/session/arc_session_impl_unittest.cc
+++ b/ash/components/arc/session/arc_session_impl_unittest.cc
@@ -940,17 +940,6 @@
       GetClient(arc_session.get())->last_start_params().enable_tts_caching);
 }
 
-// Test that validates TTS caching is enabled.
-TEST_F(ArcSessionImplTest, TTSCachingEnabled) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitWithFeatureState(arc::kEnableTTSCacheSetup, true);
-  auto arc_session = CreateArcSession();
-  arc_session->StartMiniInstance();
-  base::RunLoop().RunUntilIdle();
-  EXPECT_TRUE(
-      GetClient(arc_session.get())->last_start_params().enable_tts_caching);
-}
-
 // Test "<<" operator for ArcSessionImpl::State type.
 TEST_F(ArcSessionImplTest, StateTypeStreamOutput) {
   EXPECT_EQ(ConvertToString(ArcSessionImpl::State::NOT_STARTED), "NOT_STARTED");
diff --git a/ash/components/arc/session/arc_start_params.h b/ash/components/arc/session/arc_start_params.h
index 16da74a..a995ea4 100644
--- a/ash/components/arc/session/arc_start_params.h
+++ b/ash/components/arc/session/arc_start_params.h
@@ -110,6 +110,9 @@
 
   // Flag to enable Privacy Hub for chrome.
   bool enable_privacy_hub_for_chrome = false;
+
+  // Flag to switch to KeyMint for T+.
+  bool arc_switch_to_keymint = false;
 };
 
 }  // namespace arc
diff --git a/ash/components/arc/session/arc_upgrade_params.cc b/ash/components/arc/session/arc_upgrade_params.cc
index 89ca30af..13355bd 100644
--- a/ash/components/arc/session/arc_upgrade_params.cc
+++ b/ash/components/arc/session/arc_upgrade_params.cc
@@ -37,9 +37,7 @@
       skip_gms_core_cache(base::CommandLine::ForCurrentProcess()->HasSwitch(
           ash::switches::kArcDisableGmsCoreCache)),
       skip_tts_cache(base::CommandLine::ForCurrentProcess()->HasSwitch(
-                         ash::switches::kArcDisableTtsCache) ||
-                     !base::FeatureList::IsEnabled(arc::kEnableTTSCacheSetup)) {
-}
+          ash::switches::kArcDisableTtsCache)) {}
 
 UpgradeParams::UpgradeParams(const UpgradeParams& other) = default;
 UpgradeParams::UpgradeParams(UpgradeParams&& other) = default;
diff --git a/ash/components/arc/session/arc_upgrade_params_unittest.cc b/ash/components/arc/session/arc_upgrade_params_unittest.cc
index 97c4d5f..ade90da9 100644
--- a/ash/components/arc/session/arc_upgrade_params_unittest.cc
+++ b/ash/components/arc/session/arc_upgrade_params_unittest.cc
@@ -27,12 +27,5 @@
   EXPECT_FALSE(upgradeParams.skip_tts_cache);
 }
 
-TEST(ArcUpgradeParamsTest, Constructor_WithTtsCacheSetupFeatureDisabled) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitWithFeatureState(arc::kEnableTTSCacheSetup, true);
-  UpgradeParams upgradeParams;
-  EXPECT_FALSE(upgradeParams.skip_tts_cache);
-}
-
 }  // namespace
 }  // namespace arc
diff --git a/ash/components/arc/session/arc_vm_client_adapter.cc b/ash/components/arc/session/arc_vm_client_adapter.cc
index 7277360..c0d8192 100644
--- a/ash/components/arc/session/arc_vm_client_adapter.cc
+++ b/ash/components/arc/session/arc_vm_client_adapter.cc
@@ -366,6 +366,10 @@
 
   mini_instance_request->set_enable_privacy_hub_for_chrome(
       base::FeatureList::IsEnabled(ash::features::kCrosPrivacyHub));
+  if (GetArcAndroidSdkVersionAsInt() == kArcVersionT) {
+    mini_instance_request->set_arc_switch_to_keymint(
+        base::FeatureList::IsEnabled(kSwitchToKeyMintOnT));
+  }
 
   request.set_enable_rw(file_system_status.is_host_rootfs_writable() &&
                         file_system_status.is_system_image_ext_format());
diff --git a/ash/constants/BUILD.gn b/ash/constants/BUILD.gn
index f7c86c3a..385a0d5 100644
--- a/ash/constants/BUILD.gn
+++ b/ash/constants/BUILD.gn
@@ -12,6 +12,8 @@
   sources = [
     "ambient_theme.cc",
     "ambient_theme.h",
+    "ambient_video.cc",
+    "ambient_video.h",
     "app_types.h",
     "ash_constants.cc",
     "ash_constants.h",
diff --git a/ash/constants/ambient_theme.cc b/ash/constants/ambient_theme.cc
index a8932e4..61c5ba2 100644
--- a/ash/constants/ambient_theme.cc
+++ b/ash/constants/ambient_theme.cc
@@ -17,10 +17,8 @@
       return "FeelTheBreeze";
     case AmbientTheme::kFloatOnBy:
       return "FloatOnBy";
-    case AmbientTheme::kVideoNewMexico:
-      return "NewMexico";
-    case AmbientTheme::kVideoClouds:
-      return "Clouds";
+    case AmbientTheme::kVideo:
+      return "Video";
   }
 }
 
diff --git a/ash/constants/ambient_theme.h b/ash/constants/ambient_theme.h
index e817632..75cf634 100644
--- a/ash/constants/ambient_theme.h
+++ b/ash/constants/ambient_theme.h
@@ -25,9 +25,8 @@
   kFloatOnBy = 2,
   // Scenic videos that get played on loop at full screen. The videos are static
   // and Google-owned.
-  kVideoNewMexico = 3,
-  kVideoClouds = 4,
-  kMaxValue = kVideoClouds,
+  kVideo = 3,
+  kMaxValue = kVideo,
 };
 
 inline constexpr AmbientTheme kDefaultAmbientTheme = AmbientTheme::kSlideshow;
diff --git a/ash/constants/ambient_video.cc b/ash/constants/ambient_video.cc
new file mode 100644
index 0000000..2d27b9e
--- /dev/null
+++ b/ash/constants/ambient_video.cc
@@ -0,0 +1,21 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/constants/ambient_video.h"
+
+namespace ash {
+
+base::StringPiece ToString(AmbientVideo video) {
+  // See the "AmbientModeThemes" <variants> tag in histograms.xml. These names
+  // are currently used for metrics purposes, so they cannot be arbitrarily
+  // renamed.
+  switch (video) {
+    case AmbientVideo::kVideoNewMexico:
+      return "NewMexico";
+    case AmbientVideo::kVideoClouds:
+      return "Clouds";
+  }
+}
+
+}  // namespace ash
diff --git a/ash/constants/ambient_video.h b/ash/constants/ambient_video.h
new file mode 100644
index 0000000..66cee3a
--- /dev/null
+++ b/ash/constants/ambient_video.h
@@ -0,0 +1,34 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ASH_CONSTANTS_AMBIENT_VIDEO_H_
+#define ASH_CONSTANTS_AMBIENT_VIDEO_H_
+
+#include "base/component_export.h"
+#include "base/strings/string_piece.h"
+
+namespace ash {
+
+// Only applies when |AmbientTheme::kVideo| is active.
+//
+// Each corresponds to a video in ambient mode that can be selected by the user.
+// The videos get played on loop at full screen. They are static and
+// Google-owned.
+//
+// These values are persisted in user pref storage and logs, so they should
+// never be renumbered or reused.
+enum class AmbientVideo {
+  kVideoNewMexico = 0,
+  kVideoClouds = 1,
+  kMaxValue = kVideoClouds,
+};
+
+// The returned StringPiece is guaranteed to be null-terminated and point to
+// memory valid for the lifetime of the program.
+COMPONENT_EXPORT(ASH_CONSTANTS)
+base::StringPiece ToString(AmbientVideo video);
+
+}  // namespace ash
+
+#endif  // ASH_CONSTANTS_AMBIENT_THEME_H_
diff --git a/ash/constants/ash_features.cc b/ash/constants/ash_features.cc
index 77fa0927..1b78f90b 100644
--- a/ash/constants/ash_features.cc
+++ b/ash/constants/ash_features.cc
@@ -286,10 +286,6 @@
              "BorealisStorageBallooning",
              base::FEATURE_ENABLED_BY_DEFAULT);
 
-// Enable or disable calendar view from the system tray. Also enables the system
-// tray to show date in the shelf when the screen is sufficiently large.
-BASE_FEATURE(kCalendarView, "CalendarView", base::FEATURE_ENABLED_BY_DEFAULT);
-
 // Enable or disable debug mode for CalendarModel.
 BASE_FEATURE(kCalendarModelDebugMode,
              "CalendarModelDebugMode",
@@ -2376,10 +2372,6 @@
   return base::FeatureList::IsEnabled(kBluetoothQualityReport);
 }
 
-bool IsCalendarViewEnabled() {
-  return base::FeatureList::IsEnabled(kCalendarView);
-}
-
 bool IsCalendarModelDebugModeEnabled() {
   return base::FeatureList::IsEnabled(kCalendarModelDebugMode);
 }
diff --git a/ash/constants/ash_features.h b/ash/constants/ash_features.h
index 6cef393..41221e5 100644
--- a/ash/constants/ash_features.h
+++ b/ash/constants/ash_features.h
@@ -83,7 +83,6 @@
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kBorealisPermitted);
 COMPONENT_EXPORT(ASH_CONSTANTS)
 BASE_DECLARE_FEATURE(kBorealisStorageBallooning);
-COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kCalendarView);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kCalendarModelDebugMode);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kCalendarJelly);
 COMPONENT_EXPORT(ASH_CONSTANTS)
@@ -650,7 +649,6 @@
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBackgroundBlurEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBentoBarEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBluetoothQualityReportEnabled();
-COMPONENT_EXPORT(ASH_CONSTANTS) bool IsCalendarViewEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsCalendarModelDebugModeEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsCalendarJellyEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsCaptivePortalErrorPageEnabled();
diff --git a/ash/curtain/security_curtain_controller_impl_unittest.cc b/ash/curtain/security_curtain_controller_impl_unittest.cc
index dba285d05..fb0864f0 100644
--- a/ash/curtain/security_curtain_controller_impl_unittest.cc
+++ b/ash/curtain/security_curtain_controller_impl_unittest.cc
@@ -14,6 +14,7 @@
 #include "ash/system/power/power_button_menu_view.h"
 #include "ash/system/power/power_button_test_base.h"
 #include "ash/test/ash_test_base.h"
+#include "base/check_deref.h"
 #include "chromeos/ash/components/audio/cras_audio_handler.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -247,6 +248,14 @@
     return *power_button_test_api().GetPowerButtonMenuView()->GetWidget();
   }
 
+  const aura::Window& GetPowerMenuWidgetContainerParent() {
+    EXPECT_TRUE(power_button_test_api().IsMenuOpened());
+
+    return CHECK_DEREF(Shell::GetPrimaryRootWindow()
+                           ->GetChildById(kShellWindowId_PowerMenuContainer)
+                           ->parent());
+  }
+
  private:
   std::unique_ptr<PowerButtonControllerTestApi> power_button_test_api_;
 };
@@ -572,15 +581,17 @@
 TEST_F(SecurityCurtainControllerImplTest,
        ShouldResetParentOfPowerMenuWidgetWhenDisabled) {
   PressPowerButton();
-  views::Widget* parent_before_enabled = GetOpenPowerWidget().parent();
+  const aura::Window& parent_before_enabled =
+      GetPowerMenuWidgetContainerParent();
   ReleasePowerButton();
 
   security_curtain_controller().Enable(init_params());
   security_curtain_controller().Disable();
   PressPowerButton();
-  views::Widget* parent_after_disabled = GetOpenPowerWidget().parent();
+  const aura::Window& parent_after_disabled =
+      GetPowerMenuWidgetContainerParent();
 
-  ASSERT_EQ(parent_before_enabled, parent_after_disabled);
+  ASSERT_EQ(&parent_before_enabled, &parent_after_disabled);
 }
 
 }  // namespace ash::curtain
diff --git a/ash/events/accessibility_event_rewriter_unittest.cc b/ash/events/accessibility_event_rewriter_unittest.cc
index 72d089ce..82e41540 100644
--- a/ash/events/accessibility_event_rewriter_unittest.cc
+++ b/ash/events/accessibility_event_rewriter_unittest.cc
@@ -188,7 +188,7 @@
     return true;
   }
 
-  bool TopRowKeysAreFunctionKeys() const override { return false; }
+  bool TopRowKeysAreFunctionKeys(int device_id) const override { return false; }
 
   bool IsExtensionCommandRegistered(ui::KeyboardCode key_code,
                                     int flags) const override {
@@ -583,7 +583,7 @@
     return true;
   }
 
-  bool TopRowKeysAreFunctionKeys() const override { return false; }
+  bool TopRowKeysAreFunctionKeys(int device_id) const override { return false; }
 
   bool IsExtensionCommandRegistered(ui::KeyboardCode key_code,
                                     int flags) const override {
diff --git a/ash/public/cpp/ambient/ambient_prefs.cc b/ash/public/cpp/ambient/ambient_prefs.cc
index 10ad8d2..f4fdc881 100644
--- a/ash/public/cpp/ambient/ambient_prefs.cc
+++ b/ash/public/cpp/ambient/ambient_prefs.cc
@@ -19,6 +19,11 @@
 // theme".
 constexpr char kAmbientTheme[] = "ash.ambient.animation_theme";
 
+constexpr char kAmbientUiSettings[] = "ash.ambient.ui_settings";
+
+constexpr char kAmbientUiSettingsFieldTheme[] = "theme";
+constexpr char kAmbientUiSettingsFieldVideo[] = "video";
+
 constexpr char kAmbientBackdropClientId[] = "ash.ambient.backdrop.client.id";
 
 constexpr char kAmbientModeEnabled[] = "settings.ambient_mode.enabled";
diff --git a/ash/public/cpp/ambient/ambient_prefs.h b/ash/public/cpp/ambient/ambient_prefs.h
index 4131932..de6e3ae 100644
--- a/ash/public/cpp/ambient/ambient_prefs.h
+++ b/ash/public/cpp/ambient/ambient_prefs.h
@@ -13,8 +13,22 @@
 
 // Integer pref corresponding to the ambient mode theme that the user has
 // selected (see AmbientTheme enum).
+// DEPRECATED: Use |kAmbientUiSettings| instead; that's the successor.
 ASH_PUBLIC_EXPORT extern const char kAmbientTheme[];
 
+// Dictionary pref capturing the ambient UI that the user has selected:
+// {
+//   // Required.
+//   "theme": <integer value of |AmbientTheme| enum>
+//   // Which video the user picked. Only used if the "theme" is |kVideo|.
+//   "video": <integer value of |AmbientVideo| enum>
+// }
+ASH_PUBLIC_EXPORT extern const char kAmbientUiSettings[];
+
+// Fields in the |kAmbientUiSettings| dictionary.
+ASH_PUBLIC_EXPORT extern const char kAmbientUiSettingsFieldTheme[];
+ASH_PUBLIC_EXPORT extern const char kAmbientUiSettingsFieldVideo[];
+
 // A GUID for backdrop client.
 ASH_PUBLIC_EXPORT extern const char kAmbientBackdropClientId[];
 
diff --git a/ash/public/cpp/ash_view_ids.h b/ash/public/cpp/ash_view_ids.h
index 0b923e8..fcb83e9 100644
--- a/ash/public/cpp/ash_view_ids.h
+++ b/ash/public/cpp/ash_view_ids.h
@@ -19,6 +19,7 @@
   VIEW_ID_ACCESSIBILITY_VIRTUAL_KEYBOARD_ENABLED,
 
   // Feature tile ids.
+  VIEW_ID_ACCESSIBILITY_FEATURE_TILE,
   VIEW_ID_SCREEN_CAPTURE_FEATURE_TILE,
   VIEW_ID_DND_FEATURE_TILE,
   VIEW_ID_AUTOROTATE_FEATURE_TILE,
diff --git a/ash/public/cpp/input_device_settings_controller.cc b/ash/public/cpp/input_device_settings_controller.cc
index 48af99d..2bebeda 100644
--- a/ash/public/cpp/input_device_settings_controller.cc
+++ b/ash/public/cpp/input_device_settings_controller.cc
@@ -10,6 +10,12 @@
 InputDeviceSettingsController* g_instance = nullptr;
 }
 
+template <>
+InputDeviceSettingsController*& InputDeviceSettingsController::
+    ScopedResetterForTest::GetGlobalInstanceHolder() {
+  return g_instance;
+}
+
 InputDeviceSettingsController::InputDeviceSettingsController() {
   DCHECK_EQ(nullptr, g_instance);
   g_instance = this;
diff --git a/ash/public/cpp/input_device_settings_controller.h b/ash/public/cpp/input_device_settings_controller.h
index 9552b6a..442b675 100644
--- a/ash/public/cpp/input_device_settings_controller.h
+++ b/ash/public/cpp/input_device_settings_controller.h
@@ -8,6 +8,7 @@
 #include <vector>
 
 #include "ash/public/cpp/ash_public_export.h"
+#include "ash/public/cpp/scoped_singleton_resetter_for_test.h"
 #include "ash/public/mojom/input_device_settings.mojom-forward.h"
 #include "base/observer_list_types.h"
 
@@ -18,6 +19,8 @@
 class ASH_PUBLIC_EXPORT InputDeviceSettingsController {
  public:
   using DeviceId = uint32_t;
+  using ScopedResetterForTest =
+      ScopedSingletonResetterForTest<InputDeviceSettingsController>;
 
   class Observer : public base::CheckedObserver {
    public:
diff --git a/ash/public/cpp/system_tray_test_api.h b/ash/public/cpp/system_tray_test_api.h
index 4dd877c..7e0cabf 100644
--- a/ash/public/cpp/system_tray_test_api.h
+++ b/ash/public/cpp/system_tray_test_api.h
@@ -12,10 +12,16 @@
 
 namespace message_center {
 class MessagePopupView;
-}
+}  // namespace message_center
+
+namespace views {
+class ScrollView;
+}  // namespace views
 
 namespace ash {
 
+class AccessibilityDetailedView;
+
 // Public test API for the system tray. Methods only apply to the system tray
 // on the primary display.
 class ASH_EXPORT SystemTrayTestApi {
@@ -47,12 +53,23 @@
   void ShowAccessibilityDetailedView();
   void ShowNetworkDetailedView();
 
+  // Returns the current `ash::AccessibilityDetailedView`. This assumes that the
+  // accessibility detailed view is currently showing.
+  AccessibilityDetailedView* GetAccessibilityDetailedView();
+
   // Returns true if the view exists in the bubble and is visible.
   // If |open_tray| is true, it also opens system tray bubble.
   bool IsBubbleViewVisible(int view_id, bool open_tray);
 
-  // If the view is in a scroll contents, scrolls the bubble to shown the view.
-  void ScrollToShowView(int view_id);
+  // Returns true if the `TrayToggleButton` with ID `view_id` is toggled on,
+  // false otherwise.
+  bool IsToggleOn(int view_id);
+
+  // Searches for a `views::View` having ID `view_id`, and then scrolls it onto
+  // the screen to make it visible (if it is already visible then no scrolling
+  // is performed). The view should be a descendant of `scroll_view` (this is
+  // `DCHECK`ed).
+  void ScrollToShowView(views::ScrollView* scroll_view, int view_id);
 
   // Clicks the view |view_id|.
   void ClickBubbleView(int view_id);
diff --git a/ash/shelf/scrollable_shelf_view_unittest.cc b/ash/shelf/scrollable_shelf_view_unittest.cc
index 33d837f..2eb5c186 100644
--- a/ash/shelf/scrollable_shelf_view_unittest.cc
+++ b/ash/shelf/scrollable_shelf_view_unittest.cc
@@ -91,10 +91,12 @@
   void InkDropRippleAnimationEnded(
       views::InkDropState ink_drop_state) override {
     if (ink_drop_state != views::InkDropState::ACTIVATED &&
-        ink_drop_state != views::InkDropState::HIDDEN)
+        ink_drop_state != views::InkDropState::HIDDEN) {
       return;
-    if (run_loop_.get())
+    }
+    if (run_loop_.get()) {
       run_loop_->Quit();
+    }
   }
 
   views::Button* button_ = nullptr;
@@ -159,8 +161,9 @@
   void AddAppShortcutsUntilRightArrowIsShown() {
     ASSERT_FALSE(scrollable_shelf_view_->right_arrow()->GetVisible());
 
-    while (!scrollable_shelf_view_->right_arrow()->GetVisible())
+    while (!scrollable_shelf_view_->right_arrow()->GetVisible()) {
       AddAppShortcut();
+    }
   }
 
   void CheckFirstAndLastTappableIconsBounds() {
@@ -256,8 +259,9 @@
   // Adds enough app icons so that after display rotation the scrollable
   // shelf is still in overflow mode.
   const int num = display.bounds().height() / shelf_view_->GetButtonSize();
-  for (int i = 0; i < num; i++)
+  for (int i = 0; i < num; i++) {
     AddAppShortcut();
+  }
 
   // Because the display's height is greater than the display's width,
   // the scrollable shelf is in overflow mode before display rotation.
@@ -572,8 +576,9 @@
 // (https://crbug.com/1035596).
 TEST_P(ScrollableShelfViewRTLTest, CheckTappableIndicesOnSecondDisplay) {
   constexpr size_t icon_number = 5;
-  for (size_t i = 0; i < icon_number; i++)
+  for (size_t i = 0; i < icon_number; i++) {
     AddAppShortcut();
+  }
 
   // Adds the second display.
   UpdateDisplay("600x800,600x800");
@@ -597,8 +602,9 @@
 // after switching to tablet mode (https://crbug.com/1017979).
 TEST_P(ScrollableShelfViewRTLTest, CorrectUIAfterSwitchingToTablet) {
   // Add enough app shortcuts to ensure that at least three pages of icons show.
-  for (int i = 0; i < 25; i++)
+  for (int i = 0; i < 25; i++) {
     AddAppShortcut();
+  }
   ASSERT_EQ(ScrollableShelfView::kShowRightArrowButton,
             scrollable_shelf_view_->layout_strategy_for_test());
 
@@ -658,8 +664,9 @@
 TEST_P(ScrollableShelfViewRTLTest, CorrectUIInTabletWithoutOverflow) {
   Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
 
-  for (int i = 0; i < 3; i++)
+  for (int i = 0; i < 3; i++) {
     AddAppShortcut();
+  }
   ASSERT_EQ(ScrollableShelfView::kNotShowArrowButtons,
             scrollable_shelf_view_->layout_strategy_for_test());
 
@@ -1215,8 +1222,9 @@
             scrollable_shelf_view_->layout_strategy_for_test());
 
   // Pins the icons of running apps to the shelf.
-  for (size_t i = 0; i < 2 * num; i++)
+  for (size_t i = 0; i < 2 * num; i++) {
     AddAppShortcut(ShelfItemType::TYPE_APP);
+  }
 
   {
     ShelfID shelf_id = AddAppShortcut();
@@ -1242,22 +1250,21 @@
   ~ScrollableShelfViewWithAppScalingTest() override = default;
 
   void SetUp() override {
-    // When the calendar view is enabled, the status widget's bounds could vary
-    // under different dates. For example, "June 10" is longer than "June 9".
+    // With the calendar view, the status widget's bounds could vary under
+    // different dates. For example, "June 10" is longer than "June 9".
     // Therefore, the code below sets the constant date to avoid flakiness.
-    if (features::IsCalendarViewEnabled()) {
-      scoped_locale_ =
-          std::make_unique<base::test::ScopedRestoreICUDefaultLocale>("en_US"),
-      time_zone_ = std::make_unique<base::test::ScopedRestoreDefaultTimezone>(
-          "America/Chicago");
 
-      constexpr char kFakeNowTimeString[] = "Sunday, 5 June 2022 14:30:00 CDT";
-      ASSERT_TRUE(base::Time::FromString(kFakeNowTimeString,
-                                         &TimeOverrideHelper::current_time));
-      time_override_ = std::make_unique<base::subtle::ScopedTimeClockOverrides>(
-          &TimeOverrideHelper::TimeNow, /*time_ticks_override=*/nullptr,
-          /*thread_ticks_override=*/nullptr);
-    }
+    scoped_locale_ =
+        std::make_unique<base::test::ScopedRestoreICUDefaultLocale>("en_US"),
+    time_zone_ = std::make_unique<base::test::ScopedRestoreDefaultTimezone>(
+        "America/Chicago");
+
+    constexpr char kFakeNowTimeString[] = "Sunday, 5 June 2022 14:30:00 CDT";
+    ASSERT_TRUE(base::Time::FromString(kFakeNowTimeString,
+                                       &TimeOverrideHelper::current_time));
+    time_override_ = std::make_unique<base::subtle::ScopedTimeClockOverrides>(
+        &TimeOverrideHelper::TimeNow, /*time_ticks_override=*/nullptr,
+        /*thread_ticks_override=*/nullptr);
 
     ScrollableShelfViewTest::SetUp();
 
@@ -1306,10 +1313,8 @@
 // its children's sizes if there is insufficient space for shelf buttons to show
 // without scrolling.
 TEST_F(ScrollableShelfViewWithAppScalingTest, AppScalingBasics) {
-  if (features::IsCalendarViewEnabled())
-    PopulateAppShortcut(kAppCountWithShowingDateTray);
-  else
-    PopulateAppShortcut(kAppCount);
+  PopulateAppShortcut(kAppCountWithShowingDateTray);
+
   HotseatWidget* hotseat_widget =
       GetPrimaryShelf()->shelf_widget()->hotseat_widget();
   EXPECT_EQ(HotseatDensity::kNormal, hotseat_widget->target_hotseat_density());
@@ -1352,10 +1357,8 @@
 // Verifies that app scaling works as expected with hotseat state transition.
 TEST_F(ScrollableShelfViewWithAppScalingTest,
        VerifyWithHotseatStateTransition) {
-  if (features::IsCalendarViewEnabled())
-    PopulateAppShortcut(kAppCountWithShowingDateTray);
-  else
-    PopulateAppShortcut(kAppCount);
+  PopulateAppShortcut(kAppCountWithShowingDateTray);
+
   HotseatWidget* hotseat_widget =
       GetPrimaryShelf()->shelf_widget()->hotseat_widget();
   EXPECT_EQ(HotseatDensity::kNormal, hotseat_widget->target_hotseat_density());
diff --git a/ash/shelf/shelf_view.cc b/ash/shelf/shelf_view.cc
index 86f3867..4d29c8a 100644
--- a/ash/shelf/shelf_view.cc
+++ b/ash/shelf/shelf_view.cc
@@ -8,11 +8,13 @@
 #include <utility>
 
 #include "ash/app_list/app_list_controller_impl.h"
+#include "ash/app_list/app_list_item_util.h"
 #include "ash/app_list/views/app_drag_icon_proxy.h"
 #include "ash/app_list/views/ghost_image_view.h"
 #include "ash/constants/ash_features.h"
 #include "ash/keyboard/keyboard_util.h"
 #include "ash/keyboard/ui/keyboard_ui_controller.h"
+#include "ash/public/cpp/app_list/app_list_features.h"
 #include "ash/public/cpp/metrics_util.h"
 #include "ash/public/cpp/shelf_item.h"
 #include "ash/public/cpp/shelf_model.h"
@@ -65,6 +67,7 @@
 #include "components/services/app_service/public/cpp/app_types.h"
 #include "ui/accessibility/ax_node_data.h"
 #include "ui/aura/window.h"
+#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/base/menu_source_utils.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
@@ -1183,6 +1186,8 @@
     return;
   }
 
+  DCHECK(app_list_features::IsDragAndDropRefactorEnabled() ||
+         !drag_icon_proxy_);
   drag_icon_proxy_ = std::move(icon_proxy);
 
   views::View* drag_and_drop_view =
@@ -1352,6 +1357,8 @@
                          base::Unretained(this),
                          std::make_unique<ViewOpacityResetter>(drag_view_)));
     }
+  } else if (drag_view_) {
+    drag_view_->layer()->SetOpacity(1.0f);
   }
 
   // If the drag pointer is NONE, no drag operation is going on and the
@@ -1688,7 +1695,9 @@
 
       return;
     }
-    drag_icon_proxy_->UpdatePosition(screen_location);
+    if (drag_icon_proxy_) {
+      drag_icon_proxy_->UpdatePosition(screen_location);
+    }
     return;
   }
 
@@ -1724,7 +1733,9 @@
       }
       // Make the item partially disappear to show that it will get removed if
       // dropped.
-      drag_icon_proxy_->SetOpacity(kDraggedImageOpacity);
+      if (drag_icon_proxy_) {
+        drag_icon_proxy_->SetOpacity(kDraggedImageOpacity);
+      }
     }
   }
 }
@@ -1737,7 +1748,6 @@
 
   // Coming here we should always have a |drag_view_|.
   DCHECK(drag_view_);
-  DCHECK(drag_icon_proxy_);
 
   delegate_->CancelScrollForItemDrag();
 
@@ -1982,7 +1992,7 @@
 
 gfx::Rect ShelfView::GetDragIconBoundsInScreenForTest() const {
   if (!drag_icon_proxy_)
-    return gfx::Rect();
+    return drag_view_ ? drag_view_->GetBoundsInScreen() : gfx::Rect();
   return drag_icon_proxy_->GetBoundsInScreen();
 }
 
@@ -2779,6 +2789,80 @@
       [this](size_t idx) { return IsItemPinned(model_->items()[idx]); });
 }
 
+views::View::DropCallback ShelfView::GetDropCallback(
+    const ui::DropTargetEvent& event) {
+  return app_list_features::IsDragAndDropRefactorEnabled()
+             ? base::BindOnce(&ShelfView::EndDragCallback,
+                              base::Unretained(this))
+             : base::DoNothing();
+}
+
+void ShelfView::EndDragCallback(const ui::DropTargetEvent& event,
+                                ui::mojom::DragOperation& output_drag_op) {
+  // TODO(b/271601288): Hook up drop animation with the drag image icon.
+  output_drag_op = ui::mojom::DragOperation::kMove;
+  EndDrag(false, /*icon_proxy = */ nullptr);
+}
+
+bool ShelfView::GetDropFormats(
+    int* formats,
+    std::set<ui::ClipboardFormatType>* format_types) {
+  if (app_list_features::IsDragAndDropRefactorEnabled()) {
+    format_types->insert(GetAppItemFormatType());
+  }
+  return true;
+}
+
+bool ShelfView::CanDrop(const OSExchangeData& data) {
+  if (!app_list_features::IsDragAndDropRefactorEnabled()) {
+    return true;
+  }
+
+  auto app_id = GetAppIdFromDropData(data);
+  if (app_id->empty()) {
+    return false;
+  }
+
+  std::set<ui::ClipboardFormatType> format_types;
+  format_types.insert(GetAppItemFormatType());
+  return data.HasAnyFormat(0, format_types);
+}
+
+void ShelfView::OnDragExited() {
+  if (!app_list_features::IsDragAndDropRefactorEnabled()) {
+    views::View::OnDragExited();
+    return;
+  }
+  EndDrag(/*cancel=*/true, nullptr);
+}
+
+void ShelfView::OnDragEntered(const ui::DropTargetEvent& event) {
+  if (!app_list_features::IsDragAndDropRefactorEnabled()) {
+    views::View::OnDragEntered(event);
+    return;
+  }
+
+  auto app_id = GetAppIdFromDropData(event.data());
+  if (app_id->empty()) {
+    views::View::OnDragEntered(event);
+    return;
+  }
+
+  gfx::Point drag_point_in_screen = event.location();
+  views::View::ConvertPointToScreen(this, &drag_point_in_screen);
+  StartDrag(app_id.value(), drag_point_in_screen, gfx::Rect());
+}
+
+int ShelfView::OnDragUpdated(const ui::DropTargetEvent& event) {
+  if (app_list_features::IsDragAndDropRefactorEnabled()) {
+    gfx::Point drag_point_in_screen = event.location();
+    views::View::ConvertPointToScreen(this, &drag_point_in_screen);
+    Drag(drag_point_in_screen,
+         drag_view_ ? drag_view_->GetBoundsInScreen() : gfx::Rect());
+  }
+  return ui::DragDropTypes::DRAG_MOVE;
+}
+
 BEGIN_METADATA(ShelfView, views::AccessiblePaneView)
 END_METADATA
 
diff --git a/ash/shelf/shelf_view.h b/ash/shelf/shelf_view.h
index 7f6bda8..7ad481b 100644
--- a/ash/shelf/shelf_view.h
+++ b/ash/shelf/shelf_view.h
@@ -172,6 +172,17 @@
   void GetAccessibleNodeData(ui::AXNodeData* node_data) override;
   View* GetTooltipHandlerForPoint(const gfx::Point& point) override;
 
+  bool CanDrop(const OSExchangeData& data) override;
+  void OnDragEntered(const ui::DropTargetEvent& event) override;
+  void OnDragExited() override;
+  int OnDragUpdated(const ui::DropTargetEvent& event) override;
+  DropCallback GetDropCallback(const ui::DropTargetEvent& event) override;
+  bool GetDropFormats(int* formats,
+                      std::set<ui::ClipboardFormatType>* format_types) override;
+
+  void EndDragCallback(const ui::DropTargetEvent& event,
+                       ui::mojom::DragOperation& output_drag_op);
+
   // ShelfButtonDelegate:
   void OnShelfButtonAboutToRequestFocusFromTabTraversal(ShelfButton* button,
                                                         bool reverse) override;
diff --git a/ash/shelf/shelf_view_unittest.cc b/ash/shelf/shelf_view_unittest.cc
index 71fa538f..c799b883 100644
--- a/ash/shelf/shelf_view_unittest.cc
+++ b/ash/shelf/shelf_view_unittest.cc
@@ -162,8 +162,9 @@
 
   bool RunPendingContextMenuCallback(
       std::unique_ptr<ui::SimpleMenuModel> model) {
-    if (pending_context_menu_callback_.is_null())
+    if (pending_context_menu_callback_.is_null()) {
       return false;
+    }
     std::move(pending_context_menu_callback_).Run(std::move(model));
     return true;
   }
@@ -392,8 +393,9 @@
     // Set a delegate; some tests require one to select the item.
     model_->ReplaceShelfItemDelegate(
         item.id, std::make_unique<ShelfItemSelectionTracker>());
-    if (wait_for_animations)
+    if (wait_for_animations) {
       test_api_->RunMessageLoopUntilAnimationsDone();
+    }
     return item.id;
   }
   ShelfID AddAppShortcut() { return AddItem(TYPE_PINNED_APP, true); }
@@ -527,8 +529,9 @@
     if (progressively) {
       int sgn = dist_x > 0 ? 1 : -1;
       dist_x = abs(dist_x);
-      for (; dist_x; dist_x -= std::min(10, dist_x))
+      for (; dist_x; dist_x -= std::min(10, dist_x)) {
         DoDrag(sgn * std::min(10, abs(dist_x)), 0, button, pointer, to);
+      }
     } else {
       DoDrag(dist_x, dist_y, button, pointer, to);
     }
@@ -550,12 +553,14 @@
       ContinueDrag(button, pointer, button_index, destination_index, false);
     } else if (button_index < destination_index) {
       for (int cur_index = button_index + 1; cur_index <= destination_index;
-           cur_index++)
+           cur_index++) {
         ContinueDrag(button, pointer, cur_index - 1, cur_index, true);
+      }
     } else if (button_index > destination_index) {
       for (int cur_index = button_index - 1; cur_index >= destination_index;
-           cur_index--)
+           cur_index--) {
         ContinueDrag(button, pointer, cur_index + 1, cur_index, true);
+      }
     }
     return button;
   }
@@ -1318,8 +1323,9 @@
   // The tooltip shouldn't hide if the mouse is on normal buttons.
   for (size_t i = 0; i < test_api_->GetButtonCount(); i++) {
     ShelfAppButton* button = test_api_->GetButton(i);
-    if (!button)
+    if (!button) {
       continue;
+    }
     EXPECT_FALSE(shelf_view_->ShouldHideTooltip(
         button->GetMirroredBounds().CenterPoint()))
         << "ShelfView tries to hide on button " << i;
@@ -1332,11 +1338,13 @@
   int right = 0;
   for (size_t i = 0; i < test_api_->GetButtonCount(); ++i) {
     ShelfAppButton* button = test_api_->GetButton(i);
-    if (!button)
+    if (!button) {
       continue;
+    }
     right = button->GetBoundsInScreen().x();
-    if (right > left)
+    if (right > left) {
       break;
+    }
   }
 
   gfx::Point test_point(left + (right - left) / 2,
@@ -1358,8 +1366,9 @@
   gfx::Rect all_area;
   for (size_t i = 0; i < test_api_->GetButtonCount(); i++) {
     ShelfAppButton* button = test_api_->GetButton(i);
-    if (!button)
+    if (!button) {
       continue;
+    }
 
     all_area.Union(button->GetMirroredBounds());
   }
@@ -1382,8 +1391,9 @@
   // The tooltip shouldn't hide if the mouse is on normal buttons.
   for (size_t i = 2; i < test_api_->GetButtonCount(); i++) {
     ShelfAppButton* button = test_api_->GetButton(i);
-    if (!button)
+    if (!button) {
       continue;
+    }
 
     EXPECT_FALSE(shelf_view_->ShouldHideTooltip(
         button->GetMirroredBounds().CenterPoint()))
@@ -2526,11 +2536,13 @@
     gfx::Rect visible_bounds = shelf_view_->GetVisibleItemsBoundsInScreen();
     gfx::Rect shelf_bounds = shelf_view_->GetBoundsInScreen();
     EXPECT_TRUE(shelf_bounds.Contains(visible_bounds));
-    for (size_t i = 0; i < test_api_->GetButtonCount(); ++i)
+    for (size_t i = 0; i < test_api_->GetButtonCount(); ++i) {
       if (ShelfAppButton* button = test_api_->GetButton(i)) {
-        if (button->GetVisible())
+        if (button->GetVisible()) {
           EXPECT_TRUE(visible_bounds.Contains(button->GetBoundsInScreen()));
+        }
       }
+    }
   }
 
  private:
@@ -3256,13 +3268,10 @@
   ExpectNotFocused(shelf_view_);
   ExpectFocused(status_area_);
 
-  // If calendar view is enabled, move the focusing ring from the date tray to
-  // the unified tray.
-  if (features::IsCalendarViewEnabled()) {
-    DoTab();
-    ExpectNotFocused(shelf_view_);
-    ExpectFocused(status_area_);
-  }
+  // Move the focusing ring from the date tray to the unified tray.
+  DoTab();
+  ExpectNotFocused(shelf_view_);
+  ExpectFocused(status_area_);
 
   // And keep going forward, now we should be cycling back to the first shelf
   // element.
@@ -3287,13 +3296,10 @@
   ExpectNotFocused(shelf_view_);
   ExpectFocused(status_area_);
 
-  // If calendar view is enabled, move the focusing ring from the unified tray
-  // to the date tray.
-  if (features::IsCalendarViewEnabled()) {
-    DoShiftTab();
-    ExpectNotFocused(shelf_view_);
-    ExpectFocused(status_area_);
-  }
+  // Move the focusing ring from the unified tray to the date tray.
+  DoShiftTab();
+  ExpectNotFocused(shelf_view_);
+  ExpectFocused(status_area_);
 
   // Advance backwards to the last element of the shelf.
   DoShiftTab();
@@ -3559,8 +3565,9 @@
 
 // Exercises the party animation.
 TEST_P(ShelfPartyTest, PartyAnimation) {
-  for (int i = 0; i < 16; ++i)
+  for (int i = 0; i < 16; ++i) {
     AddAppShortcut();
+  }
   model_->ToggleShelfParty();
   task_environment()->FastForwardBy(base::Seconds(2));
   model_->ToggleShelfParty();
diff --git a/ash/shortcut_viewer/keyboard_shortcut_viewer_metadata.cc b/ash/shortcut_viewer/keyboard_shortcut_viewer_metadata.cc
index 5f2889b..a621938 100644
--- a/ash/shortcut_viewer/keyboard_shortcut_viewer_metadata.cc
+++ b/ash/shortcut_viewer/keyboard_shortcut_viewer_metadata.cc
@@ -1416,19 +1416,17 @@
       item_list->emplace_back(toggle_all_desks_shortcut);
     }
 
-    if (ash::features::IsCalendarViewEnabled()) {
-      const ash::KeyboardShortcutItem toggle_calendar = {
-          // |categories|
-          {ShortcutCategory::kSystemAndDisplay},
-          IDS_KSV_DESCRIPTION_TOGGLE_CALENDAR,
-          {},
-          // |accelerator_ids|
-          {},
-          // |shortcut_key_codes|
-          {{ui::VKEY_COMMAND, ui::VKEY_UNKNOWN, ui::VKEY_C}}};
+    const ash::KeyboardShortcutItem toggle_calendar = {
+        // |categories|
+        {ShortcutCategory::kSystemAndDisplay},
+        IDS_KSV_DESCRIPTION_TOGGLE_CALENDAR,
+        {},
+        // |accelerator_ids|
+        {},
+        // |shortcut_key_codes|
+        {{ui::VKEY_COMMAND, ui::VKEY_UNKNOWN, ui::VKEY_C}}};
 
-      item_list->emplace_back(toggle_calendar);
-    }
+    item_list->emplace_back(toggle_calendar);
 
     for (auto& item : *item_list) {
       if (item.shortcut_key_codes.empty() && !item.accelerator_ids.empty()) {
@@ -1444,15 +1442,17 @@
             // ui::VKEY_UNKNOWN is used as a separator and will be shown as a
             // highlighted "+" sign between the bubble views and the rest of the
             // text.
-            if (!item.shortcut_key_codes.empty())
+            if (!item.shortcut_key_codes.empty()) {
               item.shortcut_key_codes.push_back(ui::VKEY_UNKNOWN);
+            }
             item.shortcut_key_codes.push_back(GetKeyCodeForModifier(modifier));
           }
         }
         // For non grouped accelerators, we need to populate the key as well.
         if (item.accelerator_ids.size() == 1) {
-          if (!item.shortcut_key_codes.empty())
+          if (!item.shortcut_key_codes.empty()) {
             item.shortcut_key_codes.push_back(ui::VKEY_UNKNOWN);
+          }
           item.shortcut_key_codes.push_back(accelerator_id.keycode);
         }
       }
diff --git a/ash/system/accessibility/accessibility_feature_pod_controller.cc b/ash/system/accessibility/accessibility_feature_pod_controller.cc
index e7c5484..b99983b16 100644
--- a/ash/system/accessibility/accessibility_feature_pod_controller.cc
+++ b/ash/system/accessibility/accessibility_feature_pod_controller.cc
@@ -56,6 +56,7 @@
       base::BindRepeating(&FeaturePodControllerBase::OnIconPressed,
                           weak_ptr_factory_.GetWeakPtr()),
       /*is_togglable=*/false);
+  feature_tile->SetID(VIEW_ID_ACCESSIBILITY_FEATURE_TILE);
   feature_tile->SetVectorIcon(kUnifiedMenuAccessibilityIcon);
   feature_tile->SetLabel(
       l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_ACCESSIBILITY));
diff --git a/ash/system/accessibility/unified_accessibility_detailed_view_controller.h b/ash/system/accessibility/unified_accessibility_detailed_view_controller.h
index dfe26dc1..1f56a5f 100644
--- a/ash/system/accessibility/unified_accessibility_detailed_view_controller.h
+++ b/ash/system/accessibility/unified_accessibility_detailed_view_controller.h
@@ -38,6 +38,10 @@
   // AccessibilityObserver:
   void OnAccessibilityStatusChanged() override;
 
+  AccessibilityDetailedView* accessibility_detailed_view_for_testing() {
+    return view_;
+  }
+
  private:
   const std::unique_ptr<DetailedViewDelegate> detailed_view_delegate_;
 
diff --git a/ash/system/eche/eche_tray.cc b/ash/system/eche/eche_tray.cc
index c81d119..b99f289 100644
--- a/ash/system/eche/eche_tray.cc
+++ b/ash/system/eche/eche_tray.cc
@@ -376,13 +376,6 @@
     case eche_app::mojom::StreamStatus::kStreamStatusInitializing:
       is_stream_started_ = false;
       break;
-    case eche_app::mojom::StreamStatus::kStreamStatusConnected:
-      is_stream_started_ = false;
-      break;
-    case eche_app::mojom::StreamStatus::kStreamStatusFailed:
-      is_stream_started_ = false;
-      PurgeAndClose();
-      break;
     case eche_app::mojom::StreamStatus::kStreamStatusUnknown:
       PA_LOG(WARNING) << "Unexpected stream status";
       is_stream_started_ = false;
diff --git a/ash/system/eche/eche_tray_unittest.cc b/ash/system/eche/eche_tray_unittest.cc
index 01878a8e..41f92bf 100644
--- a/ash/system/eche/eche_tray_unittest.cc
+++ b/ash/system/eche/eche_tray_unittest.cc
@@ -273,7 +273,7 @@
   EXPECT_FALSE(phone_hub_tray()->eche_loading_indicator()->GetAnimating());
 }
 
-TEST_F(EcheTrayTest, EcheTrayCreatesBubbleButStreamStatusChangedToStopped) {
+TEST_F(EcheTrayTest, EcheTrayCreatesBubbleButStreamStatusChanged) {
   // Verify the eche tray button is not active, and the eche tray bubble
   // is not shown initially.
   EXPECT_FALSE(eche_tray()->is_active());
@@ -308,41 +308,6 @@
   EXPECT_FALSE(eche_tray()->GetVisible());
 }
 
-TEST_F(EcheTrayTest, EcheTrayCreatesBubbleButStreamStatusChangedToFailed) {
-  // Verify the eche tray button is not active, and the eche tray bubble
-  // is not shown initially.
-  EXPECT_FALSE(eche_tray()->is_active());
-  EXPECT_FALSE(eche_tray()->get_bubble_wrapper_for_test());
-
-  // Allow us to create the bubble but it is not visible until we need this
-  // bubble to show up.
-  eche_tray()->LoadBubble(GURL("http://google.com"), CreateTestImage(),
-                          u"app 1", u"your phone");
-
-  EXPECT_FALSE(eche_tray()->is_active());
-  EXPECT_TRUE(eche_tray()->get_bubble_wrapper_for_test());
-  EXPECT_FALSE(
-      eche_tray()->get_bubble_wrapper_for_test()->bubble_view()->GetVisible());
-
-  // When the streaming status changes, the bubble should show up.
-  eche_tray()->OnStreamStatusChanged(
-      eche_app::mojom::StreamStatus::kStreamStatusStarted);
-  // Wait for the tray bubble widget to open.
-  base::RunLoop().RunUntilIdle();
-  EXPECT_TRUE(eche_tray()->is_active());
-  EXPECT_TRUE(eche_tray()->get_bubble_wrapper_for_test());
-  EXPECT_TRUE(
-      eche_tray()->get_bubble_wrapper_for_test()->bubble_view()->GetVisible());
-
-  // Change the streaming status, the bubble should be closed.
-  eche_tray()->OnStreamStatusChanged(
-      eche_app::mojom::StreamStatus::kStreamStatusFailed);
-  // Wait for the tray bubble widget to close.
-  base::RunLoop().RunUntilIdle();
-  EXPECT_FALSE(eche_tray()->is_active());
-  EXPECT_FALSE(eche_tray()->GetVisible());
-}
-
 TEST_F(EcheTrayTest, EcheTrayMinimizeButtonClicked) {
   eche_tray()->LoadBubble(GURL("http://google.com"), CreateTestImage(),
                           u"app 1", u"your phone");
diff --git a/ash/system/input_device_settings/input_device_settings_controller_impl.cc b/ash/system/input_device_settings/input_device_settings_controller_impl.cc
index 3138c91..bcb7631 100644
--- a/ash/system/input_device_settings/input_device_settings_controller_impl.cc
+++ b/ash/system/input_device_settings/input_device_settings_controller_impl.cc
@@ -401,12 +401,14 @@
   }
 }
 
-void InputDeviceSettingsControllerImpl::DispatchKeyboardDisconnected(
-    DeviceId id) {
+void InputDeviceSettingsControllerImpl::
+    DispatchKeyboardDisconnectedAndEraseFromList(DeviceId id) {
   DCHECK(base::Contains(keyboards_, id));
-  const auto& keyboard = *keyboards_.at(id);
+  auto keyboard_iter = keyboards_.find(id);
+  auto keyboard = std::move(keyboard_iter->second);
+  keyboards_.erase(keyboard_iter);
   for (auto& observer : observers_) {
-    observer.OnKeyboardDisconnected(keyboard);
+    observer.OnKeyboardDisconnected(*keyboard);
   }
 }
 
@@ -427,12 +429,14 @@
   }
 }
 
-void InputDeviceSettingsControllerImpl::DispatchTouchpadDisconnected(
-    DeviceId id) {
+void InputDeviceSettingsControllerImpl::
+    DispatchTouchpadDisconnectedAndEraseFromList(DeviceId id) {
   DCHECK(base::Contains(touchpads_, id));
-  const auto& touchpad = *touchpads_.at(id);
+  auto touchpad_iter = touchpads_.find(id);
+  auto touchpad = std::move(touchpad_iter->second);
+  touchpads_.erase(touchpad_iter);
   for (auto& observer : observers_) {
-    observer.OnTouchpadDisconnected(touchpad);
+    observer.OnTouchpadDisconnected(*touchpad);
   }
 }
 
@@ -453,11 +457,14 @@
   }
 }
 
-void InputDeviceSettingsControllerImpl::DispatchMouseDisconnected(DeviceId id) {
+void InputDeviceSettingsControllerImpl::
+    DispatchMouseDisconnectedAndEraseFromList(DeviceId id) {
   DCHECK(base::Contains(mice_, id));
-  const auto& mouse = *mice_.at(id);
+  auto mouse_iter = mice_.find(id);
+  auto mouse = std::move(mouse_iter->second);
+  mice_.erase(mouse_iter);
   for (auto& observer : observers_) {
-    observer.OnMouseDisconnected(mouse);
+    observer.OnMouseDisconnected(*mouse);
   }
 }
 
@@ -479,12 +486,14 @@
   }
 }
 
-void InputDeviceSettingsControllerImpl::DispatchPointingStickDisconnected(
-    DeviceId id) {
+void InputDeviceSettingsControllerImpl::
+    DispatchPointingStickDisconnectedAndEraseFromList(DeviceId id) {
   DCHECK(base::Contains(pointing_sticks_, id));
-  const auto& pointing_stick = *pointing_sticks_.at(id);
+  auto pointing_stick_iter = pointing_sticks_.find(id);
+  auto pointing_stick = std::move(pointing_stick_iter->second);
+  pointing_sticks_.erase(pointing_stick_iter);
   for (auto& observer : observers_) {
-    observer.OnPointingStickDisconnected(pointing_stick);
+    observer.OnPointingStickDisconnected(*pointing_stick);
   }
 }
 
@@ -513,8 +522,7 @@
   }
 
   for (const auto id : keyboard_ids_to_remove) {
-    DispatchKeyboardDisconnected(id);
-    keyboards_.erase(id);
+    DispatchKeyboardDisconnectedAndEraseFromList(id);
   }
 }
 
@@ -532,8 +540,7 @@
   }
 
   for (const auto id : touchpad_ids_to_remove) {
-    DispatchTouchpadDisconnected(id);
-    touchpads_.erase(id);
+    DispatchTouchpadDisconnectedAndEraseFromList(id);
   }
 }
 
@@ -551,8 +558,7 @@
   }
 
   for (const auto id : mouse_ids_to_remove) {
-    DispatchMouseDisconnected(id);
-    mice_.erase(id);
+    DispatchMouseDisconnectedAndEraseFromList(id);
   }
 }
 
@@ -571,8 +577,7 @@
   }
 
   for (const auto id : pointing_stick_ids_to_remove) {
-    DispatchPointingStickDisconnected(id);
-    pointing_sticks_.erase(id);
+    DispatchPointingStickDisconnectedAndEraseFromList(id);
   }
 }
 
diff --git a/ash/system/input_device_settings/input_device_settings_controller_impl.h b/ash/system/input_device_settings/input_device_settings_controller_impl.h
index 80007008..5aadb39c 100644
--- a/ash/system/input_device_settings/input_device_settings_controller_impl.h
+++ b/ash/system/input_device_settings/input_device_settings_controller_impl.h
@@ -85,19 +85,19 @@
   void Init();
 
   void DispatchKeyboardConnected(DeviceId id);
-  void DispatchKeyboardDisconnected(DeviceId id);
+  void DispatchKeyboardDisconnectedAndEraseFromList(DeviceId id);
   void DispatchKeyboardSettingsChanged(DeviceId id);
 
   void DispatchTouchpadConnected(DeviceId id);
-  void DispatchTouchpadDisconnected(DeviceId id);
+  void DispatchTouchpadDisconnectedAndEraseFromList(DeviceId id);
   void DispatchTouchpadSettingsChanged(DeviceId id);
 
   void DispatchMouseConnected(DeviceId id);
-  void DispatchMouseDisconnected(DeviceId id);
+  void DispatchMouseDisconnectedAndEraseFromList(DeviceId id);
   void DispatchMouseSettingsChanged(DeviceId id);
 
   void DispatchPointingStickConnected(DeviceId id);
-  void DispatchPointingStickDisconnected(DeviceId id);
+  void DispatchPointingStickDisconnectedAndEraseFromList(DeviceId id);
   void DispatchPointingStickSettingsChanged(DeviceId id);
 
   base::ObserverList<InputDeviceSettingsController::Observer> observers_;
diff --git a/ash/system/power/power_button_controller.h b/ash/system/power/power_button_controller.h
index bf46480..b0be0cec 100644
--- a/ash/system/power/power_button_controller.h
+++ b/ash/system/power/power_button_controller.h
@@ -156,7 +156,7 @@
   void OnTabletModeStarted() override;
   void OnTabletModeEnded() override;
 
-  // Used by the SecurityCurtainController to notify when power button is
+  // Used by the `ash::curtain::Session` to notify when power button is
   // enabled/disabled.
   void OnSecurityCurtainEnabled();
   void OnSecurityCurtainDisabled();
diff --git a/ash/system/status_area_widget.cc b/ash/system/status_area_widget.cc
index 7e433ae7..a1379ee 100644
--- a/ash/system/status_area_widget.cc
+++ b/ash/system/status_area_widget.cc
@@ -74,8 +74,9 @@
 
 StatusAreaWidget::ScopedTrayBubbleCounter::~ScopedTrayBubbleCounter() {
   // ScopedTrayBubbleCounter may live longer than StatusAreaWidget.
-  if (!status_area_widget_)
+  if (!status_area_widget_) {
     return;
+  }
 
   --status_area_widget_->tray_bubble_count_;
   if (status_area_widget_->tray_bubble_count_ == 0) {
@@ -163,10 +164,8 @@
 
   auto unified_system_tray = std::make_unique<UnifiedSystemTray>(shelf_);
   unified_system_tray_ = unified_system_tray.get();
-  if (features::IsCalendarViewEnabled()) {
-    date_tray_ =
-        AddTrayButton(std::make_unique<DateTray>(shelf_, unified_system_tray_));
-  }
+  date_tray_ =
+      AddTrayButton(std::make_unique<DateTray>(shelf_, unified_system_tray_));
   AddTrayButton(std::move(unified_system_tray));
 
   overview_button_tray_ =
@@ -208,8 +207,9 @@
   // If QsRevamp flag is enabled, `notification_center_tray_` may be null in
   // some unittests. During the test environment tear-down, removing the
   // observer will lead to a crash.
-  if (features::IsQsRevampEnabled() && notification_center_tray_)
+  if (features::IsQsRevampEnabled() && notification_center_tray_) {
     notification_center_tray_->RemoveObserver(this);
+  }
 
   // If QsRevamp flag is enabled, reset `animation_controller_` before
   // destroying `notification_center_tray_` so that we don't run into a UaF.
@@ -225,22 +225,23 @@
 }
 
 void StatusAreaWidget::UpdateAfterLoginStatusChange(LoginStatus login_status) {
-  if (login_status_ == login_status)
+  if (login_status_ == login_status) {
     return;
+  }
   login_status_ = login_status;
 
-  for (TrayBackgroundView* tray_button : tray_buttons_)
+  for (TrayBackgroundView* tray_button : tray_buttons_) {
     tray_button->UpdateAfterLoginStatusChange();
+  }
 }
 
 void StatusAreaWidget::SetSystemTrayVisibility(bool visible) {
   unified_system_tray_->SetVisiblePreferred(visible);
+  date_tray_->SetVisiblePreferred(visible);
 
-  if (features::IsCalendarViewEnabled())
-    date_tray_->SetVisiblePreferred(visible);
-
-  if (features::IsQsRevampEnabled())
+  if (features::IsQsRevampEnabled()) {
     notification_center_tray_->OnSystemTrayVisibilityChanged(visible);
+  }
 
   if (visible) {
     Show();
@@ -252,8 +253,9 @@
 
 void StatusAreaWidget::OnSessionStateChanged(
     session_manager::SessionState state) {
-  for (TrayBackgroundView* tray_button : tray_buttons_)
+  for (TrayBackgroundView* tray_button : tray_buttons_) {
     tray_button->UpdateBackground();
+  }
 }
 
 void StatusAreaWidget::UpdateCollapseState() {
@@ -310,8 +312,9 @@
       case TrayBackgroundViewCatalogName::kVirtualKeyboardStatusArea:
       case TrayBackgroundViewCatalogName::kWmMode:
       case TrayBackgroundViewCatalogName::kVideoConferenceTray:
-        if (!tray_button->GetVisible())
+        if (!tray_button->GetVisible()) {
           continue;
+        }
         visible_pod_count += 1;
         break;
     }
@@ -327,8 +330,9 @@
 }
 
 void StatusAreaWidget::CalculateTargetBounds() {
-  for (TrayBackgroundView* tray_button : tray_buttons_)
+  for (TrayBackgroundView* tray_button : tray_buttons_) {
     tray_button->CalculateTargetBounds();
+  }
   status_area_widget_delegate_->CalculateTargetBounds();
 
   gfx::Size status_size(status_area_widget_delegate_->GetTargetBounds().size());
@@ -336,18 +340,20 @@
   const gfx::Point shelf_origin =
       shelf_->shelf_widget()->GetTargetBounds().origin();
 
-  if (shelf_->IsHorizontalAlignment())
+  if (shelf_->IsHorizontalAlignment()) {
     status_size.set_height(shelf_size.height());
-  else
+  } else {
     status_size.set_width(shelf_size.width());
+  }
 
   gfx::Point status_origin = shelf_->SelectValueForShelfAlignment(
       gfx::Point(0, 0),
       gfx::Point(shelf_size.width() - status_size.width(),
                  shelf_size.height() - status_size.height()),
       gfx::Point(0, shelf_size.height() - status_size.height()));
-  if (shelf_->IsHorizontalAlignment() && !base::i18n::IsRTL())
+  if (shelf_->IsHorizontalAlignment() && !base::i18n::IsRTL()) {
     status_origin.set_x(shelf_size.width() - status_size.width());
+  }
   status_origin.Offset(shelf_origin.x(), shelf_origin.y());
   target_bounds_ = gfx::Rect(status_origin, status_size);
 }
@@ -358,24 +364,28 @@
 
 void StatusAreaWidget::UpdateLayout(bool animate) {
   const LayoutInputs new_layout_inputs = GetLayoutInputs();
-  if (layout_inputs_ == new_layout_inputs)
+  if (layout_inputs_ == new_layout_inputs) {
     return;
+  }
 
-  if (!new_layout_inputs.should_animate)
+  if (!new_layout_inputs.should_animate) {
     animate = false;
+  }
 
-  for (TrayBackgroundView* tray_button : tray_buttons_)
+  for (TrayBackgroundView* tray_button : tray_buttons_) {
     tray_button->UpdateLayout();
+  }
   status_area_widget_delegate_->UpdateLayout(animate);
 
   // Having a window which is visible but does not have an opacity is an
   // illegal state.
   ui::Layer* layer = GetNativeView()->layer();
   layer->SetOpacity(new_layout_inputs.opacity);
-  if (new_layout_inputs.opacity)
+  if (new_layout_inputs.opacity) {
     ShowInactive();
-  else
+  } else {
     Hide();
+  }
 
   ui::ScopedLayerAnimationSettings animation_setter(layer->GetAnimator());
   animation_setter.SetTransitionDuration(
@@ -389,10 +399,11 @@
 }
 
 void StatusAreaWidget::UpdateTargetBoundsForGesture(int shelf_position) {
-  if (shelf_->IsHorizontalAlignment())
+  if (shelf_->IsHorizontalAlignment()) {
     target_bounds_.set_y(shelf_position);
-  else
+  } else {
     target_bounds_.set_x(shelf_position);
+  }
 }
 
 void StatusAreaWidget::HandleLocaleChange() {
@@ -416,8 +427,9 @@
 }
 
 void StatusAreaWidget::CalculateButtonVisibilityForCollapsedState() {
-  if (!initialized_)
+  if (!initialized_) {
     return;
+  }
 
   DCHECK(collapse_state_ == CollapseState::COLLAPSED);
 
@@ -437,8 +449,9 @@
 
   // First, reset all tray button to be hidden.
   overflow_button_tray_->ResetStateToCollapsed();
-  for (TrayBackgroundView* tray_button : tray_buttons_)
+  for (TrayBackgroundView* tray_button : tray_buttons_) {
     tray_button->set_show_when_collapsed(false);
+  }
 
   // Iterate backwards making tray buttons visible until |available_width| is
   // exceeded.
@@ -447,11 +460,13 @@
   int used_width = 0;
   for (TrayBackgroundView* tray : base::Reversed(tray_buttons_)) {
     // Skip non-enabled tray buttons.
-    if (!tray->visible_preferred())
+    if (!tray->visible_preferred()) {
       continue;
+    }
     // Skip |stop_recording_button_tray_| since it's always visible.
-    if (tray == stop_recording_button_tray_)
+    if (tray == stop_recording_button_tray_) {
       continue;
+    }
 
     // Show overflow button once available width is exceeded.
     int tray_width = tray->tray_container()->GetPreferredSize().width();
@@ -461,8 +476,10 @@
       // Maybe remove the last tray button to make room for the overflow tray.
       int overflow_button_width =
           overflow_button_tray_->GetPreferredSize().width();
-      if (previous_tray && used_width + overflow_button_width > available_width)
+      if (previous_tray &&
+          used_width + overflow_button_width > available_width) {
         previous_tray->set_show_when_collapsed(false);
+      }
       break;
     }
 
@@ -472,13 +489,15 @@
   }
 
   // Skip |stop_recording_button_tray_| so it's always visible.
-  if (stop_recording_button_tray_->visible_preferred())
+  if (stop_recording_button_tray_->visible_preferred()) {
     stop_recording_button_tray_->set_show_when_collapsed(true);
+  }
 
   overflow_button_tray_->SetVisiblePreferred(show_overflow_button);
   overflow_button_tray_->UpdateAfterStatusAreaCollapseChange();
-  for (TrayBackgroundView* tray_button : tray_buttons_)
+  for (TrayBackgroundView* tray_button : tray_buttons_) {
     tray_button->UpdateAfterStatusAreaCollapseChange();
+  }
 }
 
 void StatusAreaWidget::EnsureTrayOrder() {
@@ -494,12 +513,14 @@
     const {
   // The status area is only collapsible in tablet mode. Otherwise, we just show
   // all trays.
-  if (!Shell::Get()->tablet_mode_controller())
+  if (!Shell::Get()->tablet_mode_controller()) {
     return CollapseState::NOT_COLLAPSIBLE;
+  }
 
   // An update may occur during initialization of the shelf, so just skip it.
-  if (!initialized_)
+  if (!initialized_) {
     return CollapseState::NOT_COLLAPSIBLE;
+  }
 
   bool is_collapsible =
       Shell::Get()->tablet_mode_controller()->InTabletMode() &&
@@ -529,15 +550,18 @@
     for (TrayBackgroundView* tray : base::Reversed(tray_buttons_)) {
       // If we reach the final overflow tray button, then all the tray buttons
       // fit and there is no need for a collapse state.
-      if (tray == overflow_button_tray_)
+      if (tray == overflow_button_tray_) {
         return CollapseState::NOT_COLLAPSIBLE;
+      }
 
       // Skip non-enabled tray buttons.
-      if (!tray->visible_preferred())
+      if (!tray->visible_preferred()) {
         continue;
+      }
       int tray_width = tray->tray_container()->GetPreferredSize().width();
-      if (used_width + tray_width > available_width)
+      if (used_width + tray_width > available_width) {
         break;
+      }
 
       used_width += tray_width;
     }
@@ -549,15 +573,17 @@
   // Use the target visibility of the layer instead of the visibility of the
   // view because the view is still visible when fading away, but we do not want
   // to anchor to this element in that case.
-  if (overview_button_tray_->layer()->GetTargetVisibility())
+  if (overview_button_tray_->layer()->GetTargetVisibility()) {
     return overview_button_tray_;
+  }
 
   return unified_system_tray_;
 }
 
 gfx::Rect StatusAreaWidget::GetMediaTrayAnchorRect() const {
-  if (!media_tray_)
+  if (!media_tray_) {
     return gfx::Rect();
+  }
 
   // Calculate anchor rect of media tray bubble. This is required because the
   // bubble can be visible while the tray button is hidden. (e.g. when user
@@ -572,8 +598,9 @@
       continue;
     }
 
-    if (!found_media_tray || !tray_button->GetVisible())
+    if (!found_media_tray || !tray_button->GetVisible()) {
       continue;
+    }
 
     offset += shelf_->IsHorizontalAlignment() ? tray_button->width()
                                               : tray_button->height();
@@ -610,30 +637,35 @@
 
 bool StatusAreaWidget::ShouldShowShelf() const {
   // If it has main bubble, return true.
-  if (unified_system_tray_->IsBubbleShown())
+  if (unified_system_tray_->IsBubbleShown()) {
     return true;
+  }
 
   // If any tray is showing a context menu, the shelf should be visible.
   for (TrayBackgroundView* tray_button : tray_buttons_) {
-    if (tray_button->IsShowingMenu())
+    if (tray_button->IsShowingMenu()) {
       return true;
+    }
   }
 
   // If it has a slider bubble, return false.
-  if (unified_system_tray_->IsSliderBubbleShown())
+  if (unified_system_tray_->IsSliderBubbleShown()) {
     return false;
+  }
 
   // Some TrayBackgroundViews' cache their bubble, the shelf should only be
   // forced to show if the bubble is visible, and we should not show the shelf
   // for cached, hidden bubbles.
   if (tray_bubble_count_ > 0) {
     for (TrayBackgroundView* tray_button : tray_buttons_) {
-      if (!tray_button->GetBubbleView())
+      if (!tray_button->GetBubbleView()) {
         continue;
+      }
 
       // Any tray bubble is showing, show shelf.
-      if (tray_button->GetBubbleView()->GetVisible())
+      if (tray_button->GetBubbleView()->GetVisible()) {
         return true;
+      }
 
       // Tray bubble view is not null and not visible, tray bubble is cached
       // for hidden case. If the tray caches the view for hidden, we should
@@ -654,22 +686,26 @@
 }
 
 void StatusAreaWidget::SchedulePaint() {
-  for (TrayBackgroundView* tray_button : tray_buttons_)
+  for (TrayBackgroundView* tray_button : tray_buttons_) {
     tray_button->SchedulePaint();
+  }
 }
 
 bool StatusAreaWidget::OnNativeWidgetActivationChanged(bool active) {
-  if (!Widget::OnNativeWidgetActivationChanged(active))
+  if (!Widget::OnNativeWidgetActivationChanged(active)) {
     return false;
-  if (active)
+  }
+  if (active) {
     status_area_widget_delegate_->SetPaneFocusAndFocusDefault();
+  }
   return true;
 }
 
 void StatusAreaWidget::OnViewVisibilityChanged(views::View* observed_view,
                                                views::View* starting_view) {
-  if (observed_view != notification_center_tray_)
+  if (observed_view != notification_center_tray_) {
     return;
+  }
 
   UpdateDateTrayRoundedCorners();
 }
@@ -706,8 +742,9 @@
 
 void StatusAreaWidget::OnScrollEvent(ui::ScrollEvent* event) {
   shelf_->ProcessScrollEvent(event);
-  if (!event->handled())
+  if (!event->handled()) {
     views::Widget::OnScrollEvent(event);
+  }
 }
 
 template <typename TrayButtonT>
@@ -725,8 +762,9 @@
   DCHECK(tray_buttons_.size() <
          std::numeric_limits<decltype(child_visibility_bitmask)>::digits);
   for (unsigned int i = 0; i < tray_buttons_.size(); ++i) {
-    if (tray_buttons_[i]->GetVisible())
+    if (tray_buttons_[i]->GetVisible()) {
       child_visibility_bitmask |= 1 << i;
+    }
   }
 
   bool should_animate = true;
@@ -750,8 +788,9 @@
 }
 
 void StatusAreaWidget::UpdateDateTrayRoundedCorners() {
-  if (!features::IsQsRevampEnabled() || !date_tray_)
+  if (!features::IsQsRevampEnabled() || !date_tray_) {
     return;
+  }
 
   date_tray_->SetRoundedCornerBehavior(
       notification_center_tray_->GetVisible()
@@ -763,16 +802,15 @@
   const int shelf_width =
       shelf_->shelf_widget()->GetClientAreaBoundsInScreen().width();
 
-  if (!force_collapsible)
+  if (!force_collapsible) {
     return shelf_width / 2 - kStatusAreaLeftPaddingForOverflow;
+  }
 
   int available_width = kStatusAreaForceCollapseAvailableWidth;
-  // If calendar view is enabled, add the date tray width to the collapse
-  // available width.
-  if (features::IsCalendarViewEnabled()) {
-    DCHECK(date_tray_);
-    available_width += date_tray_->tray_container()->GetPreferredSize().width();
-  }
+  // Add the date tray width to the collapse available width.
+  DCHECK(date_tray_);
+  available_width += date_tray_->tray_container()->GetPreferredSize().width();
+
   return available_width;
 }
 
diff --git a/ash/system/time/calendar_view_unittest.cc b/ash/system/time/calendar_view_unittest.cc
index b586b79..ad5beac 100644
--- a/ash/system/time/calendar_view_unittest.cc
+++ b/ash/system/time/calendar_view_unittest.cc
@@ -95,8 +95,9 @@
     for (const auto* child_view : month->children()) {
       auto* current_date_cell =
           static_cast<const views::LabelButton*>(child_view);
-      if (day != current_date_cell->GetText())
+      if (day != current_date_cell->GetText()) {
         continue;
+      }
 
       date_cell = current_date_cell;
       break;
@@ -667,14 +668,17 @@
   const int scroll_up_count = 10;
   const int scroll_down_count = scroll_up_count - 1;
   // Scroll up.
-  for (int i = 0; i < scroll_up_count; ++i)
+  for (int i = 0; i < scroll_up_count; ++i) {
     ScrollUpOneMonth();
+  }
   // Return to today.
-  for (int i = 0; i < scroll_up_count; ++i)
+  for (int i = 0; i < scroll_up_count; ++i) {
     ScrollDownOneMonth();
+  }
   // Scroll down from today.
-  for (int i = 0; i < scroll_down_count; ++i)
+  for (int i = 0; i < scroll_down_count; ++i) {
     ScrollDownOneMonth();
+  }
 
   DestroyCalendarViewWidget();
 
@@ -711,8 +715,9 @@
                          views::View* focused_now) override {}
   void OnDidChangeFocus(views::View* focused_before,
                         views::View* focused_now) override {
-    if (found_)
+    if (found_) {
       return;
+    }
 
     steps_taken_++;
     found_ = static_cast<const views::LabelButton*>(focused_now)->GetText() ==
@@ -1389,8 +1394,9 @@
     for (const auto* child_view : month->children()) {
       auto* current_date_cell =
           static_cast<const views::LabelButton*>(child_view);
-      if (day != current_date_cell->GetText())
+      if (day != current_date_cell->GetText()) {
         continue;
+      }
 
       date_cell = current_date_cell;
       break;
@@ -2074,8 +2080,8 @@
 
   void SetUp() override {
     scoped_feature_list_ = std::make_unique<base::test::ScopedFeatureList>();
-    scoped_feature_list_->InitWithFeatures(
-        {features::kCalendarView, features::kNotificationsRefresh}, {});
+    scoped_feature_list_->InitWithFeatures({features::kNotificationsRefresh},
+                                           {});
     AshTestBase::SetUp();
   }
 
@@ -2123,8 +2129,9 @@
     while ((current_focusable_view =
                 message_center_focus_manager()->GetNextFocusableView(
                     current_focusable_view, widget, /*reverse=*/false,
-                    /*dont_loop=*/true)))
+                    /*dont_loop=*/true))) {
       count++;
+    }
     return count;
   }
 
@@ -2166,8 +2173,9 @@
   PressTab();
 
   // Keep tabbing until exiting the message center.
-  for (int i = 0; i < number_of_focusable_views_in_message_center; i++)
+  for (int i = 0; i < number_of_focusable_views_in_message_center; i++) {
     PressTab();
+  }
 
   // The "back to today" `PillButton` is the first focused view.
   EXPECT_STREQ(calendar_focus_manager()->GetFocusedView()->GetClassName(),
@@ -2177,8 +2185,9 @@
   PressShiftTab();
 
   // Keep tabbing backwards until exiting the message center.
-  for (int i = 0; i < number_of_focusable_views_in_message_center; i++)
+  for (int i = 0; i < number_of_focusable_views_in_message_center; i++) {
     PressShiftTab();
+  }
 
   // Today's date cell should be focused now.
   EXPECT_EQ(current_date_cell_view, calendar_focus_manager()->GetFocusedView());
@@ -2195,8 +2204,7 @@
 
   void SetUp() override {
     scoped_feature_list_ = std::make_unique<base::test::ScopedFeatureList>();
-    scoped_feature_list_->InitWithFeatures(
-        {features::kCalendarView, features::kCalendarJelly}, {});
+    scoped_feature_list_->InitWithFeatures({features::kCalendarJelly}, {});
     CalendarViewTest::SetUp();
   }
 
diff --git a/ash/system/time/time_tray_item_view_unittest.cc b/ash/system/time/time_tray_item_view_unittest.cc
index af92cf1..12ff54c 100644
--- a/ash/system/time/time_tray_item_view_unittest.cc
+++ b/ash/system/time/time_tray_item_view_unittest.cc
@@ -8,12 +8,10 @@
 #include "ash/shelf/shelf.h"
 #include "ash/system/time/time_view.h"
 #include "ash/test/ash_test_base.h"
-#include "base/test/scoped_feature_list.h"
 
 namespace ash {
 
-class TimeTrayItemViewTest : public AshTestBase,
-                             public testing::WithParamInterface<bool> {
+class TimeTrayItemViewTest : public AshTestBase {
  public:
   TimeTrayItemViewTest() = default;
   ~TimeTrayItemViewTest() override = default;
@@ -21,9 +19,6 @@
   // AshTestBase:
   void SetUp() override {
     AshTestBase::SetUp();
-    scoped_feature_list_.InitWithFeatureState(features::kCalendarView,
-                                              IsCalendarViewEnabled());
-
     time_tray_item_view_ = std::make_unique<TimeTrayItemView>(
         GetPrimaryShelf(), TimeView::Type::kTime);
   }
@@ -33,8 +28,6 @@
     AshTestBase::TearDown();
   }
 
-  bool IsCalendarViewEnabled() { return GetParam(); }
-
   // Returns true if the time view is in horizontal layout, false if it is in
   // vertical layout.
   bool IsTimeViewInHorizontalLayout() {
@@ -44,15 +37,10 @@
   }
 
  protected:
-  base::test::ScopedFeatureList scoped_feature_list_;
   std::unique_ptr<TimeTrayItemView> time_tray_item_view_;
 };
 
-INSTANTIATE_TEST_SUITE_P(All,
-                         TimeTrayItemViewTest,
-                         testing::Bool() /* IsCalendarViewEnabled() */);
-
-TEST_P(TimeTrayItemViewTest, ShelfAlignment) {
+TEST_F(TimeTrayItemViewTest, ShelfAlignment) {
   // The tray should show time horizontal view when the shelf is bottom.
   GetPrimaryShelf()->SetAlignment(ShelfAlignment::kBottom);
   time_tray_item_view_->UpdateAlignmentForShelf(GetPrimaryShelf());
diff --git a/ash/system/tray/tray_detailed_view.h b/ash/system/tray/tray_detailed_view.h
index 7567a776..80b8253 100644
--- a/ash/system/tray/tray_detailed_view.h
+++ b/ash/system/tray/tray_detailed_view.h
@@ -50,6 +50,8 @@
   // Setter for `progress_bar_` accessibility label.
   void OverrideProgressBarAccessibleName(const std::u16string& name);
 
+  views::ScrollView* scroll_view_for_testing() { return scroller_; }
+
  protected:
   // views::View:
   void Layout() override;
diff --git a/ash/system/tray/tray_event_filter.cc b/ash/system/tray/tray_event_filter.cc
index b7d85106..6ef02f2 100644
--- a/ash/system/tray/tray_event_filter.cc
+++ b/ash/system/tray/tray_event_filter.cc
@@ -35,35 +35,41 @@
 void TrayEventFilter::AddBubble(TrayBubbleBase* bubble) {
   bool was_empty = bubbles_.empty();
   bubbles_.insert(bubble);
-  if (was_empty && !bubbles_.empty())
+  if (was_empty && !bubbles_.empty()) {
     Shell::Get()->AddPreTargetHandler(this);
+  }
 }
 
 void TrayEventFilter::RemoveBubble(TrayBubbleBase* bubble) {
   bubbles_.erase(bubble);
-  if (bubbles_.empty())
+  if (bubbles_.empty()) {
     Shell::Get()->RemovePreTargetHandler(this);
+  }
 }
 
 void TrayEventFilter::OnMouseEvent(ui::MouseEvent* event) {
-  if (event->type() == ui::ET_MOUSE_PRESSED)
+  if (event->type() == ui::ET_MOUSE_PRESSED) {
     ProcessPressedEvent(*event);
+  }
 }
 
 void TrayEventFilter::OnTouchEvent(ui::TouchEvent* event) {
-  if (event->type() == ui::ET_TOUCH_PRESSED)
+  if (event->type() == ui::ET_TOUCH_PRESSED) {
     ProcessPressedEvent(*event);
+  }
 }
 
 void TrayEventFilter::OnGestureEvent(ui::GestureEvent* event) {
-  if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN)
+  if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN) {
     ProcessPressedEvent(*event);
+  }
 }
 
 void TrayEventFilter::ProcessPressedEvent(const ui::LocatedEvent& event) {
   // Users in a capture session may be trying to capture tray bubble(s).
-  if (capture_mode_util::IsCaptureModeActive())
+  if (capture_mode_util::IsCaptureModeActive()) {
     return;
+  }
 
   // The hit target window for the virtual keyboard isn't the same as its
   // views::Widget.
@@ -78,8 +84,9 @@
     const int container_id = container->GetId();
     // Don't process events that occurred inside an embedded menu, for example
     // the right-click menu in a popup notification.
-    if (container_id == kShellWindowId_MenuContainer)
+    if (container_id == kShellWindowId_MenuContainer) {
       return;
+    }
     // Don't process events that occurred inside a popup notification
     // from message center.
     if (container_id == kShellWindowId_ShelfContainer &&
@@ -89,8 +96,9 @@
       return;
     }
     // Don't process events that occurred inside a virtual keyboard.
-    if (container_id == kShellWindowId_VirtualKeyboardContainer)
+    if (container_id == kShellWindowId_VirtualKeyboardContainer) {
       return;
+    }
   }
 
   std::set<TrayBackgroundView*> trays;
@@ -101,8 +109,9 @@
                      : event.root_location();
   for (const TrayBubbleBase* bubble : bubbles_) {
     const views::Widget* bubble_widget = bubble->GetBubbleWidget();
-    if (!bubble_widget)
+    if (!bubble_widget) {
       continue;
+    }
 
     gfx::Rect bounds = bubble_widget->GetWindowBoundsInScreen();
     bounds.Inset(bubble->GetBubbleView()->GetBorderInsets());
@@ -133,8 +142,7 @@
 
       // When Quick Settings bubble is opened and the date tray is clicked, the
       // bubble should not be closed since it will transition to show calendar.
-      if (features::IsCalendarViewEnabled() &&
-          status_area->date_tray()->GetBoundsInScreen().Contains(
+      if (status_area->date_tray()->GetBoundsInScreen().Contains(
               screen_location)) {
         continue;
       }
@@ -150,24 +158,27 @@
       }
     }
 
-    if (bounds.Contains(screen_location))
+    if (bounds.Contains(screen_location)) {
       continue;
+    }
     if (bubble->GetTray()) {
       // Maybe close the parent tray if the user drags on it. Otherwise, let the
       // tray logic handle the event and determine show/hide behavior if the
       // user clicks on the parent tray.
       bounds = bubble->GetTray()->GetBoundsInScreen();
       if (bubble->GetTray()->GetVisible() && bounds.Contains(screen_location) &&
-          event.type() != ui::ET_GESTURE_SCROLL_BEGIN)
+          event.type() != ui::ET_GESTURE_SCROLL_BEGIN) {
         continue;
+      }
     }
 
     trays.insert(bubble->GetTray());
   }
 
   // Close all bubbles other than the one that the user clicked on.
-  for (TrayBackgroundView* tray_background_view : trays)
+  for (TrayBackgroundView* tray_background_view : trays) {
     tray_background_view->ClickedOutsideBubble();
+  }
 }
 
 }  // namespace ash
diff --git a/ash/system/unified/system_tray_test_api.cc b/ash/system/unified/system_tray_test_api.cc
index 91f202e6..198dc200 100644
--- a/ash/system/unified/system_tray_test_api.cc
+++ b/ash/system/unified/system_tray_test_api.cc
@@ -10,9 +10,12 @@
 #include "ash/root_window_controller.h"
 #include "ash/shell.h"
 #include "ash/system/accessibility/select_to_speak/select_to_speak_tray.h"
+#include "ash/system/accessibility/unified_accessibility_detailed_view_controller.h"
 #include "ash/system/status_area_widget.h"
 #include "ash/system/time/time_tray_item_view.h"
 #include "ash/system/time/time_view.h"
+#include "ash/system/tray/tray_detailed_view.h"
+#include "ash/system/tray/tray_toggle_button.h"
 #include "ash/system/unified/power_button.h"
 #include "ash/system/unified/quick_settings_footer.h"
 #include "ash/system/unified/unified_system_tray.h"
@@ -78,6 +81,14 @@
   GetTray()->bubble_->controller_->ShowNetworkDetailedView(true /* force */);
 }
 
+AccessibilityDetailedView* SystemTrayTestApi::GetAccessibilityDetailedView() {
+  auto* unified_system_tray_controller = GetTray()->bubble_->controller_.get();
+  DCHECK(unified_system_tray_controller->IsDetailedViewShown());
+  return static_cast<UnifiedAccessibilityDetailedViewController*>(
+             unified_system_tray_controller->detailed_view_controller())
+      ->accessibility_detailed_view_for_testing();
+}
+
 bool SystemTrayTestApi::IsBubbleViewVisible(int view_id, bool open_tray) {
   if (open_tray)
     GetTray()->ShowBubble();
@@ -85,16 +96,16 @@
   return view && view->GetVisible();
 }
 
-void SystemTrayTestApi::ScrollToShowView(int view_id) {
-  views::View* view = GetBubbleView(view_id);
+bool SystemTrayTestApi::IsToggleOn(int view_id) {
+  auto* view = static_cast<TrayToggleButton*>(GetBubbleView(view_id));
   DCHECK(view);
+  return view->GetIsOn();
+}
 
-  views::View* contents = view->parent();
-  DCHECK(contents);
-
-  views::ScrollView* scroll_view =
-      views::ScrollView::GetScrollViewForContents(contents);
-  DCHECK(scroll_view);
+void SystemTrayTestApi::ScrollToShowView(views::ScrollView* scroll_view,
+                                         int view_id) {
+  views::View* view = GetBubbleView(view_id);
+  DCHECK(view && scroll_view->Contains(view));
 
   gfx::Point view_center = view->GetBoundsInScreen().CenterPoint();
   gfx::Rect scroll_bounds = scroll_view->GetBoundsInScreen();
diff --git a/ash/system/unified/unified_system_info_view.cc b/ash/system/unified/unified_system_info_view.cc
index 291bc79c..62758dc 100644
--- a/ash/system/unified/unified_system_info_view.cc
+++ b/ash/system/unified/unified_system_info_view.cc
@@ -118,9 +118,6 @@
   Update();
 
   Shell::Get()->system_tray_model()->clock()->AddObserver(this);
-  if (!features::IsCalendarViewEnabled())
-    SetEnabled(
-        Shell::Get()->system_tray_model()->clock()->IsSettingsAvailable());
   SetInstallFocusRingOnFocus(true);
   views::FocusRing::Get(this)->SetColorId(ui::kColorAshFocusRing);
   views::InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::OFF);
@@ -141,7 +138,7 @@
   quick_settings_metrics_util::RecordQsButtonActivated(
       QsButtonCatalogName::kDateViewButton);
 
-  if (features::IsCalendarViewEnabled() && controller_->IsExpanded()) {
+  if (controller_->IsExpanded()) {
     controller_->ShowCalendarView(
         calendar_metrics::CalendarViewShowSource::kDateView,
         calendar_metrics::GetEventType(event));
@@ -155,13 +152,10 @@
   base::Time now = base::Time::Now();
   label_->SetText(l10n_util::GetStringFUTF16(
       IDS_ASH_STATUS_TRAY_DATE, FormatDayOfWeek(now), FormatDate(now)));
-  if (features::IsCalendarViewEnabled()) {
-    SetAccessibleName(l10n_util::GetStringFUTF16(
-        IDS_ASH_CALENDAR_ENTRY_ACCESSIBLE_DESCRIPTION,
-        TimeFormatFriendlyDateAndTime(now)));
-  } else {
-    SetAccessibleName(TimeFormatFriendlyDateAndTime(now));
-  }
+
+  SetAccessibleName(
+      l10n_util::GetStringFUTF16(IDS_ASH_CALENDAR_ENTRY_ACCESSIBLE_DESCRIPTION,
+                                 TimeFormatFriendlyDateAndTime(now)));
 }
 
 gfx::Insets DateView::GetInsets() const {
@@ -202,8 +196,9 @@
       separator_view_->SetPreferredLength(kUnifiedSystemInfoHeight);
 
       const bool use_smart_charging_ui = UseSmartChargingUI();
-      if (use_smart_charging_ui)
+      if (use_smart_charging_ui) {
         AddChildView(std::make_unique<BatteryIconView>(controller));
+      }
       AddChildView(std::make_unique<BatteryLabelView>(controller,
                                                       use_smart_charging_ui));
     }
diff --git a/ash/system/unified/unified_system_tray.cc b/ash/system/unified/unified_system_tray.cc
index a6c3f6d1..4edac3a 100644
--- a/ash/system/unified/unified_system_tray.cc
+++ b/ash/system/unified/unified_system_tray.cc
@@ -175,10 +175,9 @@
 void UnifiedSystemTray::UiDelegate::HideMessageCenter() {}
 
 UnifiedSystemTray::UnifiedSystemTray(Shelf* shelf)
-    : TrayBackgroundView(
-          shelf,
-          TrayBackgroundViewCatalogName::kUnifiedSystem,
-          (features::IsCalendarViewEnabled() ? kEndRounded : kAllRounded)),
+    : TrayBackgroundView(shelf,
+                         TrayBackgroundViewCatalogName::kUnifiedSystem,
+                         kEndRounded),
       ui_delegate_(std::make_unique<UiDelegate>(this)),
       model_(base::MakeRefCounted<UnifiedSystemTrayModel>(shelf)),
       slider_bubble_controller_(
diff --git a/ash/system/unified/unified_system_tray_controller.cc b/ash/system/unified/unified_system_tray_controller.cc
index 6acc495..f2d9db7 100644
--- a/ash/system/unified/unified_system_tray_controller.cc
+++ b/ash/system/unified/unified_system_tray_controller.cc
@@ -509,10 +509,6 @@
 void UnifiedSystemTrayController::ShowCalendarView(
     calendar_metrics::CalendarViewShowSource show_source,
     calendar_metrics::CalendarEventSource event_source) {
-  if (!features::IsCalendarViewEnabled()) {
-    return;
-  }
-
   calendar_metrics::RecordCalendarShowMetrics(show_source, event_source);
   ShowDetailedView(std::make_unique<UnifiedCalendarViewController>(this));
 
diff --git a/ash/webui/BUILD.gn b/ash/webui/BUILD.gn
index 57e64bb..2e10d95 100644
--- a/ash/webui/BUILD.gn
+++ b/ash/webui/BUILD.gn
@@ -36,7 +36,7 @@
     "//ash/webui/scanning/mojom:unit_tests",
     "//ash/webui/shimless_rma/backend:unit_tests",
     "//ash/webui/shimless_rma/mojom:unit_tests",
-    "//ash/webui/shortcut_customization_ui/backend:unit_tests",
+    "//ash/webui/shortcut_customization_ui:unit_tests",
     "//ash/webui/shortcut_customization_ui/mojom:unit_tests",
     "//base",
     "//base/test:test_support",
diff --git a/ash/webui/diagnostics_ui/backend/input/input_data_provider_unittest.cc b/ash/webui/diagnostics_ui/backend/input/input_data_provider_unittest.cc
index 59267f0..f018e91 100644
--- a/ash/webui/diagnostics_ui/backend/input/input_data_provider_unittest.cc
+++ b/ash/webui/diagnostics_ui/backend/input/input_data_provider_unittest.cc
@@ -582,7 +582,7 @@
                                     int* result) const override {
     return false;
   }
-  bool TopRowKeysAreFunctionKeys() const override { return false; }
+  bool TopRowKeysAreFunctionKeys(int device_id) const override { return false; }
   bool IsExtensionCommandRegistered(ui::KeyboardCode key_code,
                                     int flags) const override {
     return false;
diff --git a/ash/webui/eche_app_ui/eche_stream_status_change_handler_unittest.cc b/ash/webui/eche_app_ui/eche_stream_status_change_handler_unittest.cc
index afead8755..1e93232 100644
--- a/ash/webui/eche_app_ui/eche_stream_status_change_handler_unittest.cc
+++ b/ash/webui/eche_app_ui/eche_stream_status_change_handler_unittest.cc
@@ -145,10 +145,6 @@
   EXPECT_EQ(mojom::StreamStatus::kStreamStatusInitializing,
             GetObservedStreamStatus());
 
-  NotifyStreamStatus(mojom::StreamStatus::kStreamStatusConnected);
-  EXPECT_EQ(mojom::StreamStatus::kStreamStatusConnected,
-            GetObservedStreamStatus());
-
   NotifyStreamStatus(mojom::StreamStatus::kStreamStatusStarted);
   EXPECT_EQ(mojom::StreamStatus::kStreamStatusStarted,
             GetObservedStreamStatus());
@@ -156,10 +152,6 @@
   NotifyStreamStatus(mojom::StreamStatus::kStreamStatusStopped);
   EXPECT_EQ(mojom::StreamStatus::kStreamStatusStopped,
             GetObservedStreamStatus());
-
-  NotifyStreamStatus(mojom::StreamStatus::kStreamStatusFailed);
-  EXPECT_EQ(mojom::StreamStatus::kStreamStatusFailed,
-            GetObservedStreamStatus());
 }
 
 }  // namespace eche_app
diff --git a/ash/webui/eche_app_ui/eche_tray_stream_status_observer_unittest.cc b/ash/webui/eche_app_ui/eche_tray_stream_status_observer_unittest.cc
index 44f4e91..89c767e7 100644
--- a/ash/webui/eche_app_ui/eche_tray_stream_status_observer_unittest.cc
+++ b/ash/webui/eche_app_ui/eche_tray_stream_status_observer_unittest.cc
@@ -152,25 +152,6 @@
   EXPECT_FALSE(eche_tray()->is_active());
 }
 
-TEST_F(EcheTrayStreamStatusObserverTest, OnStreamStatusChangedFailed) {
-  LaunchBubble(GURL("http://google.com"), gfx::Image(), u"app 1", u"your phone",
-               base::BindOnce(&GracefulCloseFunction),
-               base::BindRepeating(&GracefulGoBackFunction));
-  OnStreamStatusChanged(mojom::StreamStatus::kStreamStatusStarted);
-
-  // Wait for Eche Tray to load Eche Web to complete.
-  base::RunLoop().RunUntilIdle();
-  // Eche tray should be visible when streaming is active
-  EXPECT_TRUE(eche_tray()->get_bubble_wrapper_for_test());
-
-  OnStreamStatusChanged(mojom::StreamStatus::kStreamStatusFailed);
-
-  // Wait for Eche Web to close.
-  base::RunLoop().RunUntilIdle();
-  // Eche tray should not be visible when streaming is finished
-  EXPECT_FALSE(eche_tray()->is_active());
-}
-
 TEST_F(EcheTrayStreamStatusObserverTest,
        StartGracefulCloseWhenFeatureStatusToIneligible) {
   ResetUnloadWebContent();
diff --git a/ash/webui/eche_app_ui/mojom/eche_app.mojom b/ash/webui/eche_app_ui/mojom/eche_app.mojom
index 7d7ea1d..925eaa1 100644
--- a/ash/webui/eche_app_ui/mojom/eche_app.mojom
+++ b/ash/webui/eche_app_ui/mojom/eche_app.mojom
@@ -108,8 +108,6 @@
     kStreamStatusInitializing, // Eche browser is setting up video streaming
     kStreamStatusStarted,      // Video streaming is set up and started
     kStreamStatusStopped,      // Video streaming is stopped
-    kStreamStatusConnected,    // Connected to phone, but not streaming video
-    kStreamStatusFailed,       // Failed bootstrap connection
 };
 
 // Interface to notify the video streaming status from Eche browser to Eche
diff --git a/ash/webui/help_app_ui/search/search_concept.cc b/ash/webui/help_app_ui/search/search_concept.cc
index fba5709..878caab 100644
--- a/ash/webui/help_app_ui/search/search_concept.cc
+++ b/ash/webui/help_app_ui/search/search_concept.cc
@@ -12,6 +12,7 @@
 #include "base/files/file_util.h"
 #include "base/files/important_file_writer.h"
 #include "base/functional/bind.h"
+#include "base/metrics/histogram_functions.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/task/bind_post_task.h"
 #include "base/task/thread_pool.h"
@@ -23,6 +24,31 @@
 
 using Concept = SearchConceptProto::Concept;
 
+constexpr char kReadHistogram[] =
+    "Discover.SearchConcept.PersistenceReadStatus";
+constexpr char kWriteHistogram[] =
+    "Discover.SearchConcept.PersistenceWriteStatus";
+
+// The result of reading a backing file from disk. These values persist to logs.
+// Entries should not be renumbered and numeric values should never be reused.
+enum class ReadStatus {
+  kOk = 0,
+  kMissing = 1,
+  kReadError = 2,
+  kParseError = 3,
+  kMaxValue = kParseError,
+};
+
+// The result of writing a backing file to disk. These values persist to logs.
+// Entries should not be renumbered and numeric values should never be reused.
+enum class WriteStatus {
+  kOk = 0,
+  kWriteError = 1,
+  kSerializationError = 2,
+  kReplaceError = 3,
+  kMaxValue = kReplaceError,
+};
+
 // This should be incremented whenever a change to the search concept is made
 // that is incompatible with on-disk state. On reading, any state is wiped if
 // its version doesn't match.
@@ -33,18 +59,22 @@
   base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                 base::BlockingType::MAY_BLOCK);
   if (!base::PathExists(file_path)) {
+    base::UmaHistogramEnumeration(kReadHistogram, ReadStatus::kMissing);
     return nullptr;
   }
 
   std::string proto_str;
   if (!base::ReadFileToString(file_path, &proto_str)) {
+    base::UmaHistogramEnumeration(kReadHistogram, ReadStatus::kReadError);
     return nullptr;
   }
 
   auto proto = std::make_unique<SearchConceptProto>();
   if (!proto->ParseFromString(proto_str)) {
+    base::UmaHistogramEnumeration(kReadHistogram, ReadStatus::kParseError);
     return nullptr;
   }
+  base::UmaHistogramEnumeration(kReadHistogram, ReadStatus::kOk);
 
   // Discard the proto if the version does not match.
   if (!proto->has_version() || proto->version() != kVersion) {
@@ -61,6 +91,8 @@
                 const base::FilePath& temp_file_path) {
   std::string proto_str;
   if (!proto->SerializeToString(&proto_str)) {
+    base::UmaHistogramEnumeration(kWriteHistogram,
+                                  WriteStatus::kSerializationError);
     return;
   }
 
@@ -78,11 +110,18 @@
         temp_file_path, proto_str, "HelpAppPersistentProto");
   }
 
+  if (!write_succeed) {
+    base::UmaHistogramEnumeration(kWriteHistogram, WriteStatus::kWriteError);
+    return;
+  }
+
   // Replace the proto in `file_path_` by the temporary proto if the write is
   // succeed.
-  if (write_succeed) {
-    base::ReplaceFile(temp_file_path, file_path, nullptr);
-  }
+  const bool replace_succeed =
+      base::ReplaceFile(temp_file_path, file_path, nullptr);
+  base::UmaHistogramEnumeration(
+      kWriteHistogram,
+      replace_succeed ? WriteStatus::kOk : WriteStatus::kReplaceError);
 }
 
 }  // namespace
diff --git a/ash/webui/help_app_ui/search/search_handler.cc b/ash/webui/help_app_ui/search/search_handler.cc
index 5a1e093..7bab197 100644
--- a/ash/webui/help_app_ui/search/search_handler.cc
+++ b/ash/webui/help_app_ui/search/search_handler.cc
@@ -27,12 +27,12 @@
 // persist to logs. Entries should not be renumbered and numeric values should
 // never be reused.
 enum class SearchResultStatus {
-  // The first Update hasn't finished yet, and the index is still empty, so the
-  // Search Handler is not ready to handle searches.
+  // There is no cache update, and the index is still empty, so the Search
+  // Handler is not ready to handle searches.
   kNotReadyAndEmptyIndex = 0,
-  // Not ready and the status is something other than EmptyIndex. This should be
-  // far less common than kNotReadyAndEmptyIndex.
-  kNotReadyAndOtherStatus = 1,
+  // Not ready and the cache status is updating. This should be far less common
+  // than kNotReadyAndEmptyIndex.
+  kNotReadyAndUpdating = 1,
   // Ready and the LSS response status is Success.
   kReadyAndSuccess = 2,
   // Ready and the LSS response status is EmptyIndex. This can happen for
@@ -74,7 +74,8 @@
     SearchTagRegistry* search_tag_registry,
     local_search_service::LocalSearchServiceProxy* local_search_service_proxy)
     : search_tag_registry_(search_tag_registry),
-      cache_status_(CacheStatus::kEmpty) {
+      cache_status_(CacheStatus::kEmpty),
+      construction_time_(base::TimeTicks::Now()) {
   local_search_service_proxy->GetIndex(
       local_search_service::IndexId::kHelpAppLauncher,
       local_search_service::Backend::kInvertedIndex,
@@ -115,6 +116,9 @@
 
   // Reject the search request if the cache is not ready yet.
   if (cache_status_ != CacheStatus::kReady) {
+    LogSearchResultStatus(cache_status_ == CacheStatus::kEmpty
+                              ? SearchResultStatus::kNotReadyAndEmptyIndex
+                              : SearchResultStatus::kNotReadyAndUpdating);
     std::move(callback).Run({});
     return;
   }
@@ -162,6 +166,12 @@
 
 void SearchHandler::OnRegistryUpdated() {
   cache_status_ = CacheStatus::kReady;
+  if (!construction_time_.is_null()) {
+    base::UmaHistogramTimes("Discover.SearchHandler.SearchAvailableTime",
+                            base::TimeTicks::Now() - construction_time_);
+    // Reset as we only care about the first time the search is available.
+    construction_time_ = base::TimeTicks();
+  }
   for (auto& observer : observers_)
     observer->OnSearchResultAvailabilityChanged();
 }
@@ -191,19 +201,10 @@
     const absl::optional<std::vector<local_search_service::Result>>&
         local_search_service_results) {
   if (response_status != local_search_service::ResponseStatus::kSuccess) {
-    if (response_status == local_search_service::ResponseStatus::kEmptyIndex) {
-      if (cache_status_ == CacheStatus::kReady) {
-        LogSearchResultStatus(SearchResultStatus::kReadyAndEmptyIndex);
-      } else {
-        LogSearchResultStatus(SearchResultStatus::kNotReadyAndEmptyIndex);
-      }
-    } else {
-      if (cache_status_ == CacheStatus::kReady) {
-        LogSearchResultStatus(SearchResultStatus::kReadyAndOtherStatus);
-      } else {
-        LogSearchResultStatus(SearchResultStatus::kNotReadyAndOtherStatus);
-      }
-    }
+    LogSearchResultStatus(
+        response_status == local_search_service::ResponseStatus::kEmptyIndex
+            ? SearchResultStatus::kReadyAndEmptyIndex
+            : SearchResultStatus::kReadyAndOtherStatus);
     std::move(callback).Run({});
     return;
   }
diff --git a/ash/webui/help_app_ui/search/search_handler.h b/ash/webui/help_app_ui/search/search_handler.h
index 8a50df2c..c4215d6 100644
--- a/ash/webui/help_app_ui/search/search_handler.h
+++ b/ash/webui/help_app_ui/search/search_handler.h
@@ -12,6 +12,7 @@
 #include "ash/webui/help_app_ui/search/search_concept.h"
 #include "ash/webui/help_app_ui/search/search_tag_registry.h"
 #include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
 #include "chromeos/ash/components/local_search_service/public/cpp/local_search_service_proxy.h"
 #include "chromeos/ash/components/local_search_service/public/mojom/index.mojom.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
@@ -88,6 +89,11 @@
 
   CacheStatus cache_status_;
 
+  // The time this class is constructed. This is also used as a flag to indicate
+  // if the availability latency has been logged. After logging, the
+  // `construction_time_` will be reset to null.
+  base::TimeTicks construction_time_;
+
   // Note: Expected to have multiple clients, so ReceiverSet/RemoteSet are used.
   mojo::ReceiverSet<mojom::SearchHandler> receivers_;
   mojo::RemoteSet<mojom::SearchResultsObserver> observers_;
diff --git a/ash/webui/help_app_ui/search/search_handler_unittest.cc b/ash/webui/help_app_ui/search/search_handler_unittest.cc
index de738df5..25e0236 100644
--- a/ash/webui/help_app_ui/search/search_handler_unittest.cc
+++ b/ash/webui/help_app_ui/search/search_handler_unittest.cc
@@ -257,12 +257,7 @@
   EXPECT_GT(search_results[1]->relevance_score, 0.01);
 }
 
-// TODO(b/270091661): These two unit tests below are currently broken as the
-// search logic has been changed a bit. We will modify the UMA metric and
-// re-enable them when the UMA metric changes are merged.
-
-// Re-enable the UMA metric checks when the changes are made.
-TEST_F(HelpAppSearchHandlerTest, DISABLED_SearchStatusNotReadyAndEmptyIndex) {
+TEST_F(HelpAppSearchHandlerTest, SearchStatusNotReadyAndEmptyIndex) {
   base::HistogramTester histogram_tester;
   std::vector<mojom::SearchResultPtr> search_results;
 
@@ -276,22 +271,6 @@
       "Discover.SearchHandler.SearchResultStatus", 0, 1);
 }
 
-// Re-enable the UMA metric checks when the changes are made.
-TEST_F(HelpAppSearchHandlerTest, DISABLED_SearchStatusNotReadyAndOtherStatus) {
-  base::HistogramTester histogram_tester;
-  std::vector<mojom::SearchResultPtr> search_results;
-
-  // The empty search query makes the LSS respond with kEmptyQuery rather than
-  // kEmptyIndex.
-  mojom::SearchHandlerAsyncWaiter(handler_remote_.get())
-      .Search(u"", /*max_num_results=*/3u, &search_results);
-
-  EXPECT_TRUE(search_results.empty());
-  // 1 is kNotReadyAndOtherStatus.
-  histogram_tester.ExpectUniqueSample(
-      "Discover.SearchHandler.SearchResultStatus", 1, 1);
-}
-
 TEST_F(HelpAppSearchHandlerTest, SearchStatusReadyAndSuccess) {
   // Add one item to the search index.
   std::vector<mojom::SearchConceptPtr> search_concepts;
diff --git a/ash/webui/personalization_app/mojom/personalization_app.mojom b/ash/webui/personalization_app/mojom/personalization_app.mojom
index c459e63..84aa31f 100644
--- a/ash/webui/personalization_app/mojom/personalization_app.mojom
+++ b/ash/webui/personalization_app/mojom/personalization_app.mojom
@@ -543,8 +543,7 @@
   kSlideshow = 0,
   kFeelTheBreeze = 1,
   kFloatOnBy = 2,
-  kVideoNewMexico = 3,
-  kVideoClouds = 4,
+  kVideo = 3,
 };
 
 // The source of the screensaver images come from.
@@ -677,11 +676,19 @@
 const uint32 kIndigoColor = 0x4285F4;
 const uint32 kPurpleColor = 0xB76DF8;
 
+union CurrentBacklightState {
+  // Set if the user has selected a specific color as backlight color.
+  BacklightColor color;
+
+  // Set if the user has selected multi-zone colors as backlight color.
+  array<BacklightColor> zone_colors;
+};
+
 // Receives information whenever there are keyboard backlight related changes
 // such as backlight colors.
 interface KeyboardBacklightObserver {
-  // Notifies the JS side about the current state of the backlight color.
-  OnBacklightColorChanged(BacklightColor color);
+  // Notifies the JS side about the current backlight state.
+  OnBacklightStateChanged(CurrentBacklightState currentBacklightState);
 
   // Notifies the JS side the current wallpaper-extracted color.
   OnWallpaperColorChanged(skia.mojom.SkColor wallpaper_color);
diff --git a/ash/webui/personalization_app/mojom/personalization_app_mojom_traits.cc b/ash/webui/personalization_app/mojom/personalization_app_mojom_traits.cc
index 3c0cb75..3bd03ab4 100644
--- a/ash/webui/personalization_app/mojom/personalization_app_mojom_traits.cc
+++ b/ash/webui/personalization_app/mojom/personalization_app_mojom_traits.cc
@@ -401,10 +401,8 @@
       return MojomAnimationTheme::kFeelTheBreeze;
     case ash::AmbientTheme::kFloatOnBy:
       return MojomAnimationTheme::kFloatOnBy;
-    case ash::AmbientTheme::kVideoNewMexico:
-      return MojomAnimationTheme::kVideoNewMexico;
-    case ash::AmbientTheme::kVideoClouds:
-      return MojomAnimationTheme::kVideoClouds;
+    case ash::AmbientTheme::kVideo:
+      return MojomAnimationTheme::kVideo;
   }
 }
 
@@ -421,11 +419,8 @@
     case MojomAnimationTheme::kFloatOnBy:
       *output = ash::AmbientTheme::kFloatOnBy;
       return true;
-    case MojomAnimationTheme::kVideoNewMexico:
-      *output = ash::AmbientTheme::kVideoNewMexico;
-      return true;
-    case MojomAnimationTheme::kVideoClouds:
-      *output = ash::AmbientTheme::kVideoClouds;
+    case MojomAnimationTheme::kVideo:
+      *output = ash::AmbientTheme::kVideo;
       return true;
   }
   NOTREACHED();
diff --git a/ash/webui/personalization_app/personalization_app_ui.cc b/ash/webui/personalization_app/personalization_app_ui.cc
index e9d7987..c7d871cd 100644
--- a/ash/webui/personalization_app/personalization_app_ui.cc
+++ b/ash/webui/personalization_app/personalization_app_ui.cc
@@ -259,6 +259,7 @@
        IDS_PERSONALIZATION_APP_KEYBOARD_BACKLIGHT_ZONE_CUSTOMIZATION_DISMISS_BUTTON},
       {"wallpaperColorDescription",
        IDS_PERSONALIZATION_APP_KEYBOARD_BACKLIGHT_WALLPAPER_COLOR_DESCRIPTION},
+      {"zoneTitle", IDS_PERSONALIZATION_APP_KEYBOARD_BACKLIGHT_ZONE_TITLE},
 
       // Google Photos strings
       // TODO(b/229149314): Finalize error and retry strings.
@@ -342,6 +343,7 @@
   AddResources(source);
   AddStrings(source);
   AddBooleans(source);
+  AddIntegers(source);
 }
 
 PersonalizationAppUI::~PersonalizationAppUI() = default;
@@ -403,11 +405,13 @@
 
   source->AddBoolean("isPersonalizationJellyEnabled",
                      features::IsPersonalizationJellyEnabled());
+}
 
-  source->AddBoolean(
-      "isMultiZoneRgbKeyboardSupported",
-      features::IsMultiZoneRgbKeyboardEnabled() &&
-          Shell::Get()->rgb_keyboard_manager()->GetZoneCount() > 1);
+void PersonalizationAppUI::AddIntegers(content::WebUIDataSource* source) {
+  source->AddInteger("keyboardBacklightZoneCount",
+                     features::IsMultiZoneRgbKeyboardEnabled()
+                         ? Shell::Get()->rgb_keyboard_manager()->GetZoneCount()
+                         : 0);
 }
 
 void PersonalizationAppUI::HandleWebUIRequest(
diff --git a/ash/webui/personalization_app/personalization_app_ui.h b/ash/webui/personalization_app/personalization_app_ui.h
index 378525d..c65e447 100644
--- a/ash/webui/personalization_app/personalization_app_ui.h
+++ b/ash/webui/personalization_app/personalization_app_ui.h
@@ -72,6 +72,8 @@
  private:
   void AddBooleans(content::WebUIDataSource* source);
 
+  void AddIntegers(content::WebUIDataSource* source);
+
   void HandleWebUIRequest(const std::string& path,
                           content::WebUIDataSource::GotDataCallback callback);
 
diff --git a/ash/webui/personalization_app/resources/BUILD.gn b/ash/webui/personalization_app/resources/BUILD.gn
index dc94157..f224703e 100644
--- a/ash/webui/personalization_app/resources/BUILD.gn
+++ b/ash/webui/personalization_app/resources/BUILD.gn
@@ -55,6 +55,7 @@
     "js/ambient/zero_state_element.ts",
     "js/keyboard_backlight/keyboard_backlight_element.ts",
     "js/keyboard_backlight/zone_customization_element.ts",
+    "js/keyboard_backlight/color_icon_element.ts",
     "js/personalization_main_element.ts",
     "js/personalization_router_element.ts",
     "js/personalization_toast_element.ts",
diff --git a/ash/webui/personalization_app/resources/js/ambient/ambient_preview_large_element.html b/ash/webui/personalization_app/resources/js/ambient/ambient_preview_large_element.html
index c4580a52..34f4781 100644
--- a/ash/webui/personalization_app/resources/js/ambient/ambient_preview_large_element.html
+++ b/ash/webui/personalization_app/resources/js/ambient/ambient_preview_large_element.html
@@ -273,6 +273,11 @@
     width: 100%;
   }
 
+  #thumbnailContainer.thumbnail-0 {
+    background-color: var(--personalization-app-grid-item-background-color);
+    border-radius: 12px;
+  }
+
   #thumbnailContainer.thumbnail-1 .thumbnail-item {
     border-radius: 60px;
   }
diff --git a/ash/webui/personalization_app/resources/js/ambient/ambient_preview_large_element.ts b/ash/webui/personalization_app/resources/js/ambient/ambient_preview_large_element.ts
index f22aa9c..d9aafbcdd 100644
--- a/ash/webui/personalization_app/resources/js/ambient/ambient_preview_large_element.ts
+++ b/ash/webui/personalization_app/resources/js/ambient/ambient_preview_large_element.ts
@@ -46,24 +46,20 @@
       collageImages_: {
         type: Array,
         computed:
-            'computeCollageImages_(topicSource_, previewAlbums_, previewImages_)',
+            'computeThumbnailImages_(topicSource_, previewAlbums_, previewImages_)',
       },
     };
   }
 
   private collageImages_: Url[];
 
-  /** Returns the array of images that form the collage. */
+  /** Returns the array of images that form the collage when Jelly is off. */
   private computeCollageImages_(): Url[] {
-    const maxLength = this.isPersonalizationJellyEnabled_ ? 3 : 4;
     switch (this.topicSource_) {
       case TopicSource.kArtGallery:
-        if (this.isPersonalizationJellyEnabled_ &&
-            isNonEmptyArray(this.previewImages_)) {
-          return this.previewImages_.slice(0, 2);
-        }
         return (this.previewAlbums_ || []).map(album => album.url).slice(0, 2);
       case TopicSource.kGooglePhotos:
+        const maxLength = 4;
         if (isNonEmptyArray(this.previewImages_)) {
           return this.previewImages_.length < maxLength ?
               [this.previewImages_[0]] :
@@ -78,6 +74,20 @@
     return [];
   }
 
+  /** Returns the array of thumbnail images. */
+  private computeThumbnailImages_(): Url[] {
+    if (!this.isPersonalizationJellyEnabled_) {
+      return this.computeCollageImages_();
+    }
+    if (isNonEmptyArray(this.previewImages_)) {
+      const maxLength = Math.min(
+          this.previewImages_.length,
+          this.topicSource_ === TopicSource.kArtGallery ? 2 : 3);
+      return this.previewImages_.slice(0, maxLength);
+    }
+    return [];
+  }
+
   private onClickAmbientSubpageLink_() {
     PersonalizationRouter.instance().goToRoute(Paths.AMBIENT);
   }
diff --git a/ash/webui/personalization_app/resources/js/ambient/ambient_preview_small_element.html b/ash/webui/personalization_app/resources/js/ambient/ambient_preview_small_element.html
index 391b71de..62af25a2 100644
--- a/ash/webui/personalization_app/resources/js/ambient/ambient_preview_small_element.html
+++ b/ash/webui/personalization_app/resources/js/ambient/ambient_preview_small_element.html
@@ -130,6 +130,7 @@
           <cr-button
               class$="[[getScreenSaverPreviewClass_(ambientUiVisibility_)]]"
               aria-label$="[[getScreenSaverPreviewAriaLabel_(ambientUiVisibility_)]]"
+              role$="[[getScreenSaverPreviewRole_(ambientUiVisibility_)]]"
               on-click="startScreenSaverPreview_">
             <iron-icon icon="personalization:fullscreen"
                 hidden$="[[screenSaverPreviewActive_]]"></iron-icon>
diff --git a/ash/webui/personalization_app/resources/js/ambient/ambient_preview_small_element.ts b/ash/webui/personalization_app/resources/js/ambient/ambient_preview_small_element.ts
index 38d1e70..e906df0c 100644
--- a/ash/webui/personalization_app/resources/js/ambient/ambient_preview_small_element.ts
+++ b/ash/webui/personalization_app/resources/js/ambient/ambient_preview_small_element.ts
@@ -99,6 +99,10 @@
         this.i18n('screenSaverPreviewDownloadingAriaLabel') :
         this.i18n('screenSaverPreviewButtonAriaLabel');
   }
+
+  private getScreenSaverPreviewRole_(): string {
+    return this.screenSaverPreviewActive_ ? 'none' : 'button';
+  }
 }
 
 customElements.define(AmbientPreviewSmall.is, AmbientPreviewSmall);
diff --git a/ash/webui/personalization_app/resources/js/keyboard_backlight/color_icon_element.html b/ash/webui/personalization_app/resources/js/keyboard_backlight/color_icon_element.html
new file mode 100644
index 0000000..bd4fc483
--- /dev/null
+++ b/ash/webui/personalization_app/resources/js/keyboard_backlight/color_icon_element.html
@@ -0,0 +1,42 @@
+<style include="common">
+  :host {
+    height: 28px;
+    width: 28px;
+  }
+  .color-inner-container {
+    align-items: center;
+    border-radius: 50%;
+    display: flex;
+    height: 100%;
+    justify-content: center;
+    position: relative;
+    width: 100%;
+  }
+  .dark-icon {
+    fill: var(--cros-icon-color-primary-dark);
+  }
+  .light-icon {
+    fill: var(--cros-icon-color-primary-light);
+  }
+</style>
+<template is="dom-if" if="[[isWallpaperColorId_(colorId)]]">
+  <div class="color-inner-container"
+        style$="[[getWallpaperColorInnerContainerStyle_(wallpaperColor_)]]">
+      <svg class$="[[getWallpaperIconColorClass_(wallpaperColor_)]]" viewBox="0 0 20 20" width="16" height="16">
+        <path fill-rule="evenodd" clip-rule="evenodd" d="M12.0605 5.585L13.5805 6.935C14.3705 7.715 14.2005 8.985 13.4105 9.765L4.76469 18.4108C3.98364 19.1918 2.71731 19.1918 1.93626 18.4108L0.583834 17.0584C-0.196879 16.2776 -0.197265 15.012 0.582974 14.2308L7.40048 7.405L9.23048 5.585C10.0105 4.805 11.2805 4.805 12.0605 5.585ZM8.82101 8.81494L1.95101 15.6849L3.36101 17.1049L10.231 10.2249L8.82101 8.81494Z">
+        </path>
+        <path d="M16 6L15.06 3.94L13 3L15.06 2.06L16 0L16.94 2.06L19 3L16.94 3.94L16 6Z">
+        </path>
+        <path d="M17.001 17L16.061 14.94L14.001 14L16.061 13.06L17.001 11L17.941 13.06L20.001 14L17.941 14.94L17.001 17Z">
+        </path>
+        <path d="M17.001 17L16.061 14.94L14.001 14L16.061 13.06L17.001 11L17.941 13.06L20.001 14L17.941 14.94L17.001 17Z">
+        </path>
+        <path d="M3.06 4.94L4 7L4.94 4.94L7 4L4.94 3.06L4 1L3.06 3.06L1 4L3.06 4.94Z">
+        </path>
+      </svg>
+  </div>
+</template>
+<template is="dom-if" if="[[!isWallpaperColorId_(colorId)]]">
+  <div class="color-inner-container" style$="[[getColorInnerContainerStyle_(colorId)]]">
+  </div>
+</template>
diff --git a/ash/webui/personalization_app/resources/js/keyboard_backlight/color_icon_element.ts b/ash/webui/personalization_app/resources/js/keyboard_backlight/color_icon_element.ts
new file mode 100644
index 0000000..2d5d747
--- /dev/null
+++ b/ash/webui/personalization_app/resources/js/keyboard_backlight/color_icon_element.ts
@@ -0,0 +1,110 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js';
+
+import {WithPersonalizationStore} from '../personalization_store.js';
+import {convertToRgbHexStr, getPresetColors, GREEN, INDIGO, RAINBOW, RED, WALLPAPER, YELLOW} from '../utils.js';
+
+import {getTemplate} from './color_icon_element.html.js';
+
+/**
+ * @fileoverview
+ * The color icon indicates wallpaper or preset colors in keyboard backlight and
+ * zone customization section.
+ */
+
+/**
+  Based on this algorithm suggested by the W3:
+  https://www.w3.org/TR/AERT/#color-contrast
+*/
+function calculateColorBrightness(hexVal: number): number {
+  const r = (hexVal >> 16) & 0xff;  // extract red
+  const g = (hexVal >> 8) & 0xff;   // extract green
+  const b = (hexVal >> 0) & 0xff;   // extract blue
+  return (r * 299 + g * 587 + b * 114) / 1000;
+}
+
+export class ColorIcon extends WithPersonalizationStore {
+  static get is() {
+    return 'color-icon';
+  }
+
+  static get template() {
+    return getTemplate();
+  }
+
+  static get properties() {
+    return {
+      /** The color id indicates the color of the icon. */
+      colorId: {
+        type: String,
+        value: null,
+        reflectToAttribute: true,
+      },
+
+      /** The current wallpaper extracted color. */
+      wallpaperColor_: Object,
+    };
+  }
+
+  colorId: string|null;
+  private wallpaperColor_: SkColor|null;
+
+  override connectedCallback() {
+    super.connectedCallback();
+    this.watch(
+        'wallpaperColor_', state => state.keyboardBacklight.wallpaperColor);
+    this.updateFromStore();
+  }
+
+  private isWallpaperColorId_(colorId: string|null): boolean {
+    return colorId === WALLPAPER;
+  }
+
+  private getColorInnerContainerStyle_(colorId: string|null): string {
+    if (colorId === null) {
+      return '';
+    }
+    const colors = getPresetColors();
+    const outlineStyle = `outline: 2px solid var(--cros-separator-color);
+                    outline-offset: -2px;`;
+    switch (colorId) {
+      case RAINBOW:
+        return `background-image: linear-gradient(90deg,
+            ${colors[RED].hexVal},
+            ${colors[YELLOW].hexVal},
+            ${colors[GREEN].hexVal},
+            ${colors[INDIGO].hexVal});
+            ${outlineStyle}`;
+      default:
+        return `background-color: ${colors[colorId].hexVal};
+                                    ${outlineStyle};`;
+    }
+  }
+
+  private getWallpaperColorInnerContainerStyle_(wallpaperColor: SkColor|
+                                                null): string {
+    const hexStr = !wallpaperColor ?
+        '#FFFFFF' :
+        convertToRgbHexStr(wallpaperColor.value & 0xFFFFFF);
+    return `background-color: ${hexStr};
+        outline: 2px solid var(--cros-separator-color);
+        outline-offset: -2px;`;
+  }
+
+  private getWallpaperIconColorClass_(wallpaperColor: SkColor): string {
+    if (!wallpaperColor || (wallpaperColor.value & 0xFFFFFF) === 0xFFFFFF) {
+      return `light-icon`;
+    }
+    const brightness =
+        calculateColorBrightness(wallpaperColor.value & 0xFFFFFF);
+    if (brightness < 125) {
+      return `dark-icon`;
+    }
+    return `light-icon`;
+  }
+}
+
+customElements.define(ColorIcon.is, ColorIcon);
diff --git a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_actions.ts b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_actions.ts
index 3a50bc4..887e4d7 100644
--- a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_actions.ts
+++ b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_actions.ts
@@ -5,7 +5,7 @@
 import {Action} from 'chrome://resources/ash/common/store/store.js';
 import {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js';
 
-import {BacklightColor} from '../../personalization_app.mojom-webui.js';
+import {CurrentBacklightState} from '../../personalization_app.mojom-webui.js';
 
 
 /**
@@ -13,17 +13,17 @@
  */
 
 export enum KeyboardBacklightActionName {
-  SET_BACKLIGHT_COLOR = 'set_backlight_color',
+  SET_CURRENT_BACKLIGHT_STATE = 'set_current_backlight_state',
   SET_SHOULD_SHOW_NUDGE = 'set_should_show_nudge',
   SET_WALLPAPER_COLOR = 'set_wallpaper_color',
 }
 
-export type KeyboardBacklightActions =
-    SetBacklightColorAction|SetShouldShowNudgeAction|SetWallpaperColorAction;
+export type KeyboardBacklightActions = SetCurrentBacklightStateAction|
+    SetShouldShowNudgeAction|SetWallpaperColorAction;
 
-export type SetBacklightColorAction = Action&{
-  name: KeyboardBacklightActionName.SET_BACKLIGHT_COLOR,
-  backlightColor: BacklightColor,
+export type SetCurrentBacklightStateAction = Action&{
+  name: KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE,
+  currentBacklightState: CurrentBacklightState,
 };
 
 export type SetShouldShowNudgeAction = Action&{
@@ -37,13 +37,14 @@
 };
 
 /**
- * Sets the current value of the backlight color.
+ * Sets the current value of the backlight state.
  */
-export function setBacklightColorAction(backlightColor: BacklightColor):
-    SetBacklightColorAction {
+export function setCurrentBacklightStateAction(
+    currentBacklightState: CurrentBacklightState):
+    SetCurrentBacklightStateAction {
   return {
-    name: KeyboardBacklightActionName.SET_BACKLIGHT_COLOR,
-    backlightColor,
+    name: KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE,
+    currentBacklightState,
   };
 }
 
diff --git a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_controller.ts b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_controller.ts
index b9b45b923..650459f5 100644
--- a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_controller.ts
+++ b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_controller.ts
@@ -5,7 +5,7 @@
 import {BacklightColor, KeyboardBacklightProviderInterface} from '../../personalization_app.mojom-webui.js';
 import {PersonalizationStore} from '../personalization_store.js';
 
-import {setBacklightColorAction, setShouldShowNudgeAction} from './keyboard_backlight_actions.js';
+import {setCurrentBacklightStateAction, setShouldShowNudgeAction} from './keyboard_backlight_actions.js';
 
 /**
  * @fileoverview contains all of the functions to interact with keyboard
@@ -19,8 +19,8 @@
     provider: KeyboardBacklightProviderInterface, store: PersonalizationStore) {
   provider.setBacklightColor(backlightColor);
 
-  // Dispatch action to highlight backlight color.
-  store.dispatch(setBacklightColorAction(backlightColor));
+  // Dispatch action to set the current backlight state.
+  store.dispatch(setCurrentBacklightStateAction({color: backlightColor}));
 }
 
 // Set the keyboard backlight color for the given zone.
diff --git a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_element.html b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_element.html
index f225c416..414575b7 100644
--- a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_element.html
+++ b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_element.html
@@ -70,25 +70,7 @@
     width: var(--color-container-size);
   }
 
-  .color-inner-container {
-    align-items: center;
-    border-radius: 50%;
-    display: flex;
-    height: 28px;
-    justify-content: center;
-    position: relative;
-    width: 28px;
-  }
-
-  .dark-icon {
-    fill: var(--cros-icon-color-primary-dark);
-  }
-
-  .light-icon {
-    fill: var(--cros-icon-color-primary-light);
-  }
-
-  .color-container[aria-checked='true'] .color-inner-container {
+  .color-container[aria-checked='true'] color-icon {
     height: 36px;
     width: 36px;
   }
@@ -176,21 +158,7 @@
           aria-checked$="[[getWallpaperColorAriaChecked_(backlightColor_)]]"
           title$="[[getWallpaperColorTitle_()]]"
           role="radio">
-        <div class="color-inner-container"
-            style$="[[getWallpaperColorInnerContainerStyle_(wallpaperColor_)]]">
-          <svg class$="[[getWallpaperIconColorClass_(wallpaperColor_)]]" viewBox="0 0 20 20" width="16" height="16">
-            <path fill-rule="evenodd" clip-rule="evenodd" d="M12.0605 5.585L13.5805 6.935C14.3705 7.715 14.2005 8.985 13.4105 9.765L4.76469 18.4108C3.98364 19.1918 2.71731 19.1918 1.93626 18.4108L0.583834 17.0584C-0.196879 16.2776 -0.197265 15.012 0.582974 14.2308L7.40048 7.405L9.23048 5.585C10.0105 4.805 11.2805 4.805 12.0605 5.585ZM8.82101 8.81494L1.95101 15.6849L3.36101 17.1049L10.231 10.2249L8.82101 8.81494Z">
-            </path>
-            <path d="M16 6L15.06 3.94L13 3L15.06 2.06L16 0L16.94 2.06L19 3L16.94 3.94L16 6Z">
-            </path>
-            <path d="M17.001 17L16.061 14.94L14.001 14L16.061 13.06L17.001 11L17.941 13.06L20.001 14L17.941 14.94L17.001 17Z">
-            </path>
-            <path d="M17.001 17L16.061 14.94L14.001 14L16.061 13.06L17.001 11L17.941 13.06L20.001 14L17.941 14.94L17.001 17Z">
-            </path>
-            <path d="M3.06 4.94L4 7L4.94 4.94L7 4L4.94 3.06L4 1L3.06 3.06L1 4L3.06 4.94Z">
-            </path>
-          </svg>
-        </div>
+        <color-icon color-id="[[wallpaperColorId_]]"></color-icon>
       </div>
     </template>
     <template is="dom-if" if="[[shouldShowNudge_]]">
@@ -215,9 +183,7 @@
           role="radio"
           aria-checked$="[[getPresetColorAriaChecked_(presetColorId, presetColors_, backlightColor_)]]"
           title$="[[getPresetColorAriaLabel_(presetColorId)]]">
-        <div class="color-inner-container"
-            style$="[[getColorInnerContainerStyle_(presetColorId, presetColors_)]]">
-        </div>
+        <color-icon color-id="[[presetColorId]]"></color-icon>
       </div>
     </template>
     <div id$="[[rainbowColorId_]]"
@@ -227,9 +193,7 @@
         aria-label="$i18n{rainbowColor}" role="radio"
         aria-checked$="[[getRainbowColorAriaChecked_(backlightColor_)]]"
         title="$i18n{rainbowColor}">
-      <div class="color-inner-container"
-          style$="[[getColorInnerContainerStyle_(rainbowColorId_, presetColors_)]]">
-      </div>
+      <color-icon color-id="[[rainbowColorId_]]"></color-icon>
     </div>
     <template is="dom-if" if="[[isMultiZoneRgbKeyboardSupported_]]">
       <div id$="[[wallpaperColorId_]]"
@@ -241,28 +205,14 @@
           aria-checked$="[[getWallpaperColorAriaChecked_(backlightColor_)]]"
           title$="[[getWallpaperColorTitle_()]]"
           role="radio">
-        <div class="color-inner-container"
-            style$="[[getWallpaperColorInnerContainerStyle_(wallpaperColor_)]]">
-          <svg class$="[[getWallpaperIconColorClass_(wallpaperColor_)]]" viewBox="0 0 20 20" width="16" height="16">
-            <path fill-rule="evenodd" clip-rule="evenodd" d="M12.0605 5.585L13.5805 6.935C14.3705 7.715 14.2005 8.985 13.4105 9.765L4.76469 18.4108C3.98364 19.1918 2.71731 19.1918 1.93626 18.4108L0.583834 17.0584C-0.196879 16.2776 -0.197265 15.012 0.582974 14.2308L7.40048 7.405L9.23048 5.585C10.0105 4.805 11.2805 4.805 12.0605 5.585ZM8.82101 8.81494L1.95101 15.6849L3.36101 17.1049L10.231 10.2249L8.82101 8.81494Z">
-            </path>
-            <path d="M16 6L15.06 3.94L13 3L15.06 2.06L16 0L16.94 2.06L19 3L16.94 3.94L16 6Z">
-            </path>
-            <path d="M17.001 17L16.061 14.94L14.001 14L16.061 13.06L17.001 11L17.941 13.06L20.001 14L17.941 14.94L17.001 17Z">
-            </path>
-            <path d="M17.001 17L16.061 14.94L14.001 14L16.061 13.06L17.001 11L17.941 13.06L20.001 14L17.941 14.94L17.001 17Z">
-            </path>
-            <path d="M3.06 4.94L4 7L4.94 4.94L7 4L4.94 3.06L4 1L3.06 3.06L1 4L3.06 4.94Z">
-            </path>
-          </svg>
-        </div>
+        <color-icon color-id="[[wallpaperColorId_]]"></color-icon>
       </div>
       <div id="wallpaperColorDescription">
         $i18n{wallpaperColorDescription}
       </div>
       <cr-button id="zoneCustomizationButton" on-click="showZoneCustomizationDialog_"
           class="secondary"
-          aria-pressed$="[[getZoneCustomizationButtonAriaPressed_()]]">
+          aria-pressed$="[[getZoneCustomizationButtonAriaPressed_(currentBacklightState_)]]">
         <iron-icon icon="personalization:circle_checkmark"></iron-icon>
         <div class="text">$i18n{zoneCustomize}</div>
       </cr-button>
diff --git a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_element.ts b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_element.ts
index 5806b0b..9251509 100644
--- a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_element.ts
+++ b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_element.ts
@@ -6,6 +6,7 @@
 import 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js';
 import 'chrome://resources/polymer/v3_0/paper-ripple/paper-ripple.js';
 import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
+import './color_icon_element.js';
 import '../../css/common.css.js';
 import '../../css/cros_button_style.css.js';
 
@@ -15,10 +16,10 @@
 import {IronA11yKeysElement} from 'chrome://resources/polymer/v3_0/iron-a11y-keys/iron-a11y-keys.js';
 import {IronSelectorElement} from 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js';
 
-import {BacklightColor, BLUE_COLOR, GREEN_COLOR, INDIGO_COLOR, PURPLE_COLOR, RED_COLOR, WHITE_COLOR, YELLOW_COLOR} from '../../personalization_app.mojom-webui.js';
+import {BacklightColor, CurrentBacklightState} from '../../personalization_app.mojom-webui.js';
 import {isMultiZoneRgbKeyboardSupported} from '../load_time_booleans.js';
 import {WithPersonalizationStore} from '../personalization_store.js';
-import {convertToRgbHexStr, isSelectionEvent} from '../utils.js';
+import {ColorInfo, getPresetColors, isSelectionEvent, RAINBOW, WALLPAPER} from '../utils.js';
 
 import {getShouldShowNudge, handleNudgeShown, setBacklightColor} from './keyboard_backlight_controller.js';
 import {getTemplate} from './keyboard_backlight_element.html.js';
@@ -41,22 +42,6 @@
   };
 }
 
-/**
-  Based on this algorithm suggested by the W3:
-  https://www.w3.org/TR/AERT/#color-contrast
-*/
-function calculateColorBrightness(hexVal: number): number {
-  const r = (hexVal >> 16) & 0xff;  // extract red
-  const g = (hexVal >> 8) & 0xff;   // extract green
-  const b = (hexVal >> 0) & 0xff;   // extract blue
-  return (r * 299 + g * 587 + b * 114) / 1000;
-}
-
-interface ColorInfo {
-  hexVal: string;
-  enumVal: BacklightColor;
-}
-
 export class KeyboardBacklight extends WithPersonalizationStore {
   static get is() {
     return 'keyboard-backlight';
@@ -87,19 +72,24 @@
 
       rainbowColorId_: {
         type: String,
-        value: 'rainbow',
+        value: RAINBOW,
       },
 
       wallpaperColorId_: {
         type: String,
-        value: 'wallpaper',
+        value: WALLPAPER,
+      },
+
+      backlightColor_: {
+        type: Object,
+        computed: 'computeBacklightColor_(currentBacklightState_)',
       },
 
       /** The color currently highlighted by keyboard navigation. */
       ironSelectedColor_: Object,
 
-      /** The selected backlight color in the system. */
-      backlightColor_: Object,
+      /** The current backlight state in the system. */
+      currentBacklightState_: Object,
 
       /** The current wallpaper extracted color. */
       wallpaperColor_: Object,
@@ -115,10 +105,11 @@
   private isMultiZoneRgbKeyboardSupported_: boolean;
   private presetColors_: Record<string, ColorInfo>;
   private presetColorIds_: string[];
+  private backlightColor_: BacklightColor|null|undefined;
   private rainbowColorId_: string;
   private wallpaperColorId_: string;
   private ironSelectedColor_: HTMLElement;
-  private backlightColor_: BacklightColor|null;
+  private currentBacklightState_: CurrentBacklightState|null;
   private wallpaperColor_: SkColor|null;
   private shouldShowNudge_: boolean;
 
@@ -130,8 +121,9 @@
   override connectedCallback() {
     super.connectedCallback();
     KeyboardBacklightObserver.initKeyboardBacklightObserverIfNeeded();
-    this.watch<KeyboardBacklight['backlightColor_']>(
-        'backlightColor_', state => state.keyboardBacklight.backlightColor);
+    this.watch<KeyboardBacklight['currentBacklightState_']>(
+        'currentBacklightState_',
+        state => state.keyboardBacklight.currentBacklightState);
     this.watch<KeyboardBacklight['shouldShowNudge_']>(
         'shouldShowNudge_', state => state.keyboardBacklight.shouldShowNudge);
     this.watch<KeyboardBacklight['wallpaperColor_']>(
@@ -142,36 +134,7 @@
   }
 
   private computePresetColors_(): Record<string, ColorInfo> {
-    return {
-      'whiteColor': {
-        hexVal: convertToRgbHexStr(WHITE_COLOR),
-        enumVal: BacklightColor.kWhite,
-      },
-      'redColor': {
-        hexVal: convertToRgbHexStr(RED_COLOR),
-        enumVal: BacklightColor.kRed,
-      },
-      'yellowColor': {
-        hexVal: convertToRgbHexStr(YELLOW_COLOR),
-        enumVal: BacklightColor.kYellow,
-      },
-      'greenColor': {
-        hexVal: convertToRgbHexStr(GREEN_COLOR),
-        enumVal: BacklightColor.kGreen,
-      },
-      'blueColor': {
-        hexVal: convertToRgbHexStr(BLUE_COLOR),
-        enumVal: BacklightColor.kBlue,
-      },
-      'indigoColor': {
-        hexVal: convertToRgbHexStr(INDIGO_COLOR),
-        enumVal: BacklightColor.kIndigo,
-      },
-      'purpleColor': {
-        hexVal: convertToRgbHexStr(PURPLE_COLOR),
-        enumVal: BacklightColor.kPurple,
-      },
-    };
+    return getPresetColors();
   }
 
   private computePresetColorIds_(presetColors: Record<string, string>):
@@ -180,6 +143,11 @@
     return Object.keys(presetColors);
   }
 
+  private computeBacklightColor_(currentBacklightState: CurrentBacklightState):
+      BacklightColor|null|undefined {
+    return currentBacklightState ? currentBacklightState.color : null;
+  }
+
   /** Handle keyboard navigation. */
   private onKeysPress_(
       e: CustomEvent<{key: string, keyboardEvent: KeyboardEvent}>) {
@@ -258,46 +226,6 @@
         this.getStore());
   }
 
-  private getColorInnerContainerStyle_(
-      colorId: string, colors: Record<string, ColorInfo>) {
-    const outlineStyle = `outline: 2px solid var(--cros-separator-color);
-                  outline-offset: -2px;`;
-    switch (colorId) {
-      case this.rainbowColorId_:
-        return `background-image: linear-gradient(90deg,
-            ${colors['redColor'].hexVal},
-            ${colors['yellowColor'].hexVal},
-            ${colors['greenColor'].hexVal},
-            ${colors['indigoColor'].hexVal});
-            ${outlineStyle}`;
-      default:
-        return `background-color: ${colors[colorId].hexVal};
-                                  ${outlineStyle};`;
-    }
-  }
-
-  private getWallpaperColorInnerContainerStyle_(wallpaperColor: SkColor):
-      string {
-    const hexStr = !wallpaperColor ?
-        '#FFFFFF' :
-        convertToRgbHexStr(wallpaperColor.value & 0xFFFFFF);
-    return `background-color: ${hexStr};
-            outline: 2px solid var(--cros-separator-color);
-            outline-offset: -2px;`;
-  }
-
-  private getWallpaperIconColorClass_(wallpaperColor: SkColor): string {
-    if (!wallpaperColor || (wallpaperColor.value & 0xFFFFFF) === 0xFFFFFF) {
-      return `light-icon`;
-    }
-    const brightness =
-        calculateColorBrightness(wallpaperColor.value & 0xFFFFFF);
-    if (brightness < 125) {
-      return `dark-icon`;
-    }
-    return `light-icon`;
-  }
-
   private getPresetColorAriaLabel_(presetColorId: string): string {
     return this.i18n(presetColorId);
   }
@@ -362,9 +290,10 @@
     this.$.zoneCustomizationRender.get().showModal();
   }
 
-  private getZoneCustomizationButtonAriaPressed_() {
-    // TODO(b/265854825): Handle selected state.
-    return 'false';
+  private getZoneCustomizationButtonAriaPressed_(
+      currentBacklightState: CurrentBacklightState): string {
+    return (!!currentBacklightState && !!currentBacklightState.zoneColors)
+        .toString();
   }
 }
 
diff --git a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_observer.ts b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_observer.ts
index fc2e218..239516cf 100644
--- a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_observer.ts
+++ b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_observer.ts
@@ -4,10 +4,10 @@
 
 import {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js';
 
-import {BacklightColor, KeyboardBacklightObserverInterface, KeyboardBacklightObserverReceiver, KeyboardBacklightProviderInterface} from '../../personalization_app.mojom-webui.js';
+import {CurrentBacklightState, KeyboardBacklightObserverInterface, KeyboardBacklightObserverReceiver, KeyboardBacklightProviderInterface} from '../../personalization_app.mojom-webui.js';
 import {PersonalizationStore} from '../personalization_store.js';
 
-import {setBacklightColorAction, setWallpaperColorAction} from './keyboard_backlight_actions.js';
+import {setCurrentBacklightStateAction, setWallpaperColorAction} from './keyboard_backlight_actions.js';
 import {getKeyboardBacklightProvider} from './keyboard_backlight_interface_provider.js';
 
 /** @fileoverview listens for updates on keyboard backlight settings changes. */
@@ -45,9 +45,9 @@
     return receiver;
   }
 
-  onBacklightColorChanged(backlightColor: BacklightColor): void {
+  onBacklightStateChanged(currentBacklightState: CurrentBacklightState): void {
     const store = PersonalizationStore.getInstance();
-    store.dispatch(setBacklightColorAction(backlightColor));
+    store.dispatch(setCurrentBacklightStateAction(currentBacklightState));
   }
 
   onWallpaperColorChanged(wallpaperColor: SkColor): void {
diff --git a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_reducers.ts b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_reducers.ts
index 1268935..17695af 100644
--- a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_reducers.ts
+++ b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_reducers.ts
@@ -4,7 +4,7 @@
 
 import {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js';
 
-import {BacklightColor} from '../../personalization_app.mojom-webui.js';
+import {CurrentBacklightState} from '../../personalization_app.mojom-webui.js';
 import {Actions} from '../personalization_actions.js';
 import {ReducerFunction} from '../personalization_reducers.js';
 import {PersonalizationState} from '../personalization_state.js';
@@ -12,12 +12,12 @@
 import {KeyboardBacklightActionName} from './keyboard_backlight_actions.js';
 import {KeyboardBacklightState} from './keyboard_backlight_state.js';
 
-export function backlightColorReducer(
-    state: BacklightColor|null, action: Actions,
-    _: PersonalizationState): BacklightColor|null {
+export function currentBacklightStateReducer(
+    state: CurrentBacklightState|null, action: Actions,
+    _: PersonalizationState): CurrentBacklightState|null {
   switch (action.name) {
-    case KeyboardBacklightActionName.SET_BACKLIGHT_COLOR:
-      return action.backlightColor;
+    case KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE:
+      return action.currentBacklightState;
     default:
       return state;
   }
@@ -48,7 +48,7 @@
   [K in keyof KeyboardBacklightState]:
       ReducerFunction<KeyboardBacklightState[K]>
 } = {
-  backlightColor: backlightColorReducer,
+  currentBacklightState: currentBacklightStateReducer,
   shouldShowNudge: shouldShowNudgeReducer,
   wallpaperColor: wallpaperColorReducer,
 };
diff --git a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_state.ts b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_state.ts
index aab0db8f..0c584136 100644
--- a/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_state.ts
+++ b/ash/webui/personalization_app/resources/js/keyboard_backlight/keyboard_backlight_state.ts
@@ -4,20 +4,20 @@
 
 import {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js';
 
-import {BacklightColor} from '../../personalization_app.mojom-webui.js';
+import {CurrentBacklightState} from '../../personalization_app.mojom-webui.js';
 
 /**
  * Stores keyboard backlight related states.
  */
 export interface KeyboardBacklightState {
-  backlightColor: BacklightColor|null;
+  currentBacklightState: CurrentBacklightState|null;
   shouldShowNudge: boolean;
   wallpaperColor: SkColor|null;
 }
 
 export function emptyState(): KeyboardBacklightState {
   return {
-    backlightColor: null,
+    currentBacklightState: null,
     shouldShowNudge: false,
     wallpaperColor: null,
   };
diff --git a/ash/webui/personalization_app/resources/js/keyboard_backlight/zone_customization_element.html b/ash/webui/personalization_app/resources/js/keyboard_backlight/zone_customization_element.html
index 64750d1b..45c122d 100644
--- a/ash/webui/personalization_app/resources/js/keyboard_backlight/zone_customization_element.html
+++ b/ash/webui/personalization_app/resources/js/keyboard_backlight/zone_customization_element.html
@@ -1,9 +1,51 @@
 <style include="common cros-button-style">
+  cr-dialog::part(dialog) {
+    width: 600px;
+  }
+  #zoneSelector {
+    background-color: var(--cros-sys-system_on_base);
+    border-radius: 18px;
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(0,1fr));
+    grid-template-rows: minmax(0,1fr);
+    width: 100%;
+  }
+  .zone-button {
+    border-style: none;
+    height: 100%;
+    width: 100%;
+  }
+  .zone-title-container {
+    align-items: center;
+    display: flex;
+    width: 100%;
+  }
+  color-icon {
+    margin-inline-end: 10px;
+  }
+  #zoneTitle {
+    max-width: calc(100% - 40px);
+    word-wrap: break-word;
+  }
 </style>
 <cr-dialog id="dialog" on-close="closeZoneCustomizationDialog_">
-  <!-- TODO(b/265853968): Add zone selector -->
   <div slot="body">
-    [temp] zone customization
+    <iron-selector
+        id="zoneSelector"
+        selected="0"
+        selected-item="{{ironSelectedZone_}}">
+      <template is="dom-repeat" items="[[zoneIdxs_]]" as="zoneIdx">
+        <cr-button
+            class="zone-button tab-slider"
+            tabindex$="[[getTabIndex_(zoneIdx)]]"
+            on-click="onClickZoneButton_">
+          <div class="zone-title-container">
+            <color-icon color-id="[[getColorId_(zoneIdx, zoneColors_)]]"></color-icon>
+            <div id="zoneTitle">[[getZoneTitle_(zoneIdx)]]</div>
+          </div>
+        </cr-button>
+      </template>
+    </iron-selector>
     <!-- TODO(b/265855838): Add color selector -->
     <cr-button on-click="setZoneOneToRed_">[temp] Red</cr-button>
   </div>
diff --git a/ash/webui/personalization_app/resources/js/keyboard_backlight/zone_customization_element.ts b/ash/webui/personalization_app/resources/js/keyboard_backlight/zone_customization_element.ts
index bd55a0f..31f278e2 100644
--- a/ash/webui/personalization_app/resources/js/keyboard_backlight/zone_customization_element.ts
+++ b/ash/webui/personalization_app/resources/js/keyboard_backlight/zone_customization_element.ts
@@ -9,11 +9,14 @@
  */
 
 import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
+import './color_icon_element.js';
 
+import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
 import {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
 
-import {BacklightColor} from '../../personalization_app.mojom-webui.js';
+import {BacklightColor, CurrentBacklightState} from '../../personalization_app.mojom-webui.js';
 import {WithPersonalizationStore} from '../personalization_store.js';
+import {RAINBOW, staticColorIds} from '../utils.js';
 
 import {setBacklightZoneColor} from './keyboard_backlight_controller.js';
 import {getKeyboardBacklightProvider} from './keyboard_backlight_interface_provider.js';
@@ -34,6 +37,50 @@
     return getTemplate();
   }
 
+  static get properties() {
+    return {
+      /** The currently selected zone index. */
+      ironSelectedZone_: Object,
+
+      /** The current backlight state in the system. */
+      currentBacklightState_: Object,
+
+      /** The current backlight zone colors. */
+      zoneColors_: {
+        type: Array,
+        computed: 'computeZoneColors_(currentBacklightState_, zoneCount_)',
+      },
+
+      /** Number of zones available for customization */
+      zoneCount_: {
+        type: Number,
+        value() {
+          return loadTimeData.getInteger('keyboardBacklightZoneCount');
+        },
+      },
+
+      /** The zone indexes (of zoneColors_) to indicate the zone number. */
+      zoneIdxs_: {
+        type: Array,
+        computed: 'computeZoneIdxs_(zoneCount_)',
+      },
+    };
+  }
+
+  private ironSelectedZone_: number;
+  private currentBacklightState_: CurrentBacklightState|null;
+  private zoneColors_: BacklightColor[]|null;
+  private zoneCount_: number;
+  private zoneIds_: number[];
+
+  override connectedCallback() {
+    super.connectedCallback();
+    this.watch(
+        'currentBacklightState_',
+        state => state.keyboardBacklight.currentBacklightState);
+    this.updateFromStore();
+  }
+
   showModal() {
     this.$.dialog.showModal();
   }
@@ -47,6 +94,45 @@
         0, BacklightColor.kRed, getKeyboardBacklightProvider());
   }
 
+  private computeZoneIdxs_(): number[] {
+    return [...Array(this.zoneCount_).keys()];
+  }
+
+  private computeZoneColors_(): BacklightColor[]|null {
+    if (this.currentBacklightState_ && this.currentBacklightState_.zoneColors) {
+      return this.currentBacklightState_.zoneColors;
+    } else if (
+        this.currentBacklightState_ &&
+        this.currentBacklightState_.color !== undefined) {
+      return Array(this.zoneCount_).fill(this.currentBacklightState_.color);
+    }
+    return null;
+  }
+
+  private getTabIndex_(zoneIdx: number): string {
+    return zoneIdx === 0 ? '0' : '-1';
+  }
+
+  private getZoneTitle_(zoneIdx: number): string {
+    return loadTimeData.getStringF('zoneTitle', zoneIdx + 1);
+  }
+
+  // Returns the matching colorId for each zone based on its zone color.
+  private getColorId_(zoneIdx: number, zoneColors: BacklightColor[]): string
+      |null {
+    if (!zoneColors) {
+      return null;
+    }
+    const zoneColor = zoneColors[zoneIdx];
+    if (zoneColor === BacklightColor.kRainbow) {
+      return RAINBOW;
+    }
+    // BacklightColor value matches with the index of staticColorIds.
+    // Ex: zoneColor value is BacklightColor.kGreen or 4, corresponding to
+    // staticColorIds[4] which is GREEN.
+    return staticColorIds[zoneColor];
+  }
+
   private closeZoneCustomizationDialog_() {
     this.$.dialog.close();
   }
diff --git a/ash/webui/personalization_app/resources/js/load_time_booleans.ts b/ash/webui/personalization_app/resources/js/load_time_booleans.ts
index 37ac306b..d0328d2a 100644
--- a/ash/webui/personalization_app/resources/js/load_time_booleans.ts
+++ b/ash/webui/personalization_app/resources/js/load_time_booleans.ts
@@ -40,5 +40,5 @@
 }
 
 export function isMultiZoneRgbKeyboardSupported() {
-  return loadTimeData.getBoolean('isMultiZoneRgbKeyboardSupported');
+  return loadTimeData.getInteger('keyboardBacklightZoneCount') > 1;
 }
diff --git a/ash/webui/personalization_app/resources/js/personalization_app.ts b/ash/webui/personalization_app/resources/js/personalization_app.ts
index dccd7fc..1bed516 100644
--- a/ash/webui/personalization_app/resources/js/personalization_app.ts
+++ b/ash/webui/personalization_app/resources/js/personalization_app.ts
@@ -22,6 +22,7 @@
 import './ambient/topic_source_item_element.js';
 import './ambient/topic_source_list_element.js';
 import './ambient/zero_state_element.js';
+import './keyboard_backlight/color_icon_element.js';
 import './keyboard_backlight/keyboard_backlight_element.js';
 import './keyboard_backlight/zone_customization_element.js';
 import './personalization_router_element.js';
@@ -67,7 +68,8 @@
 export {TopicSourceItem} from './ambient/topic_source_item_element.js';
 export {TopicSourceList} from './ambient/topic_source_list_element.js';
 export {AmbientZeroState} from './ambient/zero_state_element.js';
-export {KeyboardBacklightActionName, KeyboardBacklightActions, SetBacklightColorAction, setBacklightColorAction, SetShouldShowNudgeAction, setShouldShowNudgeAction, SetWallpaperColorAction, setWallpaperColorAction} from './keyboard_backlight/keyboard_backlight_actions.js';
+export {ColorIcon} from './keyboard_backlight/color_icon_element.js';
+export {KeyboardBacklightActionName, KeyboardBacklightActions, SetCurrentBacklightStateAction, setCurrentBacklightStateAction, SetShouldShowNudgeAction, setShouldShowNudgeAction, SetWallpaperColorAction, setWallpaperColorAction} from './keyboard_backlight/keyboard_backlight_actions.js';
 export {KeyboardBacklight} from './keyboard_backlight/keyboard_backlight_element.js';
 export {setKeyboardBacklightProviderForTesting} from './keyboard_backlight/keyboard_backlight_interface_provider.js';
 export {KeyboardBacklightObserver} from './keyboard_backlight/keyboard_backlight_observer.js';
@@ -96,7 +98,7 @@
 export {UserPreview} from './user/user_preview_element.js';
 export {UserSubpage} from './user/user_subpage_element.js';
 export {GetUserMediaProxy, getWebcamUtils, setWebcamUtilsForTesting} from './user/webcam_utils_proxy.js';
-export {getCountText, getNumberOfGridItemsPerRow, getSanitizedDefaultImageUrl} from './utils.js';
+export {getCountText, getNumberOfGridItemsPerRow, getSanitizedDefaultImageUrl, staticColorIds} from './utils.js';
 export {DefaultImageSymbol, DisplayableImage, kDefaultImageSymbol, kMaximumLocalImagePreviews} from './wallpaper/constants.js';
 export {GooglePhotosAlbums} from './wallpaper/google_photos_albums_element.js';
 export {GooglePhotosCollection} from './wallpaper/google_photos_collection_element.js';
diff --git a/ash/webui/personalization_app/resources/js/utils.ts b/ash/webui/personalization_app/resources/js/utils.ts
index a3320a0..2311b339 100644
--- a/ash/webui/personalization_app/resources/js/utils.ts
+++ b/ash/webui/personalization_app/resources/js/utils.ts
@@ -10,9 +10,27 @@
 import {String16} from 'chrome://resources/mojo/mojo/public/mojom/base/string16.mojom-webui.js';
 import {Url} from 'chrome://resources/mojo/url/mojom/url.mojom-webui.js';
 
-import {AmbientModeAlbum, GooglePhotosAlbum} from './../personalization_app.mojom-webui.js';
+import {AmbientModeAlbum, BacklightColor, BLUE_COLOR, GooglePhotosAlbum, GREEN_COLOR, INDIGO_COLOR, PURPLE_COLOR, RED_COLOR, WHITE_COLOR, YELLOW_COLOR} from './../personalization_app.mojom-webui.js';
 import {isPersonalizationJellyEnabled} from './load_time_booleans.js';
 
+export interface ColorInfo {
+  hexVal: string;
+  enumVal: BacklightColor;
+}
+
+export const WALLPAPER: string = 'wallpaper';
+export const WHITE: string = 'whiteColor';
+export const RED: string = 'redColor';
+export const YELLOW: string = 'yellowColor';
+export const GREEN: string = 'greenColor';
+export const BLUE: string = 'blueColor';
+export const INDIGO: string = 'indigoColor';
+export const PURPLE: string = 'purpleColor';
+export const RAINBOW: string = 'rainbow';
+
+export const staticColorIds =
+    [WALLPAPER, WHITE, RED, YELLOW, GREEN, BLUE, INDIGO, PURPLE];
+
 export type PersonalizationAppSelectionEvent =
     MouseEvent&{type: 'click'}|KeyboardEvent&{key: 'Enter'};
 
@@ -103,6 +121,43 @@
 }
 
 /**
+ * Returns the mapping of preset colors to their hex value and enum value in
+ * BacklightColor.
+ */
+export function getPresetColors(): Record<string, ColorInfo> {
+  return {
+    [WHITE]: {
+      hexVal: convertToRgbHexStr(WHITE_COLOR),
+      enumVal: BacklightColor.kWhite,
+    },
+    [RED]: {
+      hexVal: convertToRgbHexStr(RED_COLOR),
+      enumVal: BacklightColor.kRed,
+    },
+    [YELLOW]: {
+      hexVal: convertToRgbHexStr(YELLOW_COLOR),
+      enumVal: BacklightColor.kYellow,
+    },
+    [GREEN]: {
+      hexVal: convertToRgbHexStr(GREEN_COLOR),
+      enumVal: BacklightColor.kGreen,
+    },
+    [BLUE]: {
+      hexVal: convertToRgbHexStr(BLUE_COLOR),
+      enumVal: BacklightColor.kBlue,
+    },
+    [INDIGO]: {
+      hexVal: convertToRgbHexStr(INDIGO_COLOR),
+      enumVal: BacklightColor.kIndigo,
+    },
+    [PURPLE]: {
+      hexVal: convertToRgbHexStr(PURPLE_COLOR),
+      enumVal: BacklightColor.kPurple,
+    },
+  };
+}
+
+/**
  * Returns whether the given album is Recent Highlights.
  */
 export function isRecentHighlightsAlbum(album: AmbientModeAlbum|
diff --git a/ash/webui/shortcut_customization_ui/BUILD.gn b/ash/webui/shortcut_customization_ui/BUILD.gn
index 2bb5cbef..41f2647 100644
--- a/ash/webui/shortcut_customization_ui/BUILD.gn
+++ b/ash/webui/shortcut_customization_ui/BUILD.gn
@@ -33,3 +33,39 @@
     "//ui/webui",
   ]
 }
+
+source_set("unit_tests") {
+  testonly = true
+
+  sources = [
+    "backend/accelerator_configuration_provider_unittest.cc",
+    "backend/search/search_concept_registry_unittest.cc",
+    "backend/search/search_handler_unittest.cc",
+    "shortcuts_app_manager_unittest.cc",
+  ]
+
+  deps = [
+    ":shortcut_customization_ui",
+    "//ash",
+    "//ash:test_support",
+    "//ash/public/cpp",
+    "//ash/public/mojom",
+    "//ash/webui/shortcut_customization_ui/backend",
+    "//ash/webui/shortcut_customization_ui/backend/search:mojo_bindings",
+    "//ash/webui/shortcut_customization_ui/mojom",
+    "//base/test:test_support",
+    "//chromeos/ash/components:test_support",
+    "//chromeos/ash/components/local_search_service/public/cpp:cpp",
+    "//chromeos/ash/components/local_search_service/public/mojom",
+    "//chromeos/ash/components/local_search_service/public/mojom:mojom",
+    "//content/test:test_support",
+    "//device/udev_linux:test_support",
+    "//mojo/public/cpp/bindings:bindings",
+    "//testing/gtest",
+    "//ui/base:test_support",
+    "//ui/base/ime/ash",
+    "//ui/chromeos/events",
+    "//ui/events/devices",
+    "//ui/events/devices:test_support",
+  ]
+}
diff --git a/ash/webui/shortcut_customization_ui/backend/BUILD.gn b/ash/webui/shortcut_customization_ui/backend/BUILD.gn
index a64096f..ed8960a 100644
--- a/ash/webui/shortcut_customization_ui/backend/BUILD.gn
+++ b/ash/webui/shortcut_customization_ui/backend/BUILD.gn
@@ -38,37 +38,3 @@
     "//ui/events/devices",
   ]
 }
-
-source_set("unit_tests") {
-  testonly = true
-
-  sources = [
-    "accelerator_configuration_provider_unittest.cc",
-    "search/search_concept_registry_unittest.cc",
-    "search/search_handler_unittest.cc",
-  ]
-
-  deps = [
-    ":backend",
-    "//ash",
-    "//ash:test_support",
-    "//ash/public/cpp",
-    "//ash/public/mojom",
-    "//ash/webui/shortcut_customization_ui/backend/search:mojo_bindings",
-    "//ash/webui/shortcut_customization_ui/mojom",
-    "//base/test:test_support",
-    "//chromeos/ash/components:test_support",
-    "//chromeos/ash/components/local_search_service/public/cpp:cpp",
-    "//chromeos/ash/components/local_search_service/public/mojom",
-    "//chromeos/ash/components/local_search_service/public/mojom:mojom",
-    "//content/test:test_support",
-    "//device/udev_linux:test_support",
-    "//mojo/public/cpp/bindings:bindings",
-    "//testing/gtest",
-    "//ui/base:test_support",
-    "//ui/base/ime/ash",
-    "//ui/chromeos/events",
-    "//ui/events/devices",
-    "//ui/events/devices:test_support",
-  ]
-}
diff --git a/ash/webui/shortcut_customization_ui/backend/search/fake_search_data.h b/ash/webui/shortcut_customization_ui/backend/search/fake_search_data.h
index 386b79a9..83d41b8 100644
--- a/ash/webui/shortcut_customization_ui/backend/search/fake_search_data.h
+++ b/ash/webui/shortcut_customization_ui/backend/search/fake_search_data.h
@@ -14,7 +14,7 @@
 
 namespace ash::shortcut_ui::fake_search_data {
 
-enum FakeActionIds { kAction1 = 1, kAction2 = 2 };
+enum FakeActionIds { kAction1 = 1, kAction2 = 2, kAction3 = 3 };
 
 ash::mojom::AcceleratorInfoPtr CreateFakeStandardAcceleratorInfo();
 
diff --git a/ash/webui/shortcut_customization_ui/backend/search/search_concept_registry.cc b/ash/webui/shortcut_customization_ui/backend/search/search_concept_registry.cc
index 53f765e..54c4f11 100644
--- a/ash/webui/shortcut_customization_ui/backend/search/search_concept_registry.cc
+++ b/ash/webui/shortcut_customization_ui/backend/search/search_concept_registry.cc
@@ -26,54 +26,6 @@
 
 namespace ash::shortcut_ui {
 
-namespace {
-
-// Given text accelerator properties, return a string that will be searchable
-// by the Local Search Service.
-std::u16string TextAcceleratorToContentString(
-    const mojom::TextAcceleratorPropertiesPtr& text_accelerator_properties) {
-  // To get the searchable part of a Text Accelerator, combine all of the
-  // TextAcceleratorParts into one string.
-  std::u16string output;
-  for (const auto& part : text_accelerator_properties->parts) {
-    base::StrAppend(&output, {part->text});
-  }
-  return output;
-}
-
-// Given text accelerator properties, return an ID that will be used for the
-// Local Search Service Content object, which represents a searchable piece of
-// data.
-std::string StandardAcceleratorToContentId(
-    const mojom::StandardAcceleratorPropertiesPtr&
-        standard_accelerator_properties) {
-  // ID strings only need to be unique within a given SearchConcept,
-  // so it's sufficient to create an ID from the accelerator modifiers and
-  // key_code.
-  return base::StrCat(
-      {base::NumberToString(
-           standard_accelerator_properties->accelerator.modifiers()),
-       "-",
-       base::NumberToString(
-           standard_accelerator_properties->accelerator.key_code())});
-}
-
-// Given standard accelerator properties, return a string that will be
-// searchable by the Local Search Service.
-std::u16string StandardAcceleratorToContentString(
-    const mojom::StandardAcceleratorPropertiesPtr&
-        standard_accelerator_properties) {
-  // TODO(cambickel) GetShortcutText outputs a shortcut with "+" as the
-  // delimiter, e.g. "Ctrl+Shift+Q". We want to delimit with spaces, e.g. "Ctrl
-  // Shift Q". This has a fair amount of edge cases so we'll handle this later.
-  // TODO(cambickel) GetShortcutText also doesn't properly handle certain
-  // "special" keys, e.g. BrightnessDown. For now, we'll use it, but we should
-  // eventually switch to a more robust solution.
-  return standard_accelerator_properties->accelerator.GetShortcutText();
-}
-
-}  // namespace
-
 SearchConceptRegistry::SearchConceptRegistry(
     local_search_service::LocalSearchServiceProxy& local_search_service_proxy) {
   local_search_service_proxy.GetIndex(
@@ -149,9 +101,8 @@
   // queries.
   std::vector<local_search_service::Content> local_search_service_contents;
   // Reserve the size for the vector since we insert once for the description
-  // and once for each AcceleratorInfo.
-  local_search_service_contents.reserve(
-      search_concept.accelerator_infos.size() + 1);
+  // and at most once more in the case of text accelerators.
+  local_search_service_contents.reserve(2);
 
   // First, add this SearchConcept's description as searchable Content.
   // Note that the Content ID is prefixed with the SearchConcept ID, since
@@ -162,30 +113,25 @@
       /*id=*/base::StrCat({search_concept.id, "-description"}),
       /*content=*/search_concept.accelerator_layout_info->description);
 
-  // Next, for each accelerator_info, register it (and its accelerators in the
-  // case of standard accelerators) as searchable Content.
-  for (const auto& accelerator_info : search_concept.accelerator_infos) {
+  // Only text accelerators should become searchable LSS Content.
+  DCHECK(search_concept.accelerator_infos.size() > 0);
+  // Text accelerators should only have one entry in accelerator_infos.
+  const mojom::AcceleratorInfoPtr& first_accelerator_info =
+      search_concept.accelerator_infos.at(0);
+
+  if (first_accelerator_info->layout_properties->is_text_accelerator()) {
     // Content->id needs to be unique across the entire index,
     // so we prefix it with the SearchConcept's id.
-    // The part of the id besides the SearchConcept's id only
-    // needs to be unique within that SearchConcept's accelerators.
-    std::string content_id;
-    std::u16string content_string;
+    std::string content_id =
+        base::StrCat({search_concept.id, "-text-accelerator"});
 
-    if (accelerator_info->layout_properties->is_text_accelerator()) {
-      // The content_id for a text accelerator doesn't need to be based on the
-      // content of the text accelerator because text accelerators have only
-      // one entry in SearchConcept.accelerator_infos.
-      content_id = base::StrCat({search_concept.id, "-text-accelerator"});
-      content_string = TextAcceleratorToContentString(
-          accelerator_info->layout_properties->get_text_accelerator());
-    } else {
-      content_id = base::StrCat(
-          {search_concept.id, "-",
-           StandardAcceleratorToContentId(accelerator_info->layout_properties
-                                              ->get_standard_accelerator())});
-      content_string = StandardAcceleratorToContentString(
-          accelerator_info->layout_properties->get_standard_accelerator());
+    // To get the searchable part of a Text Accelerator, combine all of the
+    // TextAcceleratorParts into one string.
+    std::u16string content_string;
+    for (const auto& part :
+         first_accelerator_info->layout_properties->get_text_accelerator()
+             ->parts) {
+      base::StrAppend(&content_string, {part->text});
     }
 
     local_search_service_contents.emplace_back(
diff --git a/ash/webui/shortcut_customization_ui/backend/search/search_concept_registry.h b/ash/webui/shortcut_customization_ui/backend/search/search_concept_registry.h
index 246052a..bece3977 100644
--- a/ash/webui/shortcut_customization_ui/backend/search/search_concept_registry.h
+++ b/ash/webui/shortcut_customization_ui/backend/search/search_concept_registry.h
@@ -66,6 +66,7 @@
                            SearchConceptToDataStandardAccelerator);
   FRIEND_TEST_ALL_PREFIXES(SearchConceptRegistryTest,
                            SearchConceptToDataTextAccelerator);
+  FRIEND_TEST_ALL_PREFIXES(ShortcutsAppManagerTest, SetSearchConcepts);
 
   void NotifyRegistryUpdated();
 
diff --git a/ash/webui/shortcut_customization_ui/backend/search/search_concept_registry_unittest.cc b/ash/webui/shortcut_customization_ui/backend/search/search_concept_registry_unittest.cc
index 1480316..79761f6 100644
--- a/ash/webui/shortcut_customization_ui/backend/search/search_concept_registry_unittest.cc
+++ b/ash/webui/shortcut_customization_ui/backend/search/search_concept_registry_unittest.cc
@@ -219,28 +219,11 @@
 
   // The overall data ID should be source + action.
   EXPECT_EQ(data.id, "0-1");
-  // There should be three entries: one for the description, and two more total
-  // (one for each of the AcceleratorInfos).
-  EXPECT_EQ(data.contents.size(), 3u);
+  // There should be only one contents entry for the description.
+  EXPECT_EQ(data.contents.size(), 1u);
   // The first entry will always be the description of the SearchConcept.
   EXPECT_EQ(data.contents[0].id, "0-1-description");
   EXPECT_EQ(data.contents[0].content, u"Open the Foobar");
-  // The second entry in this case will be the first accelerator info's
-  // accelerator. The "0-1" in the id is the SearchConcept's id, the "6" is the
-  // modifier, and the "65" is the key_code.
-  EXPECT_EQ(data.contents[1].id, "0-1-6-65");
-  // TODO(cambickel): When we update the Content string here to be delimited by
-  // spaces instead of pluses, update this test.
-  EXPECT_EQ(data.contents[1].content, u"Ctrl+Shift+A");
-  // The third entry in this case will be the second accelerator info's
-  // accelerator. The "0-1" in the id is the SearchConcept's id, the "8" is the
-  // modifier, and the "216" is the key_code.
-  EXPECT_EQ(data.contents[2].id, "0-1-8-216");
-  // TODO(cambickel): When we update the Content string here to be delimited by
-  // spaces instead of pluses, update this test. Also, "BrightnessDown" and
-  // other top-row keys are not currently handled correctly, so when we fix
-  // that, update this test accordingly.
-  EXPECT_EQ(data.contents[2].content, u"Alt+");
 }
 
 TEST_F(SearchConceptRegistryTest, SearchConceptToDataTextAccelerator) {
diff --git a/ash/webui/shortcut_customization_ui/shortcuts_app_manager.cc b/ash/webui/shortcut_customization_ui/shortcuts_app_manager.cc
index 0f397508..0e40857 100644
--- a/ash/webui/shortcut_customization_ui/shortcuts_app_manager.cc
+++ b/ash/webui/shortcut_customization_ui/shortcuts_app_manager.cc
@@ -7,7 +7,10 @@
 #include <memory>
 
 #include "ash/constants/ash_features.h"
+#include "ash/public/mojom/accelerator_info.mojom-forward.h"
+#include "ash/public/mojom/accelerator_info.mojom.h"
 #include "ash/webui/shortcut_customization_ui/backend/accelerator_configuration_provider.h"
+#include "ash/webui/shortcut_customization_ui/backend/search/search_concept.h"
 #include "ash/webui/shortcut_customization_ui/backend/search/search_concept_registry.h"
 #include "ash/webui/shortcut_customization_ui/backend/search/search_handler.h"
 #include "base/feature_list.h"
@@ -16,9 +19,6 @@
 
 namespace ash::shortcut_ui {
 
-// TODO(cambickel): Update this constructor to fetch the list of accelerators
-// and use them to call SearchConceptRegistry.AddSearchConcepts, to populate
-// the LSS index.
 ShortcutsAppManager::ShortcutsAppManager(
     local_search_service::LocalSearchServiceProxy* local_search_service_proxy) {
   if (features::IsSearchInShortcutsAppEnabled()) {
@@ -66,8 +66,25 @@
     shortcut_ui::AcceleratorConfigurationProvider::AcceleratorConfigurationMap
         config,
     std::vector<mojom::AcceleratorLayoutInfoPtr> layout_infos) {
-  // TODO(cambickel): Use accelerators to create search concepts and add them to
-  // the search concept registry.
+  if (!features::IsSearchInShortcutsAppEnabled()) {
+    return;
+  }
+
+  std::vector<SearchConcept> search_concepts;
+
+  for (auto& layout_info : layout_infos) {
+    if (const auto& config_iterator = config.find(layout_info->source);
+        config_iterator != config.end()) {
+      if (const auto& map_iterator =
+              config_iterator->second.find(layout_info->action);
+          map_iterator != config_iterator->second.end()) {
+        search_concepts.emplace_back(std::move(layout_info),
+                                     std::move(map_iterator->second));
+      }
+    }
+  }
+
+  search_concept_registry_->SetSearchConcepts(std::move(search_concepts));
 }
 
 }  // namespace ash::shortcut_ui
diff --git a/ash/webui/shortcut_customization_ui/shortcuts_app_manager.h b/ash/webui/shortcut_customization_ui/shortcuts_app_manager.h
index cc3b2d2df..9496a3d9 100644
--- a/ash/webui/shortcut_customization_ui/shortcuts_app_manager.h
+++ b/ash/webui/shortcut_customization_ui/shortcuts_app_manager.h
@@ -5,12 +5,14 @@
 #ifndef ASH_WEBUI_SHORTCUT_CUSTOMIZATION_UI_SHORTCUTS_APP_MANAGER_H_
 #define ASH_WEBUI_SHORTCUT_CUSTOMIZATION_UI_SHORTCUTS_APP_MANAGER_H_
 
+#include <memory>
+
+#include "ash/public/mojom/accelerator_info.mojom-forward.h"
 #include "ash/webui/shortcut_customization_ui/backend/accelerator_configuration_provider.h"
 #include "ash/webui/shortcut_customization_ui/mojom/shortcut_customization.mojom.h"
+#include "base/gtest_prod_util.h"
 #include "components/keyed_service/core/keyed_service.h"
 
-#include <memory>
-
 namespace ash {
 
 namespace local_search_service {
@@ -53,6 +55,7 @@
           configs) override;
 
  private:
+  FRIEND_TEST_ALL_PREFIXES(ShortcutsAppManagerTest, SetSearchConcepts);
   // KeyedService:
   void Shutdown() override;
 
diff --git a/ash/webui/shortcut_customization_ui/shortcuts_app_manager_unittest.cc b/ash/webui/shortcut_customization_ui/shortcuts_app_manager_unittest.cc
new file mode 100644
index 0000000..bc9129c4
--- /dev/null
+++ b/ash/webui/shortcut_customization_ui/shortcuts_app_manager_unittest.cc
@@ -0,0 +1,148 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/webui/shortcut_customization_ui/shortcuts_app_manager.h"
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+#include "ash/constants/ash_features.h"
+#include "ash/public/cpp/accelerator_configuration.h"
+#include "ash/public/mojom/accelerator_info.mojom-shared.h"
+#include "ash/public/mojom/accelerator_info.mojom.h"
+#include "ash/shell.h"
+#include "ash/test/ash_test_base.h"
+#include "ash/webui/shortcut_customization_ui/backend/accelerator_configuration_provider.h"
+#include "ash/webui/shortcut_customization_ui/backend/search/fake_search_data.h"
+#include "ash/webui/shortcut_customization_ui/backend/search/search_concept_registry.h"
+#include "ash/webui/shortcut_customization_ui/shortcuts_app_manager.h"
+#include "base/containers/flat_map.h"
+#include "base/run_loop.h"
+#include "base/test/scoped_feature_list.h"
+#include "chromeos/ash/components/local_search_service/public/cpp/local_search_service_proxy.h"
+#include "chromeos/ash/components/test/ash_test_suite.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/base/resource/resource_bundle.h"
+
+namespace ash::shortcut_ui {
+
+class ShortcutsAppManagerTest : public AshTestBase {
+ protected:
+  ShortcutsAppManagerTest() = default;
+  ~ShortcutsAppManagerTest() override = default;
+
+  // AshTestBase:
+  void SetUp() override {
+    scoped_feature_list_.InitWithFeatures({features::kSearchInShortcutsApp},
+                                          {});
+    ui::ResourceBundle::CleanupSharedInstance();
+    AshTestSuite::LoadTestResources();
+    AshTestBase::SetUp();
+
+    local_search_service_proxy_ =
+        std::make_unique<local_search_service::LocalSearchServiceProxy>(
+            /*for_testing=*/true);
+
+    manager_ = std::make_unique<ShortcutsAppManager>(
+        local_search_service_proxy_.get());
+
+    // Let the RunLoop so that the AshAcceleratorConfiguration can load its
+    // default accelerators.
+    base::RunLoop().RunUntilIdle();
+  }
+
+  void TearDown() override {
+    manager_.reset();
+    AshTestBase::TearDown();
+  }
+
+  void ValidateSearchConceptById(
+      const base::flat_map<std::string, SearchConcept>& search_concepts_map,
+      const std::string search_concept_id,
+      const mojom::AcceleratorSource expected_source,
+      const uint32_t expected_action) const {
+    EXPECT_TRUE(search_concepts_map.contains(search_concept_id));
+    EXPECT_EQ(search_concepts_map.at(search_concept_id)
+                  .accelerator_layout_info->source,
+              expected_source);
+    EXPECT_EQ(search_concepts_map.at(search_concept_id)
+                  .accelerator_layout_info->action,
+              expected_action);
+  }
+
+  base::test::ScopedFeatureList scoped_feature_list_;
+  std::unique_ptr<local_search_service::LocalSearchServiceProxy>
+      local_search_service_proxy_;
+  std::unique_ptr<ShortcutsAppManager> manager_;
+};
+
+TEST_F(ShortcutsAppManagerTest, SetSearchConcepts) {
+  // Create all the fake AcceleratorInfo maps for use in the fake config.
+  AcceleratorConfigurationProvider::ActionIdToAcceleratorsInfoMap ash_info_map;
+  ash_info_map.insert({fake_search_data::FakeActionIds::kAction1,
+                       fake_search_data::CreateFakeAcceleratorInfoList()});
+
+  AcceleratorConfigurationProvider::ActionIdToAcceleratorsInfoMap
+      browser_info_map;
+  browser_info_map.insert({fake_search_data::FakeActionIds::kAction2,
+                           fake_search_data::CreateFakeAcceleratorInfoList()});
+  browser_info_map.insert({fake_search_data::FakeActionIds::kAction3,
+                           fake_search_data::CreateFakeAcceleratorInfoList()});
+
+  // Create the fake config.
+  shortcut_ui::AcceleratorConfigurationProvider::AcceleratorConfigurationMap
+      fake_config;
+  fake_config.insert({mojom::AcceleratorSource::kAsh, std::move(ash_info_map)});
+  fake_config.insert(
+      {mojom::AcceleratorSource::kBrowser, std::move(browser_info_map)});
+
+  // Create the fake AcceleratorLayoutInfos list.
+  std::vector<mojom::AcceleratorLayoutInfoPtr> fake_layout_infos;
+  fake_layout_infos.push_back(fake_search_data::CreateFakeAcceleratorLayoutInfo(
+      u"Open launcher", ash::mojom::AcceleratorSource::kAsh,
+      fake_search_data::FakeActionIds::kAction1,
+      ash::mojom::AcceleratorLayoutStyle::kDefault));
+  fake_layout_infos.push_back(fake_search_data::CreateFakeAcceleratorLayoutInfo(
+      u"Open new tab", ash::mojom::AcceleratorSource::kBrowser,
+      fake_search_data::FakeActionIds::kAction2,
+      ash::mojom::AcceleratorLayoutStyle::kDefault));
+  fake_layout_infos.push_back(fake_search_data::CreateFakeAcceleratorLayoutInfo(
+      u"Close tab", ash::mojom::AcceleratorSource::kBrowser,
+      fake_search_data::FakeActionIds::kAction3,
+      ash::mojom::AcceleratorLayoutStyle::kDefault));
+
+  auto& registry_search_concepts =
+      manager_->search_concept_registry_.get()->result_id_to_search_concept_;
+
+  // AshAcceleratorConfiguration loads some initial accelerators (which ends up
+  // populating the registry map), so clear the registry map to get a fresh
+  // slate for the test.
+  registry_search_concepts.clear();
+  EXPECT_EQ(registry_search_concepts.size(), 0u);
+
+  manager_->SetSearchConcepts(std::move(fake_config),
+                              std::move(fake_layout_infos));
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_EQ(registry_search_concepts.size(), 3u);
+  // Test that the expected search concepts are present and check a few
+  // attributes to be sure.
+  ValidateSearchConceptById(/*search_concepts_map=*/registry_search_concepts,
+                            /*search_concept_id=*/"0-1",
+                            /*expected_source=*/mojom::AcceleratorSource::kAsh,
+                            /*expected_action=*/fake_search_data::kAction1);
+  ValidateSearchConceptById(
+      /*search_concepts_map=*/registry_search_concepts,
+      /*search_concept_id=*/"2-2",
+      /*expected_source=*/mojom::AcceleratorSource::kBrowser,
+      /*expected_action=*/fake_search_data::kAction2);
+  ValidateSearchConceptById(
+      /*search_concepts_map=*/registry_search_concepts,
+      /*search_concept_id=*/"2-3",
+      /*expected_source=*/mojom::AcceleratorSource::kBrowser,
+      /*expected_action=*/fake_search_data::kAction3);
+}
+
+}  // namespace ash::shortcut_ui
\ No newline at end of file
diff --git a/ash/wm/desks/desks_unittests.cc b/ash/wm/desks/desks_unittests.cc
index c5412f6..ee4db71 100644
--- a/ash/wm/desks/desks_unittests.cc
+++ b/ash/wm/desks/desks_unittests.cc
@@ -4864,7 +4864,7 @@
                                     int* result) const override {
     return false;
   }
-  bool TopRowKeysAreFunctionKeys() const override { return false; }
+  bool TopRowKeysAreFunctionKeys(int device_id) const override { return false; }
   bool IsExtensionCommandRegistered(ui::KeyboardCode key_code,
                                     int flags) const override {
     return false;
diff --git a/ash/wm/float/float_controller.cc b/ash/wm/float/float_controller.cc
index de23f24..48e86ec 100644
--- a/ash/wm/float/float_controller.cc
+++ b/ash/wm/float/float_controller.cc
@@ -42,8 +42,6 @@
 #include "ui/display/screen.h"
 #include "ui/wm/core/coordinate_conversion.h"
 
-using MagnetismCorner = ash::FloatController::MagnetismCorner;
-
 namespace ash {
 
 namespace {
diff --git a/ash/wm/float/float_controller_unittest.cc b/ash/wm/float/float_controller_unittest.cc
index 623db6d..6ea5ee1 100644
--- a/ash/wm/float/float_controller_unittest.cc
+++ b/ash/wm/float/float_controller_unittest.cc
@@ -1686,11 +1686,11 @@
   ASSERT_TRUE(Shell::Get()->overview_controller()->InOverviewSession());
   ASSERT_TRUE(split_view_controller->InSplitViewMode());
 
-  // Float the window so we can snap it again. Assert that we are still in
-  // overview, but no longer in splitview.
+  // Float the window so we can snap it again. Assert that we are no longer in
+  // overview or splitview.
   PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
   ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
-  ASSERT_TRUE(Shell::Get()->overview_controller()->InOverviewSession());
+  ASSERT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
   ASSERT_FALSE(split_view_controller->InSplitViewMode());
 
   // Create a second window.
@@ -1704,6 +1704,14 @@
   EXPECT_TRUE(split_view_controller->BothSnapped());
   EXPECT_EQ(split_view_controller->primary_window(), window.get());
   EXPECT_EQ(split_view_controller->secondary_window(), other_window.get());
+
+  // Tests that in overview mode, with at least one app window in overview, that
+  // we also exit splitview and overview when floating the snapped window.
+  ToggleOverview();
+  wm::ActivateWindow(window.get());
+  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
+  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
+  EXPECT_FALSE(split_view_controller->InSplitViewMode());
 }
 
 // When reset a floated window that's previously snapped, maximize instead of
diff --git a/ash/wm/splitview/split_view_controller.cc b/ash/wm/splitview/split_view_controller.cc
index 1e65af9..2ab5d42 100644
--- a/ash/wm/splitview/split_view_controller.cc
+++ b/ash/wm/splitview/split_view_controller.cc
@@ -1781,19 +1781,16 @@
     if (do_divider_spawn_animation)
       DoSplitDividerSpawnAnimation(window);
   } else if (window_state->IsNormalStateType() || window_state->IsMaximized() ||
-             window_state->IsFullscreen()) {
+             window_state->IsFullscreen() || window_state->IsFloated()) {
     // End split view, and also overview if overview is active, in these cases:
     // 1. A left clamshell split view window gets unsnapped by Alt+[.
     // 2. A right clamshell split view window gets unsnapped by Alt+].
     // 3. A (clamshell or tablet) split view window gets maximized.
     // 4. A (clamshell or tablet) split view window becomes full screen.
+    // 5. A split view window becomes floated.
     EndSplitView();
     Shell::Get()->overview_controller()->EndOverview(
         OverviewEndAction::kSplitView);
-  } else if (window_state->IsFloated()) {
-    OnSnappedWindowDetached(window, WindowDetachedReason::kWindowFloated);
-
-    // TODO(crbug.com/1351562): Consider ending overview here.
   } else if (window_state->IsMinimized()) {
     OnSnappedWindowDetached(window, WindowDetachedReason::kWindowMinimized);
 
diff --git a/base/BUILD.gn b/base/BUILD.gn
index 60da4027..bd94a2c 100644
--- a/base/BUILD.gn
+++ b/base/BUILD.gn
@@ -933,6 +933,7 @@
     "types/pass_key.h",
     "types/strong_alias.h",
     "types/token_type.h",
+    "types/variant_util.h",
     "unguessable_token.cc",
     "unguessable_token.h",
     "value_iterators.cc",
@@ -3402,6 +3403,7 @@
     "types/pass_key_unittest.cc",
     "types/strong_alias_unittest.cc",
     "types/token_type_unittest.cc",
+    "types/variant_util_unittest.cc",
     "unguessable_token_unittest.cc",
     "value_iterators_unittest.cc",
     "values_unittest.cc",
@@ -3428,6 +3430,7 @@
     "//base/test:run_all_unittests",
     "//base/test:test_support",
     "//base/third_party/dynamic_annotations",
+    "//build:blink_buildflags",
     "//build:chromecast_buildflags",
     "//build:chromeos_buildflags",
     "//testing/gmock",
@@ -4036,6 +4039,7 @@
       "thread_annotations_unittest.nc",
       "traits_bag_unittest.nc",
       "types/pass_key_unittest.nc",
+      "types/variant_util_unittest.nc",
       "values_unittest.nc",
     ]
 
diff --git a/base/allocator/partition_allocator/shim/allocator_shim_unittest.cc b/base/allocator/partition_allocator/shim/allocator_shim_unittest.cc
index b68f6fe..626d098 100644
--- a/base/allocator/partition_allocator/shim/allocator_shim_unittest.cc
+++ b/base/allocator/partition_allocator/shim/allocator_shim_unittest.cc
@@ -19,7 +19,6 @@
 #include "base/memory/page_size.h"
 #include "base/synchronization/waitable_event.h"
 #include "base/threading/platform_thread.h"
-#include "base/threading/thread_local.h"
 #include "build/build_config.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -113,9 +112,9 @@
       // Hitting it for the first time will cause a failure, causing the
       // invocation of the std::new_handler.
       if (size == 0xFEED) {
-        if (!instance_->did_fail_realloc_0xfeed_once->Get()) {
-          instance_->did_fail_realloc_0xfeed_once->Set(
-              instance_->did_fail_realloc_0xfeed_once.get());
+        thread_local bool did_fail_realloc_0xfeed_once = false;
+        if (!did_fail_realloc_0xfeed_once) {
+          did_fail_realloc_0xfeed_once = true;
           return nullptr;
         }
         return address;
@@ -260,8 +259,6 @@
     aligned_reallocs_intercepted_by_size.resize(MaxSizeTracked());
     aligned_reallocs_intercepted_by_addr.resize(MaxSizeTracked());
     aligned_frees_intercepted_by_addr.resize(MaxSizeTracked());
-    did_fail_realloc_0xfeed_once =
-        std::make_unique<base::ThreadLocalStorage::Slot>();
     num_new_handler_calls.store(0, std::memory_order_release);
     instance_ = this;
 
@@ -303,7 +300,6 @@
   std::vector<size_t> aligned_reallocs_intercepted_by_size;
   std::vector<size_t> aligned_reallocs_intercepted_by_addr;
   std::vector<size_t> aligned_frees_intercepted_by_addr;
-  std::unique_ptr<base::ThreadLocalStorage::Slot> did_fail_realloc_0xfeed_once;
   std::atomic<uint32_t> num_new_handler_calls;
 
  private:
diff --git a/base/process/process_mac.cc b/base/process/process_mac.cc
index 066e4ce..b551ec2 100644
--- a/base/process/process_mac.cc
+++ b/base/process/process_mac.cc
@@ -18,15 +18,47 @@
 #include "base/feature_list.h"
 #include "base/mac/mach_logging.h"
 #include "base/memory/free_deleter.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace base {
 
+namespace {
+
 // Enables setting the task role of every child process to
 // TASK_DEFAULT_APPLICATION.
 BASE_FEATURE(kMacSetDefaultTaskRole,
              "MacSetDefaultTaskRole",
              FEATURE_DISABLED_BY_DEFAULT);
 
+// Returns the `task_role_t` of the process whose process ID is `pid`.
+absl::optional<task_role_t> GetTaskCategoryPolicyRole(
+    PortProvider* port_provider,
+    ProcessId pid) {
+  DCHECK(port_provider);
+
+  mach_port_t task_port = port_provider->TaskForPid(pid);
+  if (task_port == TASK_NULL) {
+    return absl::nullopt;
+  }
+
+  task_category_policy_data_t category_policy;
+  mach_msg_type_number_t task_info_count = TASK_CATEGORY_POLICY_COUNT;
+  boolean_t get_default = FALSE;
+
+  kern_return_t result =
+      task_policy_get(task_port, TASK_CATEGORY_POLICY,
+                      reinterpret_cast<task_policy_t>(&category_policy),
+                      &task_info_count, &get_default);
+  if (result != KERN_SUCCESS) {
+    MACH_LOG(ERROR, result) << "task_policy_get TASK_CATEGORY_POLICY";
+    return absl::nullopt;
+  }
+  DCHECK(!get_default);
+  return category_policy.role;
+}
+
+}  // namespace
+
 Time Process::CreationTime() const {
   int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, Pid()};
   size_t len = 0;
@@ -48,25 +80,12 @@
   DCHECK(IsValid());
   DCHECK(port_provider);
 
-  mach_port_t task_port = port_provider->TaskForPid(Pid());
-  if (task_port == TASK_NULL)
-    return false;
-
-  task_category_policy_data_t category_policy;
-  mach_msg_type_number_t task_info_count = TASK_CATEGORY_POLICY_COUNT;
-  boolean_t get_default = FALSE;
-
-  kern_return_t result =
-      task_policy_get(task_port, TASK_CATEGORY_POLICY,
-                      reinterpret_cast<task_policy_t>(&category_policy),
-                      &task_info_count, &get_default);
-  MACH_LOG_IF(ERROR, result != KERN_SUCCESS, result)
-      << "task_policy_get TASK_CATEGORY_POLICY";
-
-  if (result == KERN_SUCCESS && get_default == FALSE) {
-    return category_policy.role == TASK_BACKGROUND_APPLICATION;
-  }
-  return false;
+  // A process is backgrounded if the role is explicitly
+  // TASK_BACKGROUND_APPLICATION (as opposed to not being
+  // TASK_FOREGROUND_APPLICATION).
+  absl::optional<task_role_t> task_role =
+      GetTaskCategoryPolicyRole(port_provider, Pid());
+  return task_role && *task_role == TASK_BACKGROUND_APPLICATION;
 }
 
 bool Process::SetProcessBackgrounded(PortProvider* port_provider,
@@ -82,8 +101,16 @@
   if (task_port == TASK_NULL)
     return false;
 
-  if (IsProcessBackgrounded(port_provider) == background)
+  absl::optional<task_role_t> current_role =
+      GetTaskCategoryPolicyRole(port_provider, Pid());
+  if (!current_role) {
+    return false;
+  }
+
+  if ((background && *current_role == TASK_BACKGROUND_APPLICATION) ||
+      (!background && *current_role == TASK_FOREGROUND_APPLICATION)) {
     return true;
+  }
 
   task_category_policy category_policy;
   category_policy.role =
diff --git a/base/threading/platform_thread_unittest.cc b/base/threading/platform_thread_unittest.cc
index a4e34ee..5e08929 100644
--- a/base/threading/platform_thread_unittest.cc
+++ b/base/threading/platform_thread_unittest.cc
@@ -12,6 +12,7 @@
 #include "base/test/scoped_feature_list.h"
 #include "base/threading/thread.h"
 #include "base/threading/threading_features.h"
+#include "build/blink_buildflags.h"
 #include "build/build_config.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -539,7 +540,9 @@
 
 TEST(PlatformThreadTest, GetDefaultThreadStackSize) {
   size_t stack_size = PlatformThread::GetDefaultThreadStackSize();
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS) || BUILDFLAG(IS_FUCHSIA) ||        \
+#if BUILDFLAG(IS_IOS) && BUILDFLAG(USE_BLINK)
+  EXPECT_EQ(1024u * 1024u, stack_size);
+#elif BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS) || BUILDFLAG(IS_FUCHSIA) ||      \
     ((BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)) && defined(__GLIBC__) && \
      !defined(THREAD_SANITIZER)) ||                                           \
     (BUILDFLAG(IS_ANDROID) && !defined(ADDRESS_SANITIZER))
diff --git a/base/types/variant_util.h b/base/types/variant_util.h
new file mode 100644
index 0000000..3a40f46
--- /dev/null
+++ b/base/types/variant_util.h
@@ -0,0 +1,52 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BASE_TYPES_VARIANT_UTIL_H_
+#define BASE_TYPES_VARIANT_UTIL_H_
+
+#include <stddef.h>
+
+#include <type_traits>
+
+#include "base/types/always_false.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
+
+namespace base {
+namespace internal {
+
+template <typename Variant, typename T>
+struct VariantIndexOfTypeHelper {
+  static_assert(AlwaysFalse<Variant>, "Variant must be an absl::variant<...>");
+};
+
+template <typename... Ts, typename T>
+struct VariantIndexOfTypeHelper<absl::variant<Ts...>, T> {
+  static constexpr size_t Index() {
+    static_assert(std::is_constructible_v<absl::variant<LiteralType<Ts>...>,
+                                          LiteralType<T>>,
+                  "Variant is not constructible from T");
+    return absl::variant<LiteralType<Ts>...>(LiteralType<T>()).index();
+  }
+
+  // Helper struct; even if `Tag` may not be usable as a literal type, a
+  // `LiteralType<Tag>` will be.
+  template <typename Tag>
+  struct LiteralType {};
+};
+
+}  // namespace internal
+
+// Returns the 0-based index of `T` in `Variant`'s list of alternative types,
+// e.g. given `Variant` == `absl::variant<A, B, C>` and `T` == `B`, returns 1.
+//
+// Note that this helper cannot be used if the list of alternative types
+// contains duplicates.
+template <typename Variant, typename T>
+constexpr size_t VariantIndexOfType() {
+  return internal::VariantIndexOfTypeHelper<Variant, T>::Index();
+}
+
+}  // namespace base
+
+#endif  // BASE_TYPES_VARIANT_UTIL_H_
diff --git a/base/types/variant_util_unittest.cc b/base/types/variant_util_unittest.cc
new file mode 100644
index 0000000..b421203
--- /dev/null
+++ b/base/types/variant_util_unittest.cc
@@ -0,0 +1,22 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/types/variant_util.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
+
+namespace base {
+namespace {
+
+TEST(VariantUtilTest, IndexOfType) {
+  using TestType = absl::variant<bool, int, double>;
+
+  static_assert(VariantIndexOfType<TestType, bool>() == 0);
+  static_assert(VariantIndexOfType<TestType, int>() == 1);
+  static_assert(VariantIndexOfType<TestType, double>() == 2);
+}
+
+}  // namespace
+}  // namespace base
diff --git a/base/types/variant_util_unittest.nc b/base/types/variant_util_unittest.nc
new file mode 100644
index 0000000..aec037b
--- /dev/null
+++ b/base/types/variant_util_unittest.nc
@@ -0,0 +1,23 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This is a "No Compile Test" suite.
+// http://dev.chromium.org/developers/testing/no-compile-tests
+
+#include "base/types/variant_util.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
+
+namespace base {
+
+#if defined(NCTEST_DUPLICATE_ALTERNATIVE_TYPES)  // [r"Variant is not constructible from T"]
+
+inline constexpr size_t kValue = VariantIndexOfType<absl::variant<int, int>, int>();
+
+#elif defined(NCTEST_NOT_AN_ALTERNATIVE_TYPE)  // [r"Variant is not constructible from T"]
+
+inline constexpr size_t kValue = VariantIndexOfType<absl::variant<int, int>, bool>();
+
+#endif
+
+}  // namespace base
diff --git a/build/fuchsia/linux_internal.sdk.sha1 b/build/fuchsia/linux_internal.sdk.sha1
index 461b3b5..acf709a 100644
--- a/build/fuchsia/linux_internal.sdk.sha1
+++ b/build/fuchsia/linux_internal.sdk.sha1
@@ -1 +1 @@
-12.20230309.2.1
+12.20230309.3.1
diff --git a/chrome/android/chrome_java_sources.gni b/chrome/android/chrome_java_sources.gni
index cd458e1..12d0f33 100644
--- a/chrome/android/chrome_java_sources.gni
+++ b/chrome/android/chrome_java_sources.gni
@@ -209,6 +209,7 @@
   "java/src/org/chromium/chrome/browser/bookmarks/PowerBookmarkUtils.java",
   "java/src/org/chromium/chrome/browser/bookmarks/ReadingListSectionHeader.java",
   "java/src/org/chromium/chrome/browser/bookmarks/TabBookmarker.java",
+  "java/src/org/chromium/chrome/browser/bookmarks/TestingDelegate.java",
   "java/src/org/chromium/chrome/browser/bookmarkswidget/BookmarkWidgetProvider.java",
   "java/src/org/chromium/chrome/browser/bookmarkswidget/BookmarkWidgetProxy.java",
   "java/src/org/chromium/chrome/browser/bookmarkswidget/BookmarkWidgetServiceImpl.java",
diff --git a/chrome/android/expectations/lint-baseline.xml b/chrome/android/expectations/lint-baseline.xml
index 8126df8..4202fa5 100644
--- a/chrome/android/expectations/lint-baseline.xml
+++ b/chrome/android/expectations/lint-baseline.xml
@@ -1977,39 +1977,6 @@
 
     <issue
         id="WrongConstant"
-        message="Must be one of: ViewType.INVALID, ViewType.PERSONALIZED_SIGNIN_PROMO, ViewType.PERSONALIZED_SYNC_PROMO, ViewType.SYNC_PROMO, ViewType.FOLDER, ViewType.BOOKMARK, ViewType.DIVIDER, ViewType.SECTION_HEADER, ViewType.SHOPPING_POWER_BOOKMARK, ViewType.TAG_CHIP_LIST"
-        errorLine1="            case ViewType.SHOPPING_FILTER:"
-        errorLine2="                          ~~~~~~~~~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemsAdapter.java"
-            line="254"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: ViewType.INVALID, ViewType.PERSONALIZED_SIGNIN_PROMO, ViewType.PERSONALIZED_SYNC_PROMO, ViewType.SYNC_PROMO, ViewType.FOLDER, ViewType.BOOKMARK, ViewType.DIVIDER, ViewType.SECTION_HEADER, ViewType.SHOPPING_POWER_BOOKMARK, ViewType.TAG_CHIP_LIST"
-        errorLine1="        } else if (holder.getItemViewType() == ViewType.SHOPPING_FILTER) {"
-        errorLine2="                                                        ~~~~~~~~~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemsAdapter.java"
-            line="296"
-            column="57"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: ViewType.INVALID, ViewType.PERSONALIZED_SIGNIN_PROMO, ViewType.PERSONALIZED_SYNC_PROMO, ViewType.SYNC_PROMO, ViewType.FOLDER, ViewType.BOOKMARK, ViewType.DIVIDER, ViewType.SECTION_HEADER, ViewType.SHOPPING_POWER_BOOKMARK, ViewType.TAG_CHIP_LIST"
-        errorLine1="                ViewType.SHOPPING_FILTER, /*bookmarkItem=*/null, /*sectionHeaderData=*/null);"
-        errorLine2="                         ~~~~~~~~~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkListEntry.java"
-            line="109"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
         message="Must be one of: CustomTabsUiType.DEFAULT, CustomTabsUiType.MEDIA_VIEWER, CustomTabsUiType.INFO_PAGE, CustomTabsUiType.READER_MODE, CustomTabsUiType.MINIMAL_UI_WEBAPP, CustomTabsUiType.OFFLINE_PAGE"
         errorLine1="        intent.putExtra(CustomTabIntentDataProvider.EXTRA_UI_TYPE, CustomTabsUiType.READ_LATER);"
         errorLine2="                                                                                    ~~~~~~~~~~">
@@ -3220,83 +3187,6 @@
 
     <issue
         id="WrongConstant"
-        message="Must be one of: ScrollDirection.LEFT, ScrollDirection.RIGHT, ScrollDirection.UP, ScrollDirection.DOWN"
-        errorLine1="            mScrollDirection = ScrollDirection.UNKNOWN;"
-        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/LayoutManagerChrome.java"
-            line="516"
-            column="32"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: ScrollDirection.LEFT, ScrollDirection.RIGHT, ScrollDirection.UP, ScrollDirection.DOWN"
-        errorLine1="            mScrollDirection = ScrollDirection.UNKNOWN;"
-        errorLine2="                                               ~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/LayoutManagerChrome.java"
-            line="516"
-            column="48"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: ScrollDirection.LEFT, ScrollDirection.RIGHT, ScrollDirection.UP, ScrollDirection.DOWN"
-        errorLine1="            if (mScrollDirection != ScrollDirection.UNKNOWN) {"
-        errorLine2="                                                    ~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/LayoutManagerChrome.java"
-            line="531"
-            column="53"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: ScrollDirection.LEFT, ScrollDirection.RIGHT, ScrollDirection.UP, ScrollDirection.DOWN"
-        errorLine1="            if (mScrollDirection == ScrollDirection.UNKNOWN) return;"
-        errorLine2="                                                    ~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/LayoutManagerChrome.java"
-            line="537"
-            column="53"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: ScrollDirection.LEFT, ScrollDirection.RIGHT, ScrollDirection.UP, ScrollDirection.DOWN"
-        errorLine1="            int direction = ScrollDirection.UNKNOWN;"
-        errorLine2="                            ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/LayoutManagerChrome.java"
-            line="578"
-            column="29"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: ScrollDirection.LEFT, ScrollDirection.RIGHT, ScrollDirection.UP, ScrollDirection.DOWN"
-        errorLine1="            int direction = ScrollDirection.UNKNOWN;"
-        errorLine2="                                            ~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/LayoutManagerChrome.java"
-            line="578"
-            column="45"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: ScrollDirection.LEFT, ScrollDirection.RIGHT, ScrollDirection.UP, ScrollDirection.DOWN"
-        errorLine1="                direction = ScrollDirection.LEFT;"
-        errorLine2="                ~~~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/LayoutManagerChrome.java"
-            line="584"
-            column="17"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
         message="Must be one of: LensSupportStatus.LENS_SEARCH_SUPPORTED, LensSupportStatus.NON_GOOGLE_SEARCH_ENGINE, LensSupportStatus.ACTIVITY_NOT_ACCESSIBLE, LensSupportStatus.OUT_OF_DATE, LensSupportStatus.SEARCH_BY_IMAGE_UNAVAILABLE, LensSupportStatus.LEGACY_OS, LensSupportStatus.INVALID_PACKAGE, LensSupportStatus.LENS_SHOP_SUPPORTED, LensSupportStatus.LENS_SHOP_AND_SEARCH_SUPPORTED, LensSupportStatus.CAMERA_NOT_AVAILABLE, LensSupportStatus.DISABLED_ON_LOW_END_DEVICE, LensSupportStatus.AGSA_VERSION_NOT_SUPPORTED, LensSupportStatus.DISABLED_ON_INCOGNITO, LensSupportStatus.DISABLED_ON_TABLET, LensSupportStatus.DISABLED_FOR_ENTERPRISE_USER"
         errorLine1="                histogramName, reason, LensSupportStatus.NUM_ENTRIES);"
         errorLine2="                                                         ~~~~~~~~~~~">
@@ -4144,28 +4034,6 @@
 
     <issue
         id="WrongConstant"
-        message="Must be one of: PriceTrackingState.PRICE_TRACKING_SHOWN, PriceTrackingState.PRICE_TRACKING_ENABLED, PriceTrackingState.PRICE_TRACKING_DISABLED"
-        errorLine1="                PriceTrackingState.COUNT);"
-        errorLine2="                                   ~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/bookmarks/PowerBookmarkMetrics.java"
-            line="29"
-            column="36"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: PriceTrackingState.PRICE_TRACKING_SHOWN, PriceTrackingState.PRICE_TRACKING_ENABLED, PriceTrackingState.PRICE_TRACKING_DISABLED"
-        errorLine1="                PriceTrackingState.COUNT);"
-        errorLine2="                                   ~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/bookmarks/PowerBookmarkMetrics.java"
-            line="37"
-            column="36"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
         message="Must be one of: ExitReason.REASON_ANR, ExitReason.REASON_CRASH, ExitReason.REASON_CRASH_NATIVE, ExitReason.REASON_DEPENDENCY_DIED, ExitReason.REASON_EXCESSIVE_RESOURCE_USAGE, ExitReason.REASON_EXIT_SELF, ExitReason.REASON_INITIALIZATION_FAILURE, ExitReason.REASON_LOW_MEMORY, ExitReason.REASON_OTHER, ExitReason.REASON_PERMISSION_CHANGE, ExitReason.REASON_SIGNALED, ExitReason.REASON_UNKNOWN, ExitReason.REASON_USER_REQUESTED, ExitReason.REASON_USER_STOPPED"
         errorLine1="        RecordHistogram.recordEnumeratedHistogram(umaName, reason, ExitReason.NUM_ENTRIES);"
         errorLine2="                                                                              ~~~~~~~~~~~">
@@ -6961,50 +6829,6 @@
     <issue
         id="NotifyDataSetChanged"
         message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort."
-        errorLine1="        notifyDataSetChanged();"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemsAdapter.java"
-            line="190"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="NotifyDataSetChanged"
-        message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort."
-        errorLine1="        notifyDataSetChanged();"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemsAdapter.java"
-            line="356"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="NotifyDataSetChanged"
-        message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort."
-        errorLine1="        notifyDataSetChanged();"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemsAdapter.java"
-            line="406"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="NotifyDataSetChanged"
-        message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort."
-        errorLine1="        if (mElements != null) notifyDataSetChanged();"
-        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="../../chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemsAdapter.java"
-            line="419"
-            column="32"/>
-    </issue>
-
-    <issue
-        id="NotifyDataSetChanged"
-        message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort."
         errorLine1="                    notifyDataSetChanged();"
         errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~">
         <location
diff --git a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/PriceAlertsMessageCardTest.java b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/PriceAlertsMessageCardTest.java
index 1650ba3..5584ae0 100644
--- a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/PriceAlertsMessageCardTest.java
+++ b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/PriceAlertsMessageCardTest.java
@@ -49,7 +49,6 @@
 import org.chromium.base.metrics.RecordHistogram;
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.CriteriaHelper;
-import org.chromium.base.test.util.DisabledTest;
 import org.chromium.base.test.util.Feature;
 import org.chromium.base.test.util.MinAndroidSdkLevel;
 import org.chromium.base.test.util.Restriction;
@@ -318,7 +317,6 @@
     @Test
     @MediumTest
     @CommandLineFlags.Add({BASE_PARAMS})
-    @DisabledTest(message = "https://crbug.com/1375813")
     public void testDisableMessageAfterShowingTenTimes() {
         final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
 
diff --git a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabGridIphTest.java b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabGridIphTest.java
index 92484e04..5900d65 100644
--- a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabGridIphTest.java
+++ b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabGridIphTest.java
@@ -125,7 +125,6 @@
 
     @Test
     @MediumTest
-    @DisabledTest(message = "crbug.com/1416886")
     public void testShowAndHideIphDialog() {
         final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
 
@@ -143,7 +142,7 @@
         verifyIphDialogHiding(cta);
 
         // Check the IPH message card is showing and open the IPH dialog.
-        onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed()));
+        onViewWaiting(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed()));
         onView(allOf(withId(R.id.action_button), withParent(withId(R.id.tab_grid_message_item))))
                 .perform(click());
         verifyIphDialogShowing(cta);
@@ -151,10 +150,10 @@
         // Press back should dismiss the IPH dialog.
         pressBack();
         verifyIphDialogHiding(cta);
-        onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed()));
+        onViewWaiting(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed()));
 
         // Check the IPH message card is showing and open the IPH dialog.
-        onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed()));
+        onViewWaiting(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed()));
         onView(allOf(withId(R.id.action_button), withParent(withId(R.id.tab_grid_message_item))))
                 .perform(click());
         verifyIphDialogShowing(cta);
@@ -213,13 +212,12 @@
         mActivityTestRule.startMainActivityFromLauncher();
         cta = mActivityTestRule.getActivity();
         enterTabSwitcher(cta);
-        onViewWaiting(withId(R.id.tab_grid_message_item)).check(doesNotExist());
+        onView(withId(R.id.tab_grid_message_item)).check(doesNotExist());
     }
 
     @Test
     @MediumTest
     @Feature({"RenderTest"})
-    @DisabledTest(message = "crbug.com/1412391")
     public void testRenderIph_Portrait() throws IOException {
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
 
@@ -228,6 +226,7 @@
                 TabSwitcherCoordinator::hasAppendedMessagesForTesting);
         onViewWaiting(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed()));
 
+        ChromeRenderTestRule.sanitize(cta.findViewById(R.id.tab_grid_message_item));
         mRenderTestRule.render(
                 cta.findViewById(R.id.tab_grid_message_item), "iph_entrance_portrait");
     }
@@ -245,8 +244,9 @@
         onViewWaiting(allOf(withParent(withId(TabUiTestHelper.getTabSwitcherParentId(cta))),
                               withId(R.id.tab_list_view)))
                 .perform(RecyclerViewActions.scrollTo(withId(R.id.tab_grid_message_item)));
-        onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed()));
+        onViewWaiting(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed()));
 
+        ChromeRenderTestRule.sanitize(cta.findViewById(R.id.tab_grid_message_item));
         mRenderTestRule.render(
                 cta.findViewById(R.id.tab_grid_message_item), "iph_entrance_landscape");
     }
@@ -272,6 +272,7 @@
         Animatable iphAnimation = (Animatable) iphImageView.getDrawable();
         CriteriaHelper.pollUiThread(() -> !iphAnimation.isRunning());
 
+        ChromeRenderTestRule.sanitize(iphDialogView);
         mRenderTestRule.render(iphDialogView, "iph_dialog_portrait");
     }
 
@@ -302,12 +303,12 @@
         Animatable iphAnimation = (Animatable) iphImageView.getDrawable();
         CriteriaHelper.pollUiThread(() -> !iphAnimation.isRunning());
 
+        ChromeRenderTestRule.sanitize(iphDialogView);
         mRenderTestRule.render(iphDialogView, "iph_dialog_landscape");
     }
 
     @Test
     @MediumTest
-    @DisabledTest(message = "https://crbug.com/1412947")
     public void testIphItemChangeWithLastTab() {
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
 
@@ -323,7 +324,7 @@
 
         // Undo the closure of the last tab and the IPH item should reshow.
         CriteriaHelper.pollInstrumentationThread(TabUiTestHelper::verifyUndoBarShowingAndClickUndo);
-        onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed()));
+        onViewWaiting(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed()));
 
         // Close the last tab in the tab switcher.
         closeFirstTabInTabSwitcher(cta);
@@ -383,7 +384,7 @@
 
     private void verifyIphDialogShowing(ChromeTabbedActivity cta) {
         // Verify IPH dialog view.
-        onView(withId(R.id.iph_dialog))
+        onViewWaiting(withId(R.id.iph_dialog))
                 .inRoot(withDecorView(not(cta.getWindow().getDecorView())))
                 .check((v, noMatchException) -> {
                     if (noMatchException != null) throw noMatchException;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkDelegate.java
index b5b1f6d9..cf6e887 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkDelegate.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkDelegate.java
@@ -4,6 +4,7 @@
 
 package org.chromium.chrome.browser.bookmarks;
 
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
 import org.chromium.components.bookmarks.BookmarkId;
 import org.chromium.components.browser_ui.widget.dragreorder.DragStateDelegate;
 import org.chromium.components.browser_ui.widget.selectable_list.SelectableListLayout;
@@ -85,10 +86,11 @@
     BookmarkModel getModel();
 
     /**
-     * @return Current UiState of bookmark main UI. If no mode is stored,
-     *         {@link BookmarkUiState#STATE_LOADING} is returned.
+     * Returns current mode of bookmark main UI. If no mode is stored,
+     * {@link BookmarkUiMode.LOADING} is returned.
      */
-    int getCurrentState();
+    @BookmarkUiMode
+    int getCurrentUiMode();
 
     /**
      * @return LargeIconBridge instance. By sharing the instance, we can also share the cache.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemRow.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemRow.java
index edaf009..ce9ad5c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemRow.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemRow.java
@@ -10,6 +10,7 @@
 import android.util.AttributeSet;
 
 import org.chromium.chrome.R;
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
 import org.chromium.chrome.browser.ui.favicon.FaviconUtils;
 import org.chromium.components.bookmarks.BookmarkId;
 import org.chromium.components.bookmarks.BookmarkItem;
@@ -56,11 +57,11 @@
 
     @Override
     public void onClick() {
-        switch (mDelegate.getCurrentState()) {
-            case BookmarkUiState.STATE_FOLDER:
-            case BookmarkUiState.STATE_SEARCHING:
+        switch (mDelegate.getCurrentUiMode()) {
+            case BookmarkUiMode.FOLDER:
+            case BookmarkUiMode.SEARCHING:
                 break;
-            case BookmarkUiState.STATE_LOADING:
+            case BookmarkUiMode.LOADING:
                 assert false :
                         "The main content shouldn't be inflated if it's still loading";
                 break;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemsAdapter.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemsAdapter.java
index da4e93b..0668d0e 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemsAdapter.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkItemsAdapter.java
@@ -5,33 +5,17 @@
 package org.chromium.chrome.browser.bookmarks;
 
 import android.content.Context;
-import android.text.TextUtils;
 import android.view.View;
 import android.view.ViewGroup;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
+import androidx.recyclerview.widget.ItemTouchHelper;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
-import org.chromium.base.metrics.RecordUserAction;
-import org.chromium.chrome.R;
 import org.chromium.chrome.browser.bookmarks.BookmarkListEntry.ViewType;
-import org.chromium.chrome.browser.bookmarks.BookmarkRow.Location;
-import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
-import org.chromium.chrome.browser.flags.ChromeFeatureList;
-import org.chromium.chrome.browser.profiles.Profile;
-import org.chromium.chrome.browser.sync.SyncService;
-import org.chromium.chrome.browser.sync.SyncService.SyncStateChangedListener;
-import org.chromium.chrome.browser.ui.signin.SyncPromoController.SyncPromoState;
-import org.chromium.components.bookmarks.BookmarkId;
-import org.chromium.components.bookmarks.BookmarkItem;
-import org.chromium.components.bookmarks.BookmarkType;
 import org.chromium.components.browser_ui.widget.dragreorder.DragReorderableListAdapter;
-import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate.SelectionObserver;
-import org.chromium.components.feature_engagement.EventConstants;
-import org.chromium.components.power_bookmarks.PowerBookmarkMeta;
 import org.chromium.ui.modelutil.PropertyModel;
 
 import java.util.ArrayList;
@@ -41,9 +25,6 @@
  * BaseAdapter for {@link RecyclerView}. It manages bookmarks to list there.
  */
 public class BookmarkItemsAdapter extends DragReorderableListAdapter<BookmarkListEntry> {
-    private static final int MAXIMUM_NUMBER_OF_SEARCH_RESULTS = 500;
-    private static final String EMPTY_QUERY = null;
-
     /** Abstraction around how to build type specific {@link View}s. */
     interface ViewFactory {
         /**
@@ -59,230 +40,53 @@
         void bindView(View view, @ViewType int viewType, PropertyModel model);
     }
 
-    private final List<BookmarkId> mTopLevelFolders = new ArrayList<>();
-    private final Profile mProfile;
+    /**
+     * Temporary interface to provide functionality that needs dependencies until this adapter is
+     * replaced. See https://crbug.com/1413463.
+     */
+    interface ViewDelegate {
+        PropertyModel buildModel(ViewHolder holder, int position);
+        void recycleView(View view, @ViewType int viewType);
+        void setOrder(List<BookmarkListEntry> listEntries);
+        boolean isReorderable(BookmarkListEntry entry);
+    }
+
     private final ViewFactory mViewFactory;
     private final ViewBinder mViewBinder;
-    private final SyncService mSyncService;
-    private final BookmarkPromoHeader mPromoHeaderManager;
+    private @Nullable BookmarkDelegate mBookmarkDelegate;
+    private @Nullable ViewDelegate mViewDelegate;
 
-    // There can only be one promo header at a time. This takes on one of the values:
-    // ViewType.PERSONALIZED_SIGNIN_PROMO, ViewType.SYNC_PROMO, or ViewType.INVALID.
-    @ViewType
-    private int mPromoHeaderType = ViewType.INVALID;
-    private BookmarkDelegate mDelegate;
-    private String mSearchText;
-    private BookmarkId mCurrentFolder;
-
-    // Keep track of the currently highlighted bookmark - used for "show in folder" action.
-    private BookmarkId mHighlightedBookmark;
-
-    private BookmarkModelObserver mBookmarkModelObserver = new BookmarkModelObserver() {
-        @Override
-        public void bookmarkNodeChanged(BookmarkItem node) {
-            assert mDelegate != null;
-            clearHighlight();
-            int position = getPositionForBookmark(node.getId());
-            if (position >= 0) notifyItemChanged(position);
-        }
-
-        @Override
-        public void bookmarkNodeRemoved(BookmarkItem parent, int oldIndex, BookmarkItem node,
-                boolean isDoingExtensiveChanges) {
-            assert mDelegate != null;
-            clearHighlight();
-
-            if (mDelegate.getCurrentState() == BookmarkUiState.STATE_SEARCHING) {
-                // We cannot rely on removing the specific list item that corresponds to the
-                // removed node because the node might be a parent with children also shown
-                // in the list.
-                search(mSearchText);
-                return;
-            }
-
-            if (node.isFolder()) {
-                mDelegate.notifyStateChange(mBookmarkUiObserver);
-            } else {
-                int deletedPosition = getPositionForBookmark(node.getId());
-                if (deletedPosition >= 0) {
-                    removeItem(deletedPosition);
-                }
-            }
-        }
-
-        @Override
-        public void bookmarkModelChanged() {
-            assert mDelegate != null;
-            clearHighlight();
-            mDelegate.notifyStateChange(mBookmarkUiObserver);
-
-            if (mDelegate.getCurrentState() == BookmarkUiState.STATE_SEARCHING) {
-                if (!TextUtils.equals(mSearchText, EMPTY_QUERY)) {
-                    search(mSearchText);
-                } else {
-                    mDelegate.closeSearchUi();
-                }
-            }
-        }
-    };
-
-    private final BookmarkUiObserver mBookmarkUiObserver = new BookmarkUiObserver() {
-        @Override
-        public void onDestroy() {
-            mDelegate.removeUiObserver(mBookmarkUiObserver);
-            mDelegate.getModel().removeObserver(mBookmarkModelObserver);
-            mDelegate.getSelectionDelegate().removeObserver(mSelectionObserver);
-            mDelegate = null;
-            mPromoHeaderManager.destroy();
-            mSyncService.removeSyncStateChangedListener(mSyncStateChangedListener);
-        }
-
-        @Override
-        public void onFolderStateSet(BookmarkId folder) {
-            assert mDelegate != null;
-            clearHighlight();
-
-            mSearchText = EMPTY_QUERY;
-            mCurrentFolder = folder;
-            enableDrag();
-
-            if (topLevelFoldersShowing()) {
-                setBookmarks(mTopLevelFolders);
-            } else {
-                setBookmarks(mDelegate.getModel().getChildIDs(folder));
-            }
-
-            if (BookmarkId.SHOPPING_FOLDER.equals(folder)) {
-                mDelegate.getSelectableListLayout().setEmptyViewText(
-                        R.string.tracked_products_empty_list_title);
-            } else if (folder.getType() == BookmarkType.READING_LIST) {
-                TrackerFactory.getTrackerForProfile(mProfile).notifyEvent(
-                        EventConstants.READ_LATER_BOOKMARK_FOLDER_OPENED);
-                mDelegate.getSelectableListLayout().setEmptyViewText(
-                        R.string.reading_list_empty_list_title);
-            } else {
-                mDelegate.getSelectableListLayout().setEmptyViewText(
-                        R.string.bookmarks_folder_empty);
-            }
-        }
-
-        @Override
-        public void onSearchStateSet() {
-            clearHighlight();
-            disableDrag();
-            // Headers should not appear in Search mode
-            // Don't need to notify because we need to redraw everything in the next step
-            updateHeader(false);
-            removeSectionHeaders();
-            notifyDataSetChanged();
-        }
-    };
-
-    private final SelectionObserver<BookmarkId> mSelectionObserver = new SelectionObserver<>() {
-        @Override
-        public void onSelectionStateChange(List<BookmarkId> selectedBookmarks) {
-            clearHighlight();
-        }
-    };
-
-    private final SyncStateChangedListener mSyncStateChangedListener =
-            new SyncStateChangedListener() {
-                @Override
-                public void syncStateChanged() {
-                    // If mDelegate is null, we will set the top level folders upon its
-                    // initialization (see onBookmarkDelegateInitialized method above).
-                    if (mDelegate == null) {
-                        return;
-                    }
-                    mTopLevelFolders.clear();
-                    populateTopLevelFoldersList();
-                }
-            };
-
-    BookmarkItemsAdapter(
-            Context context, Profile profile, ViewFactory viewFactory, ViewBinder viewBinder) {
+    BookmarkItemsAdapter(Context context, ViewFactory viewFactory, ViewBinder viewBinder) {
         super(context);
-        mProfile = profile;
         mViewFactory = viewFactory;
         mViewBinder = viewBinder;
-
-        mSyncService = SyncService.get();
-        mSyncService.addSyncStateChangedListener(mSyncStateChangedListener);
-
-        Runnable promoHeaderChangeAction = () -> {
-            // Notify the view of changes to the elements list as the promo might be showing.
-            updateHeader(true);
-        };
-        mPromoHeaderManager = new BookmarkPromoHeader(mContext, mProfile, promoHeaderChangeAction);
     }
 
     /**
-     * @return The position of the given bookmark in adapter. Will return -1 if not found.
+     * Sets the delegate to use to handle UI actions related to this adapter.
+     * @param bookmarkDelegate A {@link ViewDelegate} instance to backend interactions.
+     * @param viewDelegate A {@link ViewDelegate} instance to handle model interactions.
      */
-    int getPositionForBookmark(BookmarkId bookmark) {
-        assert bookmark != null;
-        int position = -1;
-        for (int i = 0; i < getItemCount(); i++) {
-            if (bookmark.equals(getIdByPosition(i))) {
-                position = i;
-                break;
-            }
-        }
-        return position;
-    }
-
-    private void filterForPriceTrackingCategory(List<BookmarkId> bookmarks) {
-        for (int i = bookmarks.size() - 1; i >= 0; i--) {
-            PowerBookmarkMeta meta = mDelegate.getModel().getPowerBookmarkMeta(bookmarks.get(i));
-            if (meta == null || !meta.hasShoppingSpecifics()
-                    || !meta.getShoppingSpecifics().getIsPriceTracked()) {
-                bookmarks.remove(i);
-                continue;
-            }
-        }
-    }
-
-    private void setBookmarks(List<BookmarkId> bookmarks) {
-        clearHighlight();
-        mElements.clear();
-
-        // Restore the header, if it exists, then update it.
-        if (hasPromoHeader()) {
-            mElements.add(BookmarkListEntry.createSyncPromoHeader(mPromoHeaderType));
-        }
-
-        updateHeader(false);
-        if (BookmarkId.SHOPPING_FOLDER.equals(mCurrentFolder)) {
-            filterForPriceTrackingCategory(bookmarks);
-        }
-
-        for (BookmarkId bookmarkId : bookmarks) {
-            BookmarkItem item = mDelegate.getModel().getBookmarkById(bookmarkId);
-
-            mElements.add(BookmarkListEntry.createBookmarkEntry(
-                    item, mDelegate.getModel().getPowerBookmarkMeta(bookmarkId)));
-        }
-
-        if (mCurrentFolder.getType() == BookmarkType.READING_LIST
-                && mDelegate.getCurrentState() != BookmarkUiState.STATE_SEARCHING) {
-            ReadingListSectionHeader.maybeSortAndInsertSectionHeaders(mElements, mContext);
-        }
-
-        if (ChromeFeatureList.isEnabled(ChromeFeatureList.SHOPPING_LIST)
-                && topLevelFoldersShowing()) {
-            mElements.add(BookmarkListEntry.createDivider());
-            mElements.add(BookmarkListEntry.createShoppingFilter());
-        }
-
+    @SuppressWarnings("NotifyDataSetChanged")
+    void onBookmarkDelegateInitialized(
+            BookmarkDelegate bookmarkDelegate, ViewDelegate viewDelegate) {
+        mElements = new ArrayList<>();
+        mBookmarkDelegate = bookmarkDelegate;
+        mViewDelegate = viewDelegate;
+        setDragStateDelegate(bookmarkDelegate.getDragStateDelegate());
         notifyDataSetChanged();
     }
 
-    private void removeItem(int position) {
-        mElements.remove(position);
-        notifyItemRemoved(position);
+    List<BookmarkListEntry> getElements() {
+        return mElements;
+    }
+
+    ItemTouchHelper getItemTouchHelper() {
+        return mItemTouchHelper;
     }
 
     // DragReorderableListAdapter implementation.
+
     @Override
     public @ViewType int getItemViewType(int position) {
         BookmarkListEntry entry = getItemByPosition(position);
@@ -296,294 +100,30 @@
 
     @Override
     public void onBindViewHolder(ViewHolder holder, int position) {
-        PropertyModel model = new PropertyModel(BookmarkManagerProperties.ALL_KEYS);
-        @ViewType
-        int viewType = holder.getItemViewType();
-
-        if (holder.getItemViewType() == ViewType.PERSONALIZED_SIGNIN_PROMO
-                || holder.getItemViewType() == ViewType.PERSONALIZED_SYNC_PROMO) {
-            model.set(BookmarkManagerProperties.BOOKMARK_PROMO_HEADER, mPromoHeaderManager);
-        } else if (holder.getItemViewType() == ViewType.SECTION_HEADER) {
-            model.set(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY, getItemByPosition(position));
-        } else if (BookmarkListEntry.isBookmarkEntry(holder.getItemViewType())) {
-            BookmarkId id = getIdByPosition(position);
-            model.set(BookmarkManagerProperties.BOOKMARK_ID, id);
-            model.set(BookmarkManagerProperties.LOCATION, getLocationFromPosition(position));
-            model.set(BookmarkManagerProperties.IS_FROM_FILTER_VIEW,
-                    BookmarkId.SHOPPING_FOLDER.equals(mCurrentFolder));
-            model.set(BookmarkManagerProperties.ITEM_TOUCH_HELPER, mItemTouchHelper);
-            model.set(BookmarkManagerProperties.VIEW_HOLDER, holder);
-            model.set(BookmarkManagerProperties.IS_HIGHLIGHTED, id.equals(mHighlightedBookmark));
-            model.set(BookmarkManagerProperties.CLEAR_HIGHLIGHT, this::clearHighlight);
-        } else if (holder.getItemViewType() == ViewType.SHOPPING_FILTER) {
-            model.set(BookmarkManagerProperties.OPEN_FOLDER, mDelegate::openFolder);
-        }
-
-        mViewBinder.bindView(holder.itemView, viewType, model);
+        PropertyModel propertyModel = mViewDelegate.buildModel(holder, position);
+        mViewBinder.bindView(holder.itemView, holder.getItemViewType(), propertyModel);
     }
 
     @Override
     public void onViewRecycled(ViewHolder holder) {
-        switch (holder.getItemViewType()) {
-            case ViewType.PERSONALIZED_SIGNIN_PROMO:
-                // fall through
-            case ViewType.PERSONALIZED_SYNC_PROMO:
-                mPromoHeaderManager.detachPersonalizePromoView();
-                break;
-            default:
-                // Other view holders don't have special recycling code.
-        }
-    }
-
-    /**
-     * Sets the delegate to use to handle UI actions related to this adapter.
-     *
-     * @param delegate A {@link BookmarkDelegate} instance to handle all backend interaction.
-     */
-    void onBookmarkDelegateInitialized(BookmarkDelegate delegate) {
-        mDelegate = delegate;
-        mDelegate.addUiObserver(mBookmarkUiObserver);
-        mDelegate.getModel().addObserver(mBookmarkModelObserver);
-        mDelegate.getSelectionDelegate().addObserver(mSelectionObserver);
-
-        populateTopLevelFoldersList();
-
-        mElements = new ArrayList<>();
-        setDragStateDelegate(delegate.getDragStateDelegate());
-        notifyDataSetChanged();
-    }
-
-    /**
-     * Refresh the list of bookmarks within the currently visible folder.
-     */
-    public void refresh() {
-        // Tell the RecyclerView to update its elements.
-        if (mElements != null) notifyDataSetChanged();
-    }
-
-    /**
-     * Synchronously searches for the given query.
-     *
-     * @param query The query text to search for.
-     */
-    public void search(@Nullable String query) {
-        mSearchText = query == null ? "" : query.trim();
-
-        List<BookmarkId> bookmarks =
-                mDelegate.getModel().searchBookmarks(mSearchText, MAXIMUM_NUMBER_OF_SEARCH_RESULTS);
-        setBookmarks(bookmarks);
-    }
-
-    /**
-     * See {@link BookmarkDelegate#moveUpOne(BookmarkId)}.
-     */
-    void moveUpOne(BookmarkId bookmarkId) {
-        int pos = getPositionForBookmark(bookmarkId);
-        assert isReorderable(getItemByPosition(pos));
-        mElements.remove(pos);
-        mElements.add(pos - 1,
-                BookmarkListEntry.createBookmarkEntry(
-                        mDelegate.getModel().getBookmarkById(bookmarkId),
-                        mDelegate.getModel().getPowerBookmarkMeta(bookmarkId)));
-        setOrder(mElements);
-    }
-
-    /**
-     * See {@link BookmarkDelegate#moveDownOne(BookmarkId)}.
-     */
-    void moveDownOne(BookmarkId bookmarkId) {
-        int pos = getPositionForBookmark(bookmarkId);
-        assert isReorderable(getItemByPosition(pos));
-        mElements.remove(pos);
-        mElements.add(pos + 1,
-                BookmarkListEntry.createBookmarkEntry(
-                        mDelegate.getModel().getBookmarkById(bookmarkId),
-                        mDelegate.getModel().getPowerBookmarkMeta(bookmarkId)));
-        setOrder(mElements);
-    }
-
-    /**
-     * Updates mPromoHeaderType. Makes sure that the 0th index of mElements is consistent with the
-     * promo header. This 0th index is null iff there is a promo header.
-     *
-     * @param shouldNotify True iff we should notify the RecyclerView of changes to the promoheader.
-     *                     (This should be false iff we are going to make further changes to the
-     *                     list of elements, as we do in setBookmarks, and true iff we are only
-     *                     changing the header, as we do in the promoHeaderChangeAction runnable).
-     */
-    private void updateHeader(boolean shouldNotify) {
-        if (mDelegate == null) return;
-
-        boolean wasShowingPromo = hasPromoHeader();
-
-        int currentUiState = mDelegate.getCurrentState();
-        if (currentUiState == BookmarkUiState.STATE_LOADING) {
-            return;
-        } else if (currentUiState == BookmarkUiState.STATE_SEARCHING) {
-            mPromoHeaderType = ViewType.INVALID;
-        } else {
-            switch (mPromoHeaderManager.getPromoState()) {
-                case SyncPromoState.NO_PROMO:
-                    mPromoHeaderType = ViewType.INVALID;
-                    break;
-                case SyncPromoState.PROMO_FOR_SIGNED_OUT_STATE:
-                    mPromoHeaderType = ViewType.PERSONALIZED_SIGNIN_PROMO;
-                    break;
-                case SyncPromoState.PROMO_FOR_SIGNED_IN_STATE:
-                    mPromoHeaderType = ViewType.PERSONALIZED_SYNC_PROMO;
-                    break;
-                case SyncPromoState.PROMO_FOR_SYNC_TURNED_OFF_STATE:
-                    mPromoHeaderType = ViewType.SYNC_PROMO;
-                    break;
-                default:
-                    assert false : "Unexpected value for promo state!";
-            }
-        }
-
-        boolean willShowPromo = hasPromoHeader();
-
-        if (!wasShowingPromo && willShowPromo) {
-            // A null element at the 0th index represents a promo header.
-            mElements.add(0, BookmarkListEntry.createSyncPromoHeader(mPromoHeaderType));
-            if (shouldNotify) notifyItemInserted(0);
-        } else if (wasShowingPromo && willShowPromo) {
-            if (shouldNotify) notifyItemChanged(0);
-        } else if (wasShowingPromo && !willShowPromo) {
-            mElements.remove(0);
-            if (shouldNotify) notifyItemRemoved(0);
-        }
-    }
-
-    /** Removes all section headers from the current list. */
-    private void removeSectionHeaders() {
-        for (int i = mElements.size() - 1; i >= 0; i--) {
-            if (mElements.get(i).getViewType() == ViewType.SECTION_HEADER) {
-                mElements.remove(i);
-            }
-        }
-    }
-
-    private void populateTopLevelFoldersList() {
-        mTopLevelFolders.addAll(BookmarkUtils.populateTopLevelFolders(mDelegate.getModel()));
+        mViewDelegate.recycleView(holder.itemView, holder.getItemViewType());
     }
 
     @Override
     protected void setOrder(List<BookmarkListEntry> listEntries) {
-        assert !topLevelFoldersShowing() : "Cannot reorder top-level folders!";
-        assert mCurrentFolder.getType()
-                != BookmarkType.PARTNER : "Cannot reorder partner bookmarks!";
-        assert mDelegate.getCurrentState()
-                == BookmarkUiState.STATE_FOLDER : "Can only reorder items from folder mode!";
-
-        int startIndex = getBookmarkItemStartIndex();
-        int endIndex = getBookmarkItemEndIndex();
-
-        // Get the new order for the IDs.
-        long[] newOrder = new long[endIndex - startIndex + 1];
-        for (int i = startIndex; i <= endIndex; i++) {
-            BookmarkItem bookmarkItem = listEntries.get(i).getBookmarkItem();
-            assert bookmarkItem != null;
-            newOrder[i - startIndex] = bookmarkItem.getId().getId();
-        }
-        mDelegate.getModel().reorderBookmarks(mCurrentFolder, newOrder);
-        if (mDragStateDelegate.getDragActive()) {
-            RecordUserAction.record("MobileBookmarkManagerDragReorder");
-        }
-    }
-
-    private int getBookmarkItemStartIndex() {
-        return hasPromoHeader() ? 1 : 0;
-    }
-
-    private int getBookmarkItemEndIndex() {
-        int endIndex = mElements.size() - 1;
-        BookmarkItem bookmarkItem = mElements.get(endIndex).getBookmarkItem();
-        if (bookmarkItem == null || !BookmarkUtils.isMovable(bookmarkItem)) {
-            endIndex--;
-        }
-        return endIndex;
-    }
-
-    private boolean isReorderable(BookmarkListEntry entry) {
-        return entry != null && entry.getBookmarkItem() != null
-                && entry.getBookmarkItem().isReorderable();
+        mViewDelegate.setOrder(listEntries);
     }
 
     // DragReorderableListAdapter implementation.
+
     @Override
-    @VisibleForTesting
     public boolean isActivelyDraggable(ViewHolder viewHolder) {
         return isPassivelyDraggable(viewHolder)
                 && ((BookmarkRow) viewHolder.itemView).isItemSelected();
     }
 
     @Override
-    @VisibleForTesting
     public boolean isPassivelyDraggable(ViewHolder viewHolder) {
-        return isReorderable(getItemByHolder(viewHolder));
-    }
-
-    public BookmarkId getIdByPosition(int position) {
-        BookmarkListEntry entry = getItemByPosition(position);
-        if (entry == null || entry.getBookmarkItem() == null) return null;
-        return entry.getBookmarkItem().getId();
-    }
-
-    public BookmarkPromoHeader getPromoHeaderManager() {
-        return mPromoHeaderManager;
-    }
-
-    private boolean hasPromoHeader() {
-        return mPromoHeaderType != ViewType.INVALID;
-    }
-
-    private @Location int getLocationFromPosition(int position) {
-        if (position == getBookmarkItemStartIndex() && position == getBookmarkItemEndIndex()) {
-            return Location.SOLO;
-        } else if (position == getBookmarkItemStartIndex()) {
-            return Location.TOP;
-        } else if (position == getBookmarkItemEndIndex()) {
-            return Location.BOTTOM;
-        } else {
-            return Location.MIDDLE;
-        }
-    }
-
-    /**
-     * @return True iff the currently-open folder is the root folder
-     *         (which is true iff the top-level folders are showing)
-     */
-    private boolean topLevelFoldersShowing() {
-        return mCurrentFolder.equals(mDelegate.getModel().getRootFolderId());
-    }
-
-    /**
-     * Scroll the bookmarks list such that bookmarkId is shown in the view, and highlight it.
-     *
-     * @param bookmarkId The BookmarkId of the bookmark of interest
-     */
-    void highlightBookmark(BookmarkId bookmarkId) {
-        assert mHighlightedBookmark == null : "There should not already be a highlighted bookmark!";
-
-        mRecyclerView.scrollToPosition(getPositionForBookmark(bookmarkId));
-        mHighlightedBookmark = bookmarkId;
-    }
-
-    /**
-     * Clears the highlighted bookmark, if there is one.
-     */
-    private void clearHighlight() {
-        mHighlightedBookmark = null;
-    }
-
-    @VisibleForTesting
-    BookmarkDelegate getDelegateForTesting() {
-        return mDelegate;
-    }
-
-    @VisibleForTesting
-    public void simulateSignInForTesting() {
-        mSyncStateChangedListener.syncStateChanged();
-        mBookmarkUiObserver.onFolderStateSet(mCurrentFolder);
+        return mViewDelegate.isReorderable(getItemByHolder(viewHolder));
     }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerCoordinator.java
index 44cda55..c8c5abf 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerCoordinator.java
@@ -13,7 +13,6 @@
 
 import androidx.annotation.LayoutRes;
 import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.widget.RecyclerView;
 
 import org.chromium.base.ContextUtils;
@@ -58,7 +57,6 @@
     private final BookmarkOpener mBookmarkOpener;
     private final BookmarkToolbarCoordinator mBookmarkToolbarCoordinator;
     private final BookmarkManagerMediator mMediator;
-    private final BookmarkUndoController mUndoController;
     private final ImageFetcher mImageFetcher;
     private final SnackbarManager mSnackbarManager;
     private final BookmarkPromoHeader mPromoHeaderManager;
@@ -107,10 +105,9 @@
                 mMainView.findViewById(R.id.selectable_list);
         mSelectableListLayout = selectableList;
         mSelectableListLayout.initializeEmptyView(R.string.bookmarks_folder_empty);
-        BookmarkItemsAdapter bookmarkItemsAdapter =
-                new BookmarkItemsAdapter(context, profile, this::createView, this::bindView);
-        mPromoHeaderManager = bookmarkItemsAdapter.getPromoHeaderManager();
 
+        BookmarkItemsAdapter bookmarkItemsAdapter =
+                new BookmarkItemsAdapter(context, this::createView, this::bindView);
         mRecyclerView = mSelectableListLayout.initializeRecyclerView(
                 (RecyclerView.Adapter<RecyclerView.ViewHolder>) bookmarkItemsAdapter);
 
@@ -122,13 +119,16 @@
                 bookmarkDelegateSupplier, mBookmarkModel, mBookmarkOpener);
         mSelectableListLayout.configureWideDisplayStyle();
 
-        LargeIconBridge largeIconBridge = new LargeIconBridge(profile);
+        LargeIconBridge largeIconBridge = new LargeIconBridge(mProfile);
         largeIconBridge.createCache(computeCacheMaxSize());
 
-        mUndoController = new BookmarkUndoController(context, mBookmarkModel, snackbarManager);
+        BookmarkUndoController bookmarkUndoController =
+                new BookmarkUndoController(context, mBookmarkModel, snackbarManager);
         mMediator = new BookmarkManagerMediator(context, mBookmarkModel, mBookmarkOpener,
                 mSelectableListLayout, selectionDelegate, mRecyclerView, bookmarkItemsAdapter,
-                largeIconBridge, isDialogUi, isIncognito, mBackPressStateSupplier);
+                largeIconBridge, isDialogUi, isIncognito, mBackPressStateSupplier, mProfile,
+                bookmarkUndoController);
+        mPromoHeaderManager = mMediator.getPromoHeaderManager();
 
         bookmarkDelegateSupplier.set(/*bookmarkDelegate=*/mMediator);
 
@@ -204,7 +204,7 @@
 
     @Override
     public void onSearchTextChanged(String query) {
-        mMediator.onSearchTextChanged(query);
+        mMediator.search(query);
     }
 
     @Override
@@ -346,17 +346,14 @@
 
     // Testing methods.
 
-    @VisibleForTesting
     public BookmarkToolbar getToolbarForTesting() {
         return mBookmarkToolbarCoordinator.getToolbarForTesting(); // IN-TEST
     }
 
-    @VisibleForTesting
     public BookmarkUndoController getUndoControllerForTesting() {
-        return mUndoController;
+        return mMediator.getUndoControllerForTesting();
     }
 
-    @VisibleForTesting
     public RecyclerView getRecyclerViewForTesting() {
         return mRecyclerView;
     }
@@ -372,4 +369,8 @@
     public BookmarkDelegate getBookmarkDelegateForTesting() {
         return mMediator;
     }
+
+    public TestingDelegate getTestingDelegate() {
+        return mMediator;
+    }
 }
\ No newline at end of file
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediator.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediator.java
index aa9cbcc..1c8e19c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediator.java
@@ -6,40 +6,60 @@
 
 import android.content.Context;
 import android.text.TextUtils;
+import android.view.View;
 import android.view.accessibility.AccessibilityManager;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
 import org.chromium.base.ObserverList;
 import org.chromium.base.metrics.RecordUserAction;
 import org.chromium.base.supplier.ObservableSupplierImpl;
 import org.chromium.chrome.R;
+import org.chromium.chrome.browser.bookmarks.BookmarkListEntry.ViewType;
+import org.chromium.chrome.browser.bookmarks.BookmarkRow.Location;
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
+import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.partnerbookmarks.PartnerBookmarksReader;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.sync.SyncService;
+import org.chromium.chrome.browser.sync.SyncService.SyncStateChangedListener;
 import org.chromium.chrome.browser.ui.native_page.BasicNativePage;
+import org.chromium.chrome.browser.ui.signin.SyncPromoController.SyncPromoState;
 import org.chromium.components.bookmarks.BookmarkId;
 import org.chromium.components.bookmarks.BookmarkItem;
 import org.chromium.components.bookmarks.BookmarkType;
 import org.chromium.components.browser_ui.widget.dragreorder.DragStateDelegate;
 import org.chromium.components.browser_ui.widget.selectable_list.SelectableListLayout;
 import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate;
+import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate.SelectionObserver;
 import org.chromium.components.favicon.LargeIconBridge;
+import org.chromium.components.feature_engagement.EventConstants;
+import org.chromium.ui.modelutil.PropertyModel;
 import org.chromium.url.GURL;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Stack;
 
 /** Responsible for BookmarkManager business logic. */
 // TODO(crbug.com/1416611): Remove BookmarkDelegate if possible.
-class BookmarkManagerMediator
-        implements BookmarkDelegate, PartnerBookmarksReader.FaviconUpdateObserver {
+class BookmarkManagerMediator implements BookmarkDelegate, TestingDelegate,
+                                         PartnerBookmarksReader.FaviconUpdateObserver,
+                                         BookmarkItemsAdapter.ViewDelegate {
+    private static final int MAXIMUM_NUMBER_OF_SEARCH_RESULTS = 500;
+    private static final String EMPTY_QUERY = null;
+
     private static boolean sPreventLoadingForTesting;
 
     /**
      * Keeps track of whether drag is enabled / active for bookmark lists.
      */
-    class BookmarkDragStateDelegate implements DragStateDelegate {
+    private class BookmarkDragStateDelegate implements DragStateDelegate {
         private BookmarkDelegate mBookmarkDelegate;
         private SelectionDelegate<BookmarkId> mSelectionDelegate;
         private AccessibilityManager mA11yManager;
@@ -62,8 +82,7 @@
         // DragStateDelegate implementation
         @Override
         public boolean getDragEnabled() {
-            return !mA11yEnabled
-                    && mBookmarkDelegate.getCurrentState() == BookmarkUiState.STATE_FOLDER;
+            return !mA11yEnabled && mBookmarkDelegate.getCurrentUiMode() == BookmarkUiMode.FOLDER;
         }
 
         @Override
@@ -86,7 +105,7 @@
     private final BookmarkModelObserver mBookmarkModelObserver = new BookmarkModelObserver() {
         @Override
         public void bookmarkNodeChildrenReordered(BookmarkItem node) {
-            mBookmarkItemsAdapter.refresh();
+            refresh();
         }
 
         @Override
@@ -95,7 +114,7 @@
                 boolean isDoingExtensiveChanges) {
             // If the folder is removed in folder mode, show the parent folder or falls back to all
             // bookmarks mode.
-            if (getCurrentState() == BookmarkUiState.STATE_FOLDER
+            if (getCurrentUiMode() == BookmarkUiMode.FOLDER
                     && node.getId().equals(mStateStack.peek().mFolder)) {
                 if (mBookmarkModel.getTopLevelFolderIDs(true, true).contains(node.getId())) {
                     openFolder(mBookmarkModel.getDefaultFolderViewLocation());
@@ -113,7 +132,7 @@
 
         @Override
         public void bookmarkNodeChanged(BookmarkItem node) {
-            if (getCurrentState() == BookmarkUiState.STATE_FOLDER && !mStateStack.isEmpty()
+            if (getCurrentUiMode() == BookmarkUiMode.FOLDER && !mStateStack.isEmpty()
                     && node.getId().equals(mStateStack.peek().mFolder)) {
                 notifyUi(mStateStack.peek());
                 return;
@@ -125,7 +144,7 @@
         public void bookmarkModelChanged() {
             // If the folder no longer exists in folder mode, we need to fall back. Relying on the
             // default behavior by setting the folder mode again.
-            if (getCurrentState() == BookmarkUiState.STATE_FOLDER) {
+            if (getCurrentUiMode() == BookmarkUiMode.FOLDER) {
                 setState(mStateStack.peek());
             }
         }
@@ -163,6 +182,123 @@
                 }
             };
 
+    // TODO(https://crbug.com/1413463): Combine with mBookmarkModelObserver.
+    private BookmarkModelObserver mBookmarkModelObserver2 = new BookmarkModelObserver() {
+        @Override
+        public void bookmarkNodeChanged(BookmarkItem node) {
+            clearHighlight();
+            int position = getPositionForBookmark(node.getId());
+            if (position >= 0) mBookmarkItemsAdapter.notifyItemChanged(position);
+        }
+
+        @Override
+        public void bookmarkNodeRemoved(BookmarkItem parent, int oldIndex, BookmarkItem node,
+                boolean isDoingExtensiveChanges) {
+            clearHighlight();
+
+            if (getCurrentUiMode() == BookmarkUiMode.SEARCHING) {
+                // We cannot rely on removing the specific list item that corresponds to the
+                // removed node because the node might be a parent with children also shown
+                // in the list.
+                search(mSearchText);
+                return;
+            }
+
+            if (node.isFolder()) {
+                notifyStateChange(mBookmarkUiObserver);
+            } else {
+                int deletedPosition = getPositionForBookmark(node.getId());
+                if (deletedPosition >= 0) {
+                    removeItem(deletedPosition);
+                }
+            }
+        }
+
+        @Override
+        public void bookmarkModelChanged() {
+            clearHighlight();
+            notifyStateChange(mBookmarkUiObserver);
+
+            if (getCurrentUiMode() == BookmarkUiMode.SEARCHING) {
+                if (!TextUtils.equals(mSearchText, EMPTY_QUERY)) {
+                    search(mSearchText);
+                } else {
+                    closeSearchUi();
+                }
+            }
+        }
+    };
+
+    private final BookmarkUiObserver mBookmarkUiObserver = new BookmarkUiObserver() {
+        @Override
+        public void onDestroy() {
+            removeUiObserver(mBookmarkUiObserver);
+            mBookmarkModel.removeObserver(mBookmarkModelObserver2);
+            getSelectionDelegate().removeObserver(mSelectionObserver);
+            mPromoHeaderManager.destroy();
+            mSyncService.removeSyncStateChangedListener(mSyncStateChangedListener);
+        }
+
+        @Override
+        public void onFolderStateSet(BookmarkId folder) {
+            clearHighlight();
+
+            mSearchText = EMPTY_QUERY;
+            mCurrentFolder = folder;
+            mBookmarkItemsAdapter.enableDrag();
+
+            if (topLevelFoldersShowing()) {
+                setBookmarks(mTopLevelFolders);
+            } else {
+                setBookmarks(mBookmarkModel.getChildIDs(folder));
+            }
+
+            if (BookmarkId.SHOPPING_FOLDER.equals(folder)) {
+                getSelectableListLayout().setEmptyViewText(
+                        R.string.tracked_products_empty_list_title);
+            } else if (folder.getType() == BookmarkType.READING_LIST) {
+                TrackerFactory.getTrackerForProfile(mProfile).notifyEvent(
+                        EventConstants.READ_LATER_BOOKMARK_FOLDER_OPENED);
+                getSelectableListLayout().setEmptyViewText(R.string.reading_list_empty_list_title);
+            } else {
+                getSelectableListLayout().setEmptyViewText(R.string.bookmarks_folder_empty);
+            }
+        }
+
+        @SuppressWarnings("NotifyDataSetChanged")
+        @Override
+        public void onSearchStateSet() {
+            clearHighlight();
+            mBookmarkItemsAdapter.disableDrag();
+            // Headers should not appear in Search mode
+            // Don't need to notify because we need to redraw everything in the next step
+            updateHeader(false);
+            removeSectionHeaders();
+            mBookmarkItemsAdapter.notifyDataSetChanged();
+        }
+    };
+
+    private final SelectionObserver<BookmarkId> mSelectionObserver = new SelectionObserver<>() {
+        @Override
+        public void onSelectionStateChange(List<BookmarkId> selectedBookmarks) {
+            clearHighlight();
+        }
+    };
+
+    private final SyncStateChangedListener mSyncStateChangedListener =
+            new SyncStateChangedListener() {
+                @Override
+                public void syncStateChanged() {
+                    // If mDelegate is null, we will set the top level folders upon its
+                    // initialization (see onBookmarkDelegateInitialized method above).
+                    // if (mDelegate == null) {
+                    //    return;
+                    //}
+                    mTopLevelFolders.clear();
+                    populateTopLevelFoldersList();
+                }
+            };
+
     private final ObserverList<BookmarkUiObserver> mUiObservers = new ObserverList<>();
     private final BookmarkDragStateDelegate mDragStateDelegate = new BookmarkDragStateDelegate();
     private final Context mContext;
@@ -178,23 +314,36 @@
     // TODO(crbug.com/1416611): Remove reference to BookmarkItemsAdapter.
     private final BookmarkItemsAdapter mBookmarkItemsAdapter;
     private final LargeIconBridge mLargeIconBridge;
-    /** Whether we're showing in a dialog UI which is only true for phones. */
+    // Whether we're showing in a dialog UI which is only true for phones.
     private final boolean mIsDialogUi;
     private final boolean mIsIncognito;
     private final ObservableSupplierImpl<Boolean> mBackPressStateSupplier;
+    private final List<BookmarkId> mTopLevelFolders = new ArrayList<>();
+    private final Profile mProfile;
+    private final SyncService mSyncService;
+    private final BookmarkPromoHeader mPromoHeaderManager;
+    private final BookmarkUndoController mBookmarkUndoController;
 
-    /** Whether this instance has been destroyed. */
+    // Whether this instance has been destroyed.
     private boolean mIsDestroyed;
     private String mInitialUrl;
     private boolean mFaviconsNeedRefresh;
     private BasicNativePage mNativePage;
+    // There can only be one promo header at a time. This takes on one of the values:
+    // ViewType.PERSONALIZED_SIGNIN_PROMO, ViewType.SYNC_PROMO, or ViewType.INVALID.
+    private @ViewType int mPromoHeaderType = ViewType.INVALID;
+    private String mSearchText;
+    private BookmarkId mCurrentFolder;
+    // Keep track of the currently highlighted bookmark - used for "show in folder" action.
+    private BookmarkId mHighlightedBookmark;
 
     BookmarkManagerMediator(Context context, BookmarkModel bookmarkModel,
             BookmarkOpener bookmarkOpener, SelectableListLayout<BookmarkId> selectableListLayout,
             SelectionDelegate<BookmarkId> selectionDelegate, RecyclerView recyclerView,
             BookmarkItemsAdapter bookmarkItemsAdapter, LargeIconBridge largeIconBridge,
             boolean isDialogUi, boolean isIncognito,
-            ObservableSupplierImpl<Boolean> backPressStateSupplier) {
+            ObservableSupplierImpl<Boolean> backPressStateSupplier, Profile profile,
+            BookmarkUndoController bookmarkUndoController) {
         mContext = context;
         mBookmarkModel = bookmarkModel;
         mBookmarkModel.addObserver(mBookmarkModelObserver);
@@ -210,6 +359,13 @@
         mIsDialogUi = isDialogUi;
         mIsIncognito = isIncognito;
         mBackPressStateSupplier = backPressStateSupplier;
+        mProfile = profile;
+        mSyncService = SyncService.get();
+        mSyncService.addSyncStateChangedListener(mSyncStateChangedListener);
+        // Notify the view of changes to the elements list as the promo might be showing.
+        Runnable promoHeaderChangeAction = () -> updateHeader(true);
+        mPromoHeaderManager = new BookmarkPromoHeader(mContext, mProfile, promoHeaderChangeAction);
+        mBookmarkUndoController = bookmarkUndoController;
 
         // Previously we were waiting for BookmarkModel to be loaded, but it's not necessary.
         PartnerBookmarksReader.addFaviconUpdateObserver(this);
@@ -222,7 +378,16 @@
 
     void onBookmarkModelLoaded() {
         mDragStateDelegate.onBookmarkDelegateInitialized(this);
-        mBookmarkItemsAdapter.onBookmarkDelegateInitialized(this);
+
+        // TODO(https://crbug.com/1413463): This logic is here to keep the same execution order
+        // from when it was in the original adapter. It doesn't coceptaully make senes to be here,
+        // and should happen earlier.
+        addUiObserver(mBookmarkUiObserver);
+        mBookmarkModel.addObserver(mBookmarkModelObserver2);
+        mSelectionDelegate.addObserver(mSelectionObserver);
+        populateTopLevelFoldersList();
+
+        mBookmarkItemsAdapter.onBookmarkDelegateInitialized(this, this);
 
         if (!TextUtils.isEmpty(mInitialUrl)) {
             setState(BookmarkUiState.createStateFromUrl(mInitialUrl, mBookmarkModel));
@@ -237,17 +402,21 @@
         mLargeIconBridge.destroy();
         PartnerBookmarksReader.removeFaviconUpdateObserver(this);
 
+        mBookmarkUndoController.destroy();
+
         for (BookmarkUiObserver observer : mUiObservers) {
             observer.onDestroy();
         }
         assert mUiObservers.size() == 0;
     }
 
-    /** See BookmarkManager(Coordinator)#onBackPressed. */
+    /**
+     * See BookmarkManager(Coordinator)#onBackPressed.
+     */
     boolean onBackPressed() {
         if (mIsDestroyed) return false;
 
-        // TODO(twellington): replicate this behavior for other list UIs during unification.
+        // TODO(twellington): Replicate this behavior for other list UIs during unification.
         if (mSelectableListLayout.onBackPressed()) {
             return true;
         }
@@ -262,20 +431,23 @@
         return false;
     }
 
-    /** See BookmarkManager(Coordinator)#setBasicNativePage. */
+    /**
+     * See BookmarkManager(Coordinator)#setBasicNativePage.
+     */
     void setBasicNativePage(BasicNativePage nativePage) {
         mNativePage = nativePage;
     }
 
-    /** See BookmarkManager(Coordinator)#updateForUrl */
+    /**
+     * See BookmarkManager(Coordinator)#updateForUrl
+     */
     void updateForUrl(String url) {
         // Bookmark model is null if the manager has been destroyed.
         if (mBookmarkModel == null) return;
 
         if (mBookmarkModel.isBookmarkModelLoaded()) {
             BookmarkUiState searchState = null;
-            if (!mStateStack.isEmpty()
-                    && mStateStack.peek().mState == BookmarkUiState.STATE_SEARCHING) {
+            if (!mStateStack.isEmpty() && mStateStack.peek().mUiMode == BookmarkUiMode.SEARCHING) {
                 searchState = mStateStack.pop();
             }
 
@@ -287,91 +459,138 @@
         }
     }
 
-    /** See BookmarkManager(Coordinator)#getCurrentUrl. */
-    String getCurrentUrl() {
-        if (mStateStack.isEmpty()) return null;
-        return mStateStack.peek().mUrl;
+    BookmarkPromoHeader getPromoHeaderManager() {
+        return mPromoHeaderManager;
+    }
+
+    BookmarkId getIdByPosition(int position) {
+        BookmarkListEntry entry = getItemByPosition(position);
+        if (entry == null || entry.getBookmarkItem() == null) return null;
+        return entry.getBookmarkItem().getId();
     }
 
     /**
-     * Puts all UI elements to loading state. This state might be overridden synchronously by
-     * {@link #updateForUrl(String)}, if the bookmark model is already loaded.
+     * Synchronously searches for the given query.
+     * @param query The query text to search for.
      */
-    private void initializeToLoadingState() {
-        assert mStateStack.isEmpty();
-        setState(BookmarkUiState.createLoadingState());
+    void search(@Nullable String query) {
+        mSearchText = query == null ? "" : query.trim();
+        List<BookmarkId> bookmarks =
+                mBookmarkModel.searchBookmarks(mSearchText, MAXIMUM_NUMBER_OF_SEARCH_RESULTS);
+        setBookmarks(bookmarks);
     }
 
-    /**
-     * This is the ultimate internal method that updates UI and controls backstack. And it is the
-     * only method that pushes states to {@link #mStateStack}.
-     *
-     * <p>If the given state is not valid, all_bookmark state will be shown. Afterwards, this method
-     * checks the current state: if currently in loading state, it pops it out and adds the new
-     * state to the back stack. It also notifies the {@link #mNativePage} (if any) that the
-     * url has changed.
-     *
-     * <p>Also note that even if we store states to {@link #mStateStack}, on tablet the back
-     * navigation and back button are not controlled by the manager: the tab handles back key and
-     * backstack navigation.
-     */
-    private void setState(BookmarkUiState state) {
-        if (!state.isValid(mBookmarkModel)) {
-            state = BookmarkUiState.createFolderState(
-                    mBookmarkModel.getDefaultFolderViewLocation(), mBookmarkModel);
-        }
+    // BookmarkItemsAdapter.ViewDelegate implementation.
 
-        if (!mStateStack.isEmpty() && mStateStack.peek().equals(state)) return;
-
-        // The loading state is not persisted in history stack and once we have a valid state it
-        // shall be removed.
-        if (!mStateStack.isEmpty() && mStateStack.peek().mState == BookmarkUiState.STATE_LOADING) {
-            mStateStack.pop();
+    @Override
+    public PropertyModel buildModel(ViewHolder holder, int position) {
+        PropertyModel model = new PropertyModel(BookmarkManagerProperties.ALL_KEYS);
+        final @ViewType int viewType = holder.getItemViewType();
+        if (viewType == ViewType.PERSONALIZED_SIGNIN_PROMO
+                || viewType == ViewType.PERSONALIZED_SYNC_PROMO) {
+            model.set(BookmarkManagerProperties.BOOKMARK_PROMO_HEADER, mPromoHeaderManager);
+        } else if (viewType == ViewType.SECTION_HEADER) {
+            model.set(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY, getItemByPosition(position));
+        } else if (BookmarkListEntry.isBookmarkEntry(viewType)) {
+            BookmarkId id = getIdByPosition(position);
+            model.set(BookmarkManagerProperties.BOOKMARK_ID, id);
+            model.set(BookmarkManagerProperties.LOCATION, getLocationFromPosition(position));
+            model.set(BookmarkManagerProperties.IS_FROM_FILTER_VIEW,
+                    BookmarkId.SHOPPING_FOLDER.equals(mCurrentFolder));
+            model.set(BookmarkManagerProperties.ITEM_TOUCH_HELPER,
+                    mBookmarkItemsAdapter.getItemTouchHelper());
+            model.set(BookmarkManagerProperties.VIEW_HOLDER, holder);
+            model.set(BookmarkManagerProperties.IS_HIGHLIGHTED, id.equals(mHighlightedBookmark));
+            model.set(BookmarkManagerProperties.CLEAR_HIGHLIGHT, this::clearHighlight);
+        } else if (viewType == ViewType.SHOPPING_FILTER) {
+            model.set(BookmarkManagerProperties.OPEN_FOLDER, this::openFolder);
         }
-        mStateStack.push(state);
-        notifyUi(state);
+        return model;
     }
 
-    private void notifyUi(BookmarkUiState state) {
-        if (state.mState == BookmarkUiState.STATE_FOLDER) {
-            // Loading and searching states may be pushed to the stack but should never be stored in
-            // preferences.
-            BookmarkUtils.setLastUsedUrl(mContext, state.mUrl);
-            // If a loading state is replaced by another loading state, do not notify this change.
-            if (mNativePage != null) {
-                mNativePage.onStateChange(state.mUrl, false);
-            }
-        }
-
-        for (BookmarkUiObserver observer : mUiObservers) {
-            notifyStateChange(observer);
+    @Override
+    public void recycleView(View view, @ViewType int viewType) {
+        switch (viewType) {
+            case ViewType.PERSONALIZED_SIGNIN_PROMO:
+                // fall through
+            case ViewType.PERSONALIZED_SYNC_PROMO:
+                mPromoHeaderManager.detachPersonalizePromoView();
+                break;
+            default:
+                // Other view holders don't have special recycling code.
         }
     }
 
-    // TODO(lazzzis): This method can be moved to adapter after bookmark reordering launches.
-    /**
-     * Some bookmarks may be moved to another folder or removed in another devices. However, it may
-     * still be stored by {@link #mSelectionDelegate}, which causes incorrect selection counting.
-     */
-    private void syncAdapterAndSelectionDelegate() {
-        for (BookmarkId node : mSelectionDelegate.getSelectedItemsAsList()) {
-            if (mSelectionDelegate.isItemSelected(node)
-                    && mBookmarkItemsAdapter.getPositionForBookmark(node) == -1) {
-                mSelectionDelegate.toggleSelectionForItem(node);
-            }
+    @Override
+    public void setOrder(List<BookmarkListEntry> listEntries) {
+        assert !topLevelFoldersShowing() : "Cannot reorder top-level folders!";
+        assert mCurrentFolder.getType()
+                != BookmarkType.PARTNER : "Cannot reorder partner bookmarks!";
+        assert getCurrentUiMode()
+                == BookmarkUiMode.FOLDER : "Can only reorder items from folder mode!";
+
+        int startIndex = getBookmarkItemStartIndex();
+        int endIndex = getBookmarkItemEndIndex();
+
+        // Get the new order for the IDs.
+        long[] newOrder = new long[endIndex - startIndex + 1];
+        for (int i = startIndex; i <= endIndex; i++) {
+            BookmarkItem bookmarkItem = listEntries.get(i).getBookmarkItem();
+            assert bookmarkItem != null;
+            newOrder[i - startIndex] = bookmarkItem.getId().getId();
+        }
+        mBookmarkModel.reorderBookmarks(mCurrentFolder, newOrder);
+        if (mDragStateDelegate.getDragActive()) {
+            RecordUserAction.record("MobileBookmarkManagerDragReorder");
         }
     }
 
-    // BookmarkDelegate implementation
+    @Override
+    public boolean isReorderable(BookmarkListEntry entry) {
+        return entry != null && entry.getBookmarkItem() != null
+                && entry.getBookmarkItem().isReorderable();
+    }
+
+    // TestingDelegate implementation.
+
+    @Override
+    public BookmarkId getIdByPositionForTesting(int position) {
+        return getIdByPosition(position);
+    }
+
+    @Override
+    public void searchForTesting(@Nullable String query) {
+        search(query);
+    }
+
+    @Override
+    public void simulateSignInForTesting() {
+        mSyncStateChangedListener.syncStateChanged();
+        mBookmarkUiObserver.onFolderStateSet(mCurrentFolder);
+    }
+
+    // BookmarkDelegate implementation.
 
     @Override
     public void moveDownOne(BookmarkId bookmarkId) {
-        mBookmarkItemsAdapter.moveDownOne(bookmarkId);
+        int pos = getPositionForBookmark(bookmarkId);
+        assert isReorderable(getItemByPosition(pos));
+        getElements().remove(pos);
+        getElements().add(pos + 1,
+                BookmarkListEntry.createBookmarkEntry(mBookmarkModel.getBookmarkById(bookmarkId),
+                        mBookmarkModel.getPowerBookmarkMeta(bookmarkId)));
+        setOrder(getElements());
     }
 
     @Override
     public void moveUpOne(BookmarkId bookmarkId) {
-        mBookmarkItemsAdapter.moveUpOne(bookmarkId);
+        int pos = getPositionForBookmark(bookmarkId);
+        assert isReorderable(getItemByPosition(pos));
+        getElements().remove(pos);
+        getElements().add(pos - 1,
+                BookmarkListEntry.createBookmarkEntry(mBookmarkModel.getBookmarkById(bookmarkId),
+                        mBookmarkModel.getPowerBookmarkMeta(bookmarkId)));
+        setOrder(getElements());
     }
 
     @Override
@@ -401,15 +620,15 @@
 
     @Override
     public void notifyStateChange(BookmarkUiObserver observer) {
-        int state = getCurrentState();
-        observer.onStateChanged(state);
+        int state = getCurrentUiMode();
+        observer.onUiModeChanged(state);
         switch (state) {
-            case BookmarkUiState.STATE_FOLDER:
+            case BookmarkUiMode.FOLDER:
                 observer.onFolderStateSet(mStateStack.peek().mFolder);
                 break;
-            case BookmarkUiState.STATE_LOADING:
+            case BookmarkUiMode.LOADING:
                 break;
-            case BookmarkUiState.STATE_SEARCHING:
+            case BookmarkUiMode.SEARCHING:
                 observer.onSearchStateSet();
                 break;
             default:
@@ -463,9 +682,9 @@
     }
 
     @Override
-    public int getCurrentState() {
-        if (mStateStack.isEmpty()) return BookmarkUiState.STATE_LOADING;
-        return mStateStack.peek().mState;
+    public @BookmarkUiMode int getCurrentUiMode() {
+        if (mStateStack.isEmpty()) return BookmarkUiMode.LOADING;
+        return mStateStack.peek().mUiMode;
     }
 
     @Override
@@ -480,16 +699,15 @@
 
     @Override
     public void highlightBookmark(BookmarkId bookmarkId) {
-        mBookmarkItemsAdapter.highlightBookmark(bookmarkId);
+        assert mHighlightedBookmark == null : "There should not already be a highlighted bookmark!";
+
+        mRecyclerView.scrollToPosition(getPositionForBookmark(bookmarkId));
+        mHighlightedBookmark = bookmarkId;
     }
 
     // SearchDelegate implementation.
     // Actual interface implemented in BookmarkManager(Coordinator).
 
-    void onSearchTextChanged(String query) {
-        mBookmarkItemsAdapter.search(query);
-    }
-
     void onEndSearch() {
         mSelectableListLayout.onEndSearch();
 
@@ -515,13 +733,81 @@
     public void onCompletedFaviconLoading() {
         assert mBookmarkModel.isBookmarkModelLoaded();
         if (mFaviconsNeedRefresh) {
-            mBookmarkItemsAdapter.refresh();
+            refresh();
             mFaviconsNeedRefresh = false;
         }
     }
 
     // Private methods.
 
+    /**
+     * Puts all UI elements to loading state. This state might be overridden synchronously by
+     * {@link #updateForUrl(String)}, if the bookmark model is already loaded.
+     */
+    private void initializeToLoadingState() {
+        assert mStateStack.isEmpty();
+        setState(BookmarkUiState.createLoadingState());
+    }
+
+    /**
+     * This is the ultimate internal method that updates UI and controls backstack. And it is the
+     * only method that pushes states to {@link #mStateStack}.
+     *
+     * <p>If the given state is not valid, all_bookmark state will be shown. Afterwards, this method
+     * checks the current state: if currently in loading state, it pops it out and adds the new
+     * state to the back stack. It also notifies the {@link #mNativePage} (if any) that the
+     * url has changed.
+     *
+     * <p>Also note that even if we store states to {@link #mStateStack}, on tablet the back
+     * navigation and back button are not controlled by the manager: the tab handles back key and
+     * backstack navigation.
+     */
+    private void setState(BookmarkUiState state) {
+        if (!state.isValid(mBookmarkModel)) {
+            state = BookmarkUiState.createFolderState(
+                    mBookmarkModel.getDefaultFolderViewLocation(), mBookmarkModel);
+        }
+
+        if (!mStateStack.isEmpty() && mStateStack.peek().equals(state)) return;
+
+        // The loading state is not persisted in history stack and once we have a valid state it
+        // shall be removed.
+        if (!mStateStack.isEmpty() && mStateStack.peek().mUiMode == BookmarkUiMode.LOADING) {
+            mStateStack.pop();
+        }
+        mStateStack.push(state);
+        notifyUi(state);
+    }
+
+    private void notifyUi(BookmarkUiState state) {
+        if (state.mUiMode == BookmarkUiMode.FOLDER) {
+            // Loading and searching states may be pushed to the stack but should never be stored in
+            // preferences.
+            BookmarkUtils.setLastUsedUrl(mContext, state.mUrl);
+            // If a loading state is replaced by another loading state, do not notify this change.
+            if (mNativePage != null) {
+                mNativePage.onStateChange(state.mUrl, false);
+            }
+        }
+
+        for (BookmarkUiObserver observer : mUiObservers) {
+            notifyStateChange(observer);
+        }
+    }
+
+    // TODO(lazzzis): This method can be moved to adapter after bookmark reordering launches.
+    /**
+     * Some bookmarks may be moved to another folder or removed in another devices. However, it may
+     * still be stored by {@link #mSelectionDelegate}, which causes incorrect selection counting.
+     */
+    private void syncAdapterAndSelectionDelegate() {
+        for (BookmarkId node : mSelectionDelegate.getSelectedItemsAsList()) {
+            if (mSelectionDelegate.isItemSelected(node) && getPositionForBookmark(node) == -1) {
+                mSelectionDelegate.toggleSelectionForItem(node);
+            }
+        }
+    }
+
     private void onBackPressStateChanged() {
         if (mIsDestroyed) {
             mBackPressStateSupplier.set(false);
@@ -532,8 +818,196 @@
                 || mStateStack.size() > 1);
     }
 
-    // Testing methods.
+    /** @return The position of the given bookmark in adapter. Will return -1 if not found. */
+    private int getPositionForBookmark(BookmarkId bookmark) {
+        assert bookmark != null;
+        int position = -1;
+        for (int i = 0; i < getItemCount(); i++) {
+            if (bookmark.equals(getIdByPosition(i))) {
+                position = i;
+                break;
+            }
+        }
+        return position;
+    }
 
+    private void filterForPriceTrackingCategory(List<BookmarkId> bookmarks) {
+        for (int i = bookmarks.size() - 1; i >= 0; i--) {
+            org.chromium.components.power_bookmarks.PowerBookmarkMeta meta =
+                    mBookmarkModel.getPowerBookmarkMeta(bookmarks.get(i));
+            if (meta == null || !meta.hasShoppingSpecifics()
+                    || !meta.getShoppingSpecifics().getIsPriceTracked()) {
+                bookmarks.remove(i);
+                continue;
+            }
+        }
+    }
+
+    @SuppressWarnings("NotifyDataSetChanged")
+    private void setBookmarks(List<BookmarkId> bookmarks) {
+        clearHighlight();
+        getElements().clear();
+
+        // Restore the header, if it exists, then update it.
+        if (hasPromoHeader()) {
+            getElements().add(BookmarkListEntry.createSyncPromoHeader(mPromoHeaderType));
+        }
+
+        updateHeader(false);
+        if (BookmarkId.SHOPPING_FOLDER.equals(mCurrentFolder)) {
+            filterForPriceTrackingCategory(bookmarks);
+        }
+
+        for (BookmarkId bookmarkId : bookmarks) {
+            BookmarkItem item = mBookmarkModel.getBookmarkById(bookmarkId);
+
+            getElements().add(BookmarkListEntry.createBookmarkEntry(
+                    item, mBookmarkModel.getPowerBookmarkMeta(bookmarkId)));
+        }
+
+        if (mCurrentFolder.getType() == BookmarkType.READING_LIST
+                && getCurrentUiMode() != BookmarkUiMode.SEARCHING) {
+            ReadingListSectionHeader.maybeSortAndInsertSectionHeaders(getElements(), mContext);
+        }
+
+        if (ChromeFeatureList.isEnabled(ChromeFeatureList.SHOPPING_LIST)
+                && topLevelFoldersShowing()) {
+            getElements().add(BookmarkListEntry.createDivider());
+            getElements().add(BookmarkListEntry.createShoppingFilter());
+        }
+
+        mBookmarkItemsAdapter.notifyDataSetChanged();
+    }
+
+    private void removeItem(int position) {
+        getElements().remove(position);
+        mBookmarkItemsAdapter.notifyItemRemoved(position);
+    }
+
+    /** Refresh the list of bookmarks within the currently visible folder. */
+    @SuppressWarnings("NotifyDataSetChanged")
+    private void refresh() {
+        // Tell the RecyclerView to update its elements.
+        if (getElements() != null) mBookmarkItemsAdapter.notifyDataSetChanged();
+    }
+
+    /**
+     * Updates mPromoHeaderType. Makes sure that the 0th index of getElements() is consistent with
+     * the promo header. This 0th index is null iff there is a promo header.
+     *
+     * @param shouldNotify True iff we should notify the RecyclerView of changes to the promoheader.
+     *                     (This should be false iff we are going to make further changes to the
+     *                     list of elements, as we do in setBookmarks, and true iff we are only
+     *                     changing the header, as we do in the promoHeaderChangeAction runnable).
+     */
+    private void updateHeader(boolean shouldNotify) {
+        boolean wasShowingPromo = hasPromoHeader();
+
+        int currentUiState = getCurrentUiMode();
+        if (currentUiState == BookmarkUiMode.LOADING) {
+            return;
+        } else if (currentUiState == BookmarkUiMode.SEARCHING) {
+            mPromoHeaderType = ViewType.INVALID;
+        } else {
+            switch (mPromoHeaderManager.getPromoState()) {
+                case SyncPromoState.NO_PROMO:
+                    mPromoHeaderType = ViewType.INVALID;
+                    break;
+                case SyncPromoState.PROMO_FOR_SIGNED_OUT_STATE:
+                    mPromoHeaderType = ViewType.PERSONALIZED_SIGNIN_PROMO;
+                    break;
+                case SyncPromoState.PROMO_FOR_SIGNED_IN_STATE:
+                    mPromoHeaderType = ViewType.PERSONALIZED_SYNC_PROMO;
+                    break;
+                case SyncPromoState.PROMO_FOR_SYNC_TURNED_OFF_STATE:
+                    mPromoHeaderType = ViewType.SYNC_PROMO;
+                    break;
+                default:
+                    assert false : "Unexpected value for promo state!";
+            }
+        }
+
+        boolean willShowPromo = hasPromoHeader();
+        if (!wasShowingPromo && willShowPromo) {
+            // A null element at the 0th index represents a promo header.
+            getElements().add(0, BookmarkListEntry.createSyncPromoHeader(mPromoHeaderType));
+            if (shouldNotify) mBookmarkItemsAdapter.notifyItemInserted(0);
+        } else if (wasShowingPromo && willShowPromo) {
+            if (shouldNotify) mBookmarkItemsAdapter.notifyItemChanged(0);
+        } else if (wasShowingPromo && !willShowPromo) {
+            getElements().remove(0);
+            if (shouldNotify) mBookmarkItemsAdapter.notifyItemRemoved(0);
+        }
+    }
+
+    /** Removes all section headers from the current list. */
+    private void removeSectionHeaders() {
+        for (int i = getElements().size() - 1; i >= 0; i--) {
+            if (getElements().get(i).getViewType() == ViewType.SECTION_HEADER) {
+                getElements().remove(i);
+            }
+        }
+    }
+
+    private void populateTopLevelFoldersList() {
+        mTopLevelFolders.addAll(BookmarkUtils.populateTopLevelFolders(mBookmarkModel));
+    }
+
+    private int getBookmarkItemStartIndex() {
+        return hasPromoHeader() ? 1 : 0;
+    }
+
+    private int getBookmarkItemEndIndex() {
+        int endIndex = getElements().size() - 1;
+        BookmarkItem bookmarkItem = getElements().get(endIndex).getBookmarkItem();
+        if (bookmarkItem == null || !BookmarkUtils.isMovable(bookmarkItem)) {
+            endIndex--;
+        }
+        return endIndex;
+    }
+
+    private boolean hasPromoHeader() {
+        return mPromoHeaderType != ViewType.INVALID;
+    }
+
+    private @Location int getLocationFromPosition(int position) {
+        if (position == getBookmarkItemStartIndex() && position == getBookmarkItemEndIndex()) {
+            return Location.SOLO;
+        } else if (position == getBookmarkItemStartIndex()) {
+            return Location.TOP;
+        } else if (position == getBookmarkItemEndIndex()) {
+            return Location.BOTTOM;
+        } else {
+            return Location.MIDDLE;
+        }
+    }
+
+    /**
+     * Return true iff the currently-open folder is the root folder
+     *         (which is true iff the top-level folders are showing)
+     */
+    private boolean topLevelFoldersShowing() {
+        return mCurrentFolder.equals(mBookmarkModel.getRootFolderId());
+    }
+
+    /** Clears the highlighted bookmark, if there is one. */
+    private void clearHighlight() {
+        mHighlightedBookmark = null;
+    }
+
+    private BookmarkListEntry getItemByPosition(int position) {
+        return getElements().get(position);
+    }
+
+    private List<BookmarkListEntry> getElements() {
+        return mBookmarkItemsAdapter.getElements();
+    }
+
+    private int getItemCount() {
+        return getElements().size();
+    }
+
+    // Testing methods.
     /** Whether to prevent the bookmark model from fully loading for testing. */
     static void preventLoadingForTesting(boolean preventLoading) {
         sPreventLoadingForTesting = preventLoading;
@@ -542,4 +1016,8 @@
     void clearStateStackForTesting() {
         mStateStack.clear();
     }
+
+    BookmarkUndoController getUndoControllerForTesting() {
+        return mBookmarkUndoController;
+    }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkRow.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkRow.java
index 84d7ecb..f58ffc35 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkRow.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkRow.java
@@ -21,6 +21,7 @@
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.app.bookmarks.BookmarkAddEditFolderActivity;
 import org.chromium.chrome.browser.app.bookmarks.BookmarkFolderSelectActivity;
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
 import org.chromium.components.bookmarks.BookmarkId;
 import org.chromium.components.bookmarks.BookmarkItem;
 import org.chromium.components.bookmarks.BookmarkType;
@@ -190,9 +191,9 @@
             listItems.add(buildMenuListItem(R.string.bookmark_item_delete, 0, 0));
         }
 
-        if (mDelegate.getCurrentState() == BookmarkUiState.STATE_SEARCHING) {
+        if (mDelegate.getCurrentUiMode() == BookmarkUiMode.SEARCHING) {
             listItems.add(buildMenuListItem(R.string.bookmark_show_in_folder, 0, 0));
-        } else if (mDelegate.getCurrentState() == BookmarkUiState.STATE_FOLDER
+        } else if (mDelegate.getCurrentUiMode() == BookmarkUiMode.FOLDER
                 && mLocation != Location.SOLO && canReorder) {
             // Only add move up / move down buttons if there is more than 1 item
             if (mLocation != Location.TOP) {
@@ -316,7 +317,6 @@
     @Override
     public void onSearchStateSet() {}
 
-    @VisibleForTesting
     public boolean isItemSelected() {
         return mDelegate.getSelectionDelegate().isItemSelected(mBookmarkId);
     }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbar.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbar.java
index d23b07a..04e5047 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbar.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbar.java
@@ -18,6 +18,7 @@
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.app.bookmarks.BookmarkAddEditFolderActivity;
 import org.chromium.chrome.browser.app.bookmarks.BookmarkFolderSelectActivity;
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
 import org.chromium.chrome.browser.incognito.IncognitoUtils;
 import org.chromium.components.bookmarks.BookmarkId;
 import org.chromium.components.bookmarks.BookmarkItem;
@@ -42,7 +43,7 @@
     // TODO(crbug.com/1413463): Remove BookmarkId reference.
     private BookmarkId mCurrentFolderId;
     private BookmarkItem mCurrentFolder;
-    private int mBookmarkUiState;
+    private @BookmarkUiMode int mBookmarkUiMode;
 
     private Runnable mOpenSearchUiRunnable;
     private Callback<BookmarkId> mOpenFolderCallback;
@@ -82,21 +83,21 @@
         getMenu().setGroupEnabled(R.id.selection_mode_menu_group, true);
     }
 
-    void setBookmarkUiState(int state) {
-        mBookmarkUiState = state;
-        if (mBookmarkUiState == BookmarkUiState.STATE_LOADING) {
+    void setBookmarkUiMode(@BookmarkUiMode int mode) {
+        mBookmarkUiMode = mode;
+        if (mBookmarkUiMode == BookmarkUiMode.LOADING) {
             showLoadingUi();
         } else {
             showNormalView();
         }
 
-        if (state == BookmarkUiState.STATE_SEARCHING) {
+        if (mBookmarkUiMode == BookmarkUiMode.SEARCHING) {
             showSearchView(/*showKeyboard=*/true);
         } else {
             hideSearchView(/*notify=*/false);
         }
 
-        if (mBookmarkUiState == BookmarkUiState.STATE_FOLDER && mCurrentFolder != null) {
+        if (mBookmarkUiMode == BookmarkUiMode.FOLDER && mCurrentFolder != null) {
             // It's possible that the folder was renamed, so refresh the folder UI just in case.
             setCurrentFolder(mCurrentFolder.getId());
         }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarCoordinator.java
index 1dc8c4f..d76f89a 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarCoordinator.java
@@ -6,6 +6,7 @@
 
 import org.chromium.base.supplier.OneshotSupplier;
 import org.chromium.chrome.R;
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
 import org.chromium.components.bookmarks.BookmarkId;
 import org.chromium.components.browser_ui.widget.selectable_list.SelectableListLayout;
 import org.chromium.components.browser_ui.widget.selectable_list.SelectableListToolbar.SearchDelegate;
@@ -34,7 +35,7 @@
         mModel.set(BookmarkToolbarProperties.BOOKMARK_MODEL, bookmarkModel);
         mModel.set(BookmarkToolbarProperties.BOOKMARK_OPENER, bookmarkOpener);
         mModel.set(BookmarkToolbarProperties.SELECTION_DELEGATE, selectionDelegate);
-        mModel.set(BookmarkToolbarProperties.BOOKMARK_UI_STATE, BookmarkUiState.STATE_LOADING);
+        mModel.set(BookmarkToolbarProperties.BOOKMARK_UI_STATE, BookmarkUiMode.LOADING);
         mModel.set(BookmarkToolbarProperties.IS_DIALOG_UI, isDialogUi);
         mModel.set(BookmarkToolbarProperties.DRAG_ENABLED, false);
         mMediator =
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarMediator.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarMediator.java
index 2fca096..15ec9003 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarMediator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarMediator.java
@@ -49,8 +49,8 @@
     }
 
     @Override
-    public void onStateChanged(int state) {
-        mModel.set(BookmarkToolbarProperties.BOOKMARK_UI_STATE, state);
+    public void onUiModeChanged(int mode) {
+        mModel.set(BookmarkToolbarProperties.BOOKMARK_UI_STATE, mode);
     }
 
     @Override
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarViewBinder.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarViewBinder.java
index 7a08e70..cc3128dd 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarViewBinder.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarViewBinder.java
@@ -22,7 +22,7 @@
             bookmarkToolbar.setSelectionDelegate(
                     model.get(BookmarkToolbarProperties.SELECTION_DELEGATE));
         } else if (key == BookmarkToolbarProperties.BOOKMARK_UI_STATE) {
-            bookmarkToolbar.setBookmarkUiState(
+            bookmarkToolbar.setBookmarkUiMode(
                     model.get(BookmarkToolbarProperties.BOOKMARK_UI_STATE));
         } else if (key == BookmarkToolbarProperties.SOFT_KEYBOARD_VISIBLE) {
             bookmarkToolbar.setSoftKeyboardVisible(Boolean.TRUE.equals(
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUiObserver.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUiObserver.java
index 27661a49..cf99836 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUiObserver.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUiObserver.java
@@ -4,6 +4,7 @@
 
 package org.chromium.chrome.browser.bookmarks;
 
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
 import org.chromium.components.bookmarks.BookmarkId;
 
 /**
@@ -18,12 +19,12 @@
     /** @see BookmarkDelegate#openFolder(BookmarkId) */
     default void onFolderStateSet(BookmarkId folder) {}
 
-    /** Called when the UI state is set to {@link BookmarkUiState#STATE_SEARCHING}. */
+    /** Called when the UI state is set to {@link BookmarkUiMode.SEARCHING}. */
     default void onSearchStateSet() {}
 
     /** Called when a bookmark menu item is opened. */
     default void onBookmarkItemMenuOpened() {}
 
-    /** Called when the bookmark UI state changes. */
-    default void onStateChanged(int state) {}
+    /** Called when the bookmark UI mode changes. */
+    default void onUiModeChanged(@BookmarkUiMode int mode) {}
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUiState.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUiState.java
index 7a06a14..be783f8 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUiState.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUiState.java
@@ -7,62 +7,64 @@
 import android.net.Uri;
 import android.text.TextUtils;
 
+import androidx.annotation.IntDef;
+
 import org.chromium.components.bookmarks.BookmarkId;
 import org.chromium.components.embedder_support.util.UrlConstants;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * A class representing the UI state of the {@link BookmarkManager}. All
  * states can be uniquely identified by a URL.
  */
 public class BookmarkUiState {
-    // TODO(crbug.com/1419494): Use an intdef here instead.
-    public static final int STATE_LOADING = 1;
-    public static final int STATE_FOLDER = 2;
-    public static final int STATE_SEARCHING = 3;
-    private static final int STATE_INVALID = 0;
+    @IntDef({BookmarkUiMode.INVALID, BookmarkUiMode.LOADING, BookmarkUiMode.FOLDER,
+            BookmarkUiMode.SEARCHING})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BookmarkUiMode {
+        int INVALID = 0;
+        int LOADING = 1;
+        int FOLDER = 2;
+        int SEARCHING = 3;
+    }
+
     private static final String SHOPPING_FILTER_URL =
             UrlConstants.BOOKMARKS_FOLDER_URL + "/shopping";
 
-    /**
-     * One of the STATE_* constants.
-     */
-    int mState;
-    String mUrl;
-    BookmarkId mFolder;
+    final @BookmarkUiMode int mUiMode;
+    final String mUrl;
+    final BookmarkId mFolder;
 
     static BookmarkUiState createLoadingState() {
-        BookmarkUiState state = new BookmarkUiState();
-        state.mState = STATE_LOADING;
-        state.mUrl = "";
-        return state;
+        return new BookmarkUiState(BookmarkUiMode.LOADING, "", null);
     }
 
     static BookmarkUiState createSearchState() {
-        BookmarkUiState state = new BookmarkUiState();
-        state.mState = STATE_SEARCHING;
-        state.mUrl = "";
-        return state;
+        return new BookmarkUiState(BookmarkUiMode.SEARCHING, "", null);
     }
 
     static BookmarkUiState createShoppingFilterState() {
-        BookmarkUiState state = new BookmarkUiState();
-        state.mState = STATE_FOLDER;
-        state.mUrl = SHOPPING_FILTER_URL;
-        state.mFolder = BookmarkId.SHOPPING_FOLDER;
-        return state;
+        return new BookmarkUiState(
+                BookmarkUiMode.FOLDER, SHOPPING_FILTER_URL, BookmarkId.SHOPPING_FOLDER);
     }
 
     static BookmarkUiState createFolderState(BookmarkId folder, BookmarkModel bookmarkModel) {
-        if (BookmarkId.SHOPPING_FOLDER.equals(folder)) return createShoppingFilterState();
-        return createStateFromUrl(createFolderUrl(folder), bookmarkModel);
+        if (BookmarkId.SHOPPING_FOLDER.equals(folder)) {
+            return createShoppingFilterState();
+        } else {
+            return createStateFromUrl(createFolderUrl(folder), bookmarkModel);
+        }
     }
 
-    /**
-     * @see #createStateFromUrl(Uri, BookmarkModel)
-     */
+    /** @see #createStateFromUrl(Uri, BookmarkModel). */
     static BookmarkUiState createStateFromUrl(String url, BookmarkModel bookmarkModel) {
-        if (SHOPPING_FILTER_URL.equals(url)) return createShoppingFilterState();
-        return createStateFromUrl(Uri.parse(url), bookmarkModel);
+        if (SHOPPING_FILTER_URL.equals(url)) {
+            return createShoppingFilterState();
+        } else {
+            return createStateFromUrl(Uri.parse(url), bookmarkModel);
+        }
     }
 
     /**
@@ -70,25 +72,24 @@
      *         return all_bookmarks.
      */
     static BookmarkUiState createStateFromUrl(Uri uri, BookmarkModel bookmarkModel) {
-        BookmarkUiState state = new BookmarkUiState();
-        state.mState = STATE_INVALID;
-        state.mUrl = uri.toString();
+        String url = uri.toString();
 
-        if (state.mUrl.equals(UrlConstants.BOOKMARKS_URL)) {
+        BookmarkUiState tempState = null;
+        if (url.equals(UrlConstants.BOOKMARKS_URL)) {
             return createFolderState(bookmarkModel.getDefaultFolderViewLocation(), bookmarkModel);
-        } else if (state.mUrl.startsWith(UrlConstants.BOOKMARKS_FOLDER_URL)) {
+        } else if (url.startsWith(UrlConstants.BOOKMARKS_FOLDER_URL)) {
             String path = uri.getLastPathSegment();
             if (!path.isEmpty()) {
-                state.mFolder = BookmarkId.getBookmarkIdFromString(path);
-                state.mState = STATE_FOLDER;
+                tempState = new BookmarkUiState(
+                        BookmarkUiMode.FOLDER, url, BookmarkId.getBookmarkIdFromString(path));
             }
         }
 
-        if (!state.isValid(bookmarkModel)) {
-            state = createFolderState(bookmarkModel.getDefaultFolderViewLocation(), bookmarkModel);
+        if (tempState != null && tempState.isValid(bookmarkModel)) {
+            return tempState;
+        } else {
+            return createFolderState(bookmarkModel.getDefaultFolderViewLocation(), bookmarkModel);
         }
-
-        return state;
     }
 
     public static Uri createFolderUrl(BookmarkId folderId) {
@@ -99,28 +100,32 @@
         return builder.build();
     }
 
-    private BookmarkUiState() {}
+    private BookmarkUiState(@BookmarkUiMode int uiMode, String url, BookmarkId folder) {
+        mUiMode = uiMode;
+        mUrl = url;
+        mFolder = folder;
+    }
 
     @Override
     public int hashCode() {
-        return 31 * mUrl.hashCode() + mState;
+        return 31 * mUrl.hashCode() + mUiMode;
     }
 
     @Override
     public boolean equals(Object obj) {
         if (!(obj instanceof BookmarkUiState)) return false;
         BookmarkUiState other = (BookmarkUiState) obj;
-        return mState == other.mState && TextUtils.equals(mUrl, other.mUrl);
+        return mUiMode == other.mUiMode && TextUtils.equals(mUrl, other.mUrl);
     }
 
     /**
      * @return Whether this state is valid.
      */
     boolean isValid(BookmarkModel bookmarkModel) {
-        if (mUrl == null || mState == STATE_INVALID) return false;
+        if (mUrl == null || mUiMode == BookmarkUiMode.INVALID) return false;
         if (mUrl.equals(SHOPPING_FILTER_URL)) return true;
 
-        if (mState == STATE_FOLDER) {
+        if (mUiMode == BookmarkUiMode.FOLDER) {
             return mFolder != null && bookmarkModel.doesBookmarkExist(mFolder);
         }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUndoController.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUndoController.java
index 31fce5e..88a0759 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUndoController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUndoController.java
@@ -124,7 +124,7 @@
         // Adding a new bookmark should not affect undo.
     }
 
-    // BookmarkDeleteObserver implementation
+    // BookmarkDeleteObserver implementation.
 
     @Override
     public void onDeleteBookmarks(String[] titles, boolean isUndoable) {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/PowerBookmarkMetrics.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/PowerBookmarkMetrics.java
index 2e82107..c66c7d0 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/PowerBookmarkMetrics.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/PowerBookmarkMetrics.java
@@ -14,7 +14,7 @@
     // numeric values should never be reused. Keep up-to-date with the PriceTrackingState enum in
     // tools/metrics/histograms/enums.xml.
     @IntDef({PriceTrackingState.PRICE_TRACKING_SHOWN, PriceTrackingState.PRICE_TRACKING_ENABLED,
-            PriceTrackingState.PRICE_TRACKING_DISABLED})
+            PriceTrackingState.PRICE_TRACKING_DISABLED, PriceTrackingState.COUNT})
     public @interface PriceTrackingState {
         int PRICE_TRACKING_SHOWN = 0;
         int PRICE_TRACKING_ENABLED = 1;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/TestingDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/TestingDelegate.java
new file mode 100644
index 0000000..0726a3f
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/TestingDelegate.java
@@ -0,0 +1,18 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.bookmarks;
+
+import androidx.annotation.Nullable;
+
+import org.chromium.components.bookmarks.BookmarkId;
+
+/** Exposes business logic methods to the various bookmark integration. */
+public interface TestingDelegate {
+    BookmarkId getIdByPositionForTesting(int position);
+
+    void searchForTesting(@Nullable String query);
+
+    void simulateSignInForTesting();
+}
\ No newline at end of file
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/BookmarkTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/BookmarkTest.java
index bf78788b..b083016 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/BookmarkTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/BookmarkTest.java
@@ -73,8 +73,10 @@
 import org.chromium.chrome.browser.bookmarks.BookmarkRow;
 import org.chromium.chrome.browser.bookmarks.BookmarkToolbar;
 import org.chromium.chrome.browser.bookmarks.BookmarkUiState;
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
 import org.chromium.chrome.browser.bookmarks.BookmarkUtils;
 import org.chromium.chrome.browser.bookmarks.PowerBookmarkShoppingItemRow;
+import org.chromium.chrome.browser.bookmarks.TestingDelegate;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.night_mode.ChromeNightModeTestUtils;
@@ -424,7 +426,7 @@
         BookmarkTestUtil.waitForBookmarkModelLoaded();
         BookmarkDelegate delegate = getBookmarkDelegate();
 
-        Assert.assertEquals(BookmarkUiState.STATE_FOLDER, delegate.getCurrentState());
+        Assert.assertEquals(BookmarkUiMode.FOLDER, delegate.getCurrentUiMode());
         Assert.assertEquals("chrome-native://bookmarks/folder/3",
                 BookmarkUtils.getLastUsedUrl(mActivityTestRule.getActivity()));
     }
@@ -499,13 +501,13 @@
         // Open the new folder where these bookmarks were created.
         openFolder(folder);
 
-        Assert.assertEquals(BookmarkUiState.STATE_FOLDER, delegate.getCurrentState());
+        Assert.assertEquals(BookmarkUiMode.FOLDER, delegate.getCurrentUiMode());
         Assert.assertEquals(
                 "Wrong number of items before starting search.", 3, adapter.getItemCount());
 
         TestThreadUtils.runOnUiThreadBlocking(delegate::openSearchUi);
 
-        Assert.assertEquals(BookmarkUiState.STATE_SEARCHING, delegate.getCurrentState());
+        Assert.assertEquals(BookmarkUiMode.SEARCHING, delegate.getCurrentUiMode());
         Assert.assertEquals(
                 "Wrong number of items after showing search UI. The promo should be hidden.", 2,
                 adapter.getItemCount());
@@ -530,7 +532,7 @@
                 () -> mBookmarkManagerCoordinator.getToolbarForTesting().hideSearchView());
         Assert.assertEquals("Wrong number of items after closing search UI.", 3,
                 mItemsContainer.getAdapter().getItemCount());
-        Assert.assertEquals(BookmarkUiState.STATE_FOLDER, delegate.getCurrentState());
+        Assert.assertEquals(BookmarkUiMode.FOLDER, delegate.getCurrentUiMode());
     }
 
     @Test
@@ -555,7 +557,7 @@
 
         TestThreadUtils.runOnUiThreadBlocking(delegate::openSearchUi);
 
-        Assert.assertEquals(BookmarkUiState.STATE_SEARCHING, delegate.getCurrentState());
+        Assert.assertEquals(BookmarkUiMode.SEARCHING, delegate.getCurrentUiMode());
         Assert.assertEquals(
                 "Wrong number of items after showing search UI. The promo should be hidden.", 2,
                 adapter.getItemCount());
@@ -566,7 +568,7 @@
         // Exit search UI.
         TestThreadUtils.runOnUiThreadBlocking(
                 mBookmarkActivity.getOnBackPressedDispatcher()::onBackPressed);
-        Assert.assertNotEquals(BookmarkUiState.STATE_SEARCHING, delegate.getCurrentState());
+        Assert.assertNotEquals(BookmarkUiMode.SEARCHING, delegate.getCurrentUiMode());
 
         // Enter search UI again.
         TestThreadUtils.runOnUiThreadBlocking(delegate::openSearchUi);
@@ -591,21 +593,21 @@
         // Clear selection but still in search UI.
         CriteriaHelper.pollUiThread(
                 () -> !itemView.isChecked(), "Expected item \"test\" to become not selected");
-        Assert.assertEquals(BookmarkUiState.STATE_SEARCHING, delegate.getCurrentState());
+        Assert.assertEquals(BookmarkUiMode.SEARCHING, delegate.getCurrentUiMode());
         Assert.assertEquals(Boolean.TRUE,
                 mBookmarkManagerCoordinator.getHandleBackPressChangedSupplier().get());
 
         // Exit search UI.
         TestThreadUtils.runOnUiThreadBlocking(
                 mBookmarkActivity.getOnBackPressedDispatcher()::onBackPressed);
-        Assert.assertEquals(BookmarkUiState.STATE_FOLDER, delegate.getCurrentState());
+        Assert.assertEquals(BookmarkUiMode.FOLDER, delegate.getCurrentUiMode());
 
         // Exit folder.
         Assert.assertEquals(Boolean.TRUE,
                 mBookmarkManagerCoordinator.getHandleBackPressChangedSupplier().get());
         TestThreadUtils.runOnUiThreadBlocking(
                 mBookmarkActivity.getOnBackPressedDispatcher()::onBackPressed);
-        Assert.assertEquals(BookmarkUiState.STATE_FOLDER, delegate.getCurrentState());
+        Assert.assertEquals(BookmarkUiMode.FOLDER, delegate.getCurrentUiMode());
 
         // Exit bookmark activity.
         Assert.assertEquals(Boolean.FALSE,
@@ -630,8 +632,8 @@
         // Open the new folder where these bookmarks were created.
         openFolder(testFolder);
 
-        Assert.assertEquals("Wrong state, should be in folder", BookmarkUiState.STATE_FOLDER,
-                getBookmarkDelegate().getCurrentState());
+        Assert.assertEquals("Wrong state, should be in folder", BookmarkUiMode.FOLDER,
+                getBookmarkDelegate().getCurrentUiMode());
         Assert.assertEquals(
                 "Wrong number of items before starting search.", 3, adapter.getItemCount());
 
@@ -639,8 +641,8 @@
         // are currently testFolder's children (3).
         TestThreadUtils.runOnUiThreadBlocking(getBookmarkDelegate()::openSearchUi);
         RecyclerViewTestUtils.waitForStableRecyclerView(mItemsContainer);
-        Assert.assertEquals("Wrong state, should be searching", BookmarkUiState.STATE_SEARCHING,
-                getBookmarkDelegate().getCurrentState());
+        Assert.assertEquals("Wrong state, should be searching", BookmarkUiMode.SEARCHING,
+                getBookmarkDelegate().getCurrentUiMode());
         Assert.assertEquals(
                 "Wrong number of items before starting search.", 3, adapter.getItemCount());
 
@@ -669,8 +671,8 @@
 
         // The user should still be searching, and the bookmark should be gone. We're refreshing
         // the search query again here, but in this case it's now "Google".
-        Assert.assertEquals("Wrong state, should be searching", BookmarkUiState.STATE_SEARCHING,
-                getBookmarkDelegate().getCurrentState());
+        Assert.assertEquals("Wrong state, should be searching", BookmarkUiMode.SEARCHING,
+                getBookmarkDelegate().getCurrentUiMode());
         Assert.assertEquals("Wrong number of items after searching.", 0,
                 mItemsContainer.getAdapter().getItemCount());
 
@@ -680,8 +682,8 @@
 
         // The user should still be searching, and the bookmark should reappear. Refreshing the
         // search yet again, now with the "Google" search matching returning 1 result.
-        Assert.assertEquals("Wrong state, should be searching", BookmarkUiState.STATE_SEARCHING,
-                getBookmarkDelegate().getCurrentState());
+        Assert.assertEquals("Wrong state, should be searching", BookmarkUiMode.SEARCHING,
+                getBookmarkDelegate().getCurrentUiMode());
         Assert.assertEquals("Wrong number of items after searching.", 1,
                 mItemsContainer.getAdapter().getItemCount());
     }
@@ -695,12 +697,10 @@
         addBookmark(TEST_PAGE_TITLE_FOO, mTestPageFoo, testFolder);
         openBookmarkManager();
 
-        RecyclerView.Adapter adapter = getAdapter();
-
         // Start searching, enter a query.
         TestThreadUtils.runOnUiThreadBlocking(getBookmarkDelegate()::openSearchUi);
-        Assert.assertEquals("Wrong state, should be searching", BookmarkUiState.STATE_SEARCHING,
-                getBookmarkDelegate().getCurrentState());
+        Assert.assertEquals("Wrong state, should be searching", BookmarkUiMode.SEARCHING,
+                getBookmarkDelegate().getCurrentUiMode());
         searchBookmarks("test");
         Assert.assertEquals("Wrong number of items after searching.", 2,
                 mItemsContainer.getAdapter().getItemCount());
@@ -709,8 +709,8 @@
         removeBookmark(testFolder);
 
         // The user should still be searching, and the bookmark should be gone.
-        Assert.assertEquals("Wrong state, should be searching", BookmarkUiState.STATE_SEARCHING,
-                getBookmarkDelegate().getCurrentState());
+        Assert.assertEquals("Wrong state, should be searching", BookmarkUiMode.SEARCHING,
+                getBookmarkDelegate().getCurrentUiMode());
         Assert.assertEquals("Wrong number of items after searching.", 0,
                 mItemsContainer.getAdapter().getItemCount());
 
@@ -719,8 +719,8 @@
                 () -> mBookmarkManagerCoordinator.getUndoControllerForTesting().onAction(null));
 
         // The user should still be searching, and the bookmark should reappear.
-        Assert.assertEquals("Wrong state, should be searching", BookmarkUiState.STATE_SEARCHING,
-                getBookmarkDelegate().getCurrentState());
+        Assert.assertEquals("Wrong state, should be searching", BookmarkUiMode.SEARCHING,
+                getBookmarkDelegate().getCurrentUiMode());
         Assert.assertEquals("Wrong number of items after searching.", 2,
                 mItemsContainer.getAdapter().getItemCount());
     }
@@ -1342,7 +1342,7 @@
                 getAdapter().getItemCount());
 
         // Verify that bookmark 1 is editable (so more button can be triggered) but not movable.
-        BookmarkId partnerBookmarkId1 = getReorderAdapter().getIdByPosition(0);
+        BookmarkId partnerBookmarkId1 = getIdByPosition(0);
         TestThreadUtils.runOnUiThreadBlocking(() -> {
             BookmarkItem partnerBookmarkItem1 = mBookmarkModel.getBookmarkById(partnerBookmarkId1);
             partnerBookmarkItem1.forceEditableForTesting();
@@ -1362,7 +1362,7 @@
         onView(withText("Move down")).check(doesNotExist());
 
         // Verify that bookmark 2 is not movable.
-        BookmarkId partnerBookmarkId2 = getReorderAdapter().getIdByPosition(1);
+        BookmarkId partnerBookmarkId2 = getIdByPosition(1);
         TestThreadUtils.runOnUiThreadBlocking(() -> {
             BookmarkItem partnerBookmarkItem2 = mBookmarkModel.getBookmarkById(partnerBookmarkId2);
             partnerBookmarkItem2.forceEditableForTesting();
@@ -1397,7 +1397,7 @@
                     mBookmarkModel.getOtherFolderId(), 0, TEST_TITLE_A, mTestUrlA));
         });
 
-        TestThreadUtils.runOnUiThreadBlocking(adapter::simulateSignInForTesting);
+        TestThreadUtils.runOnUiThreadBlocking(getTestingDelegate()::simulateSignInForTesting);
         Assert.assertEquals(
                 "Expected promo, \"Reading List\", \"Mobile Bookmarks\" and \"Other Bookmarks\" folder to appear!",
                 4, adapter.getItemCount());
@@ -1911,6 +1911,10 @@
         return (BookmarkItemsAdapter) getAdapter();
     }
 
+    private TestingDelegate getTestingDelegate() {
+        return mBookmarkManagerCoordinator.getTestingDelegate();
+    }
+
     private void enterSearch() throws Exception {
         View searchButton = mBookmarkManagerCoordinator.getToolbarForTesting().findViewById(
                 R.id.search_menu_id);
@@ -2000,11 +2004,11 @@
     }
 
     private BookmarkId getIdByPosition(int pos) {
-        return getReorderAdapter().getIdByPosition(pos);
+        return getTestingDelegate().getIdByPositionForTesting(pos);
     }
 
     private void searchBookmarks(final String query) {
-        TestThreadUtils.runOnUiThreadBlocking(() -> getReorderAdapter().search(query));
+        TestThreadUtils.runOnUiThreadBlocking(() -> getTestingDelegate().searchForTesting(query));
     }
 
     private void openFolder(BookmarkId folder) {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/ReadingListTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/ReadingListTest.java
index 7c9e358..e9dde621d 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/ReadingListTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/ReadingListTest.java
@@ -51,15 +51,15 @@
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.ChromeTabbedActivity;
 import org.chromium.chrome.browser.bookmarks.BookmarkDelegate;
-import org.chromium.chrome.browser.bookmarks.BookmarkItemsAdapter;
 import org.chromium.chrome.browser.bookmarks.BookmarkManagerCoordinator;
 import org.chromium.chrome.browser.bookmarks.BookmarkModel;
 import org.chromium.chrome.browser.bookmarks.BookmarkPage;
 import org.chromium.chrome.browser.bookmarks.BookmarkPromoHeader;
 import org.chromium.chrome.browser.bookmarks.BookmarkRow;
 import org.chromium.chrome.browser.bookmarks.BookmarkToolbar;
-import org.chromium.chrome.browser.bookmarks.BookmarkUiState;
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
 import org.chromium.chrome.browser.bookmarks.BookmarkUtils;
+import org.chromium.chrome.browser.bookmarks.TestingDelegate;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.sync.SyncService;
 import org.chromium.chrome.browser.tab.Tab;
@@ -190,16 +190,12 @@
         return bookmarkId;
     }
 
-    private BookmarkItemsAdapter getReorderAdapter() {
-        return (BookmarkItemsAdapter) getAdapter();
-    }
-
-    private RecyclerView.Adapter getAdapter() {
-        return mItemsContainer.getAdapter();
+    private TestingDelegate getTestingDelegate() {
+        return mBookmarkManagerCoordinator.getTestingDelegate();
     }
 
     private BookmarkId getIdByPosition(int pos) {
-        return getReorderAdapter().getIdByPosition(pos);
+        return getTestingDelegate().getIdByPositionForTesting(pos);
     }
 
     private BookmarkDelegate getBookmarkDelegate() {
@@ -216,7 +212,7 @@
         BookmarkToolbar toolbar = mBookmarkManagerCoordinator.getToolbarForTesting();
 
         // We should default to the root bookmark.
-        Assert.assertEquals(BookmarkUiState.STATE_FOLDER, delegate.getCurrentState());
+        Assert.assertEquals(BookmarkUiMode.FOLDER, delegate.getCurrentUiMode());
         Assert.assertEquals("chrome-native://bookmarks/folder/0",
                 BookmarkUtils.getLastUsedUrl(mActivityTestRule.getActivity()));
         Assert.assertEquals("Bookmarks", toolbar.getTitle());
@@ -235,7 +231,7 @@
         ApplicationTestUtils.waitForActivityState(mBookmarkActivity, Stage.DESTROYED);
 
         // Reopen and make sure we're back in "Mobile bookmarks".
-        Assert.assertEquals(BookmarkUiState.STATE_FOLDER, delegate.getCurrentState());
+        Assert.assertEquals(BookmarkUiMode.FOLDER, delegate.getCurrentUiMode());
         Assert.assertEquals("chrome-native://bookmarks/folder/3",
                 BookmarkUtils.getLastUsedUrl(mActivityTestRule.getActivity()));
     }
@@ -307,8 +303,8 @@
 
         // Enter search UI, but don't enter any search key word.
         TestThreadUtils.runOnUiThreadBlocking(getBookmarkDelegate()::openSearchUi);
-        Assert.assertEquals("Wrong state, should be searching", BookmarkUiState.STATE_SEARCHING,
-                getBookmarkDelegate().getCurrentState());
+        Assert.assertEquals("Wrong state, should be searching", BookmarkUiMode.SEARCHING,
+                getBookmarkDelegate().getCurrentUiMode());
         RecyclerViewTestUtils.waitForStableRecyclerView(mItemsContainer);
 
         // Delete the reading list page in search state.
@@ -350,7 +346,7 @@
 
         View readingListRow = mItemsContainer.findViewHolderForAdapterPosition(1).itemView;
         Assert.assertEquals("The 2nd view should be reading list.", BookmarkType.READING_LIST,
-                getReorderAdapter().getIdByPosition(1).getType());
+                getIdByPosition(1).getType());
         TestThreadUtils.runOnUiThreadBlocking(() -> TouchCommon.singleClickView(readingListRow));
 
         ChromeTabbedActivity activity = BookmarkTestUtil.waitForTabbedActivity();
@@ -418,7 +414,7 @@
         Assert.assertEquals("No overflow menu for reading list folder.", View.GONE,
                 readingListRow.findViewById(R.id.more).getVisibility());
         Assert.assertEquals("The 1st view should be reading list.", BookmarkType.READING_LIST,
-                getReorderAdapter().getIdByPosition(0).getType());
+                getIdByPosition(0).getType());
         onView(withText("Reading list")).check(matches(isDisplayed()));
     }
 
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkItemRowTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkItemRowTest.java
index 113899d6..5afec90 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkItemRowTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkItemRowTest.java
@@ -26,6 +26,7 @@
 
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.bookmarks.BookmarkRow.Location;
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.components.bookmarks.BookmarkId;
 import org.chromium.components.bookmarks.BookmarkItem;
@@ -108,7 +109,7 @@
     @Test
     @SmallTest
     public void testSetBookmarkId() {
-        doReturn(BookmarkUiState.STATE_FOLDER).when(mDelegate).getCurrentState();
+        doReturn(BookmarkUiMode.FOLDER).when(mDelegate).getCurrentUiMode();
         TestThreadUtils.runOnUiThreadBlocking(
                 () -> { mBookmarkItemRow.setBookmarkId(mBookmarkId, Location.TOP, false); });
 
@@ -126,7 +127,7 @@
     @Test(expected = AssertionError.class)
     @SmallTest
     public void testSetBookmarkId_LoadingWhileClicked() {
-        doReturn(BookmarkUiState.STATE_LOADING).when(mDelegate).getCurrentState();
+        doReturn(BookmarkUiMode.LOADING).when(mDelegate).getCurrentUiMode();
         TestThreadUtils.runOnUiThreadBlocking(
                 () -> { mBookmarkItemRow.setBookmarkId(mBookmarkId, Location.TOP, false); });
 
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarTest.java
index 2824a63..f7948329 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarTest.java
@@ -39,6 +39,7 @@
 import org.chromium.chrome.browser.app.bookmarks.BookmarkAddEditFolderActivity;
 import org.chromium.chrome.browser.app.bookmarks.BookmarkEditActivity;
 import org.chromium.chrome.browser.app.bookmarks.BookmarkFolderSelectActivity;
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.incognito.IncognitoUtils;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
@@ -160,7 +161,7 @@
         mBookmarkToolbar.setBookmarkModel(mBookmarkModel);
         mBookmarkToolbar.setBookmarkOpener(mBookmarkOpener);
         mBookmarkToolbar.setSelectionDelegate(mSelectionDelegate);
-        mBookmarkToolbar.setBookmarkUiState(BookmarkUiState.STATE_FOLDER);
+        mBookmarkToolbar.setBookmarkUiMode(BookmarkUiMode.FOLDER);
         mBookmarkToolbar.setIsDialogUi(true);
         mBookmarkToolbar.setOpenSearchUiRunnable(mOpenSearchUiRunnable);
         mBookmarkToolbar.setOpenFolderCallback(mOpenFolderCallback);
@@ -221,7 +222,7 @@
     @UiThreadTest
     public void onNavigationBack_searching() {
         initializeNormal();
-        mBookmarkToolbar.setBookmarkUiState(BookmarkUiState.STATE_SEARCHING);
+        mBookmarkToolbar.setBookmarkUiMode(BookmarkUiMode.SEARCHING);
         mBookmarkToolbar.onNavigationBack();
         Assert.assertFalse(mBookmarkToolbar.isSearching());
     }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/browsing_data/BrowsingDataTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/browsing_data/BrowsingDataTest.java
index 2de08c3..6b9fd6058e 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/browsing_data/BrowsingDataTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/browsing_data/BrowsingDataTest.java
@@ -88,6 +88,11 @@
                 sActivityTestRule.getWebContents(), type);
     }
 
+    private String runJavascriptSync(String type) throws Exception {
+        return JavaScriptUtils.executeJavaScriptAndWaitForResult(
+                sActivityTestRule.getWebContents(), type);
+    }
+
     /**
      * Test cookies deletion.
      */
@@ -96,14 +101,14 @@
     public void testCookiesDeleted() throws Exception {
         Assert.assertEquals(0, getCookieCount());
         sActivityTestRule.loadUrl(mUrl);
-        Assert.assertEquals("false", runJavascriptAsync("hasCookie()"));
+        Assert.assertEquals("false", runJavascriptSync("hasCookie()"));
 
-        runJavascriptAsync("setCookie()");
-        Assert.assertEquals("true", runJavascriptAsync("hasCookie()"));
+        runJavascriptSync("setCookie()");
+        Assert.assertEquals("true", runJavascriptSync("hasCookie()"));
         Assert.assertEquals(1, getCookieCount());
 
         clearBrowsingData(BrowsingDataType.COOKIES, TimePeriod.LAST_HOUR);
-        Assert.assertEquals("false", runJavascriptAsync("hasCookie()"));
+        Assert.assertEquals("false", runJavascriptSync("hasCookie()"));
         Assert.assertEquals(0, getCookieCount());
     }
 
@@ -121,15 +126,15 @@
 
         for (String type : siteData) {
             Assert.assertEquals(type, 0, getCookieCount());
-            Assert.assertEquals(type, "false", runJavascriptAsync("has" + type + "()"));
+            Assert.assertEquals(type, "false", runJavascriptAsync("has" + type + "Async()"));
 
-            runJavascriptAsync("set" + type + "()");
+            runJavascriptAsync("set" + type + "Async()");
             Assert.assertEquals(type, 1, getCookieCount());
-            Assert.assertEquals(type, "true", runJavascriptAsync("has" + type + "()"));
+            Assert.assertEquals(type, "true", runJavascriptAsync("has" + type + "Async()"));
 
             clearBrowsingData(BrowsingDataType.COOKIES, TimePeriod.LAST_HOUR);
             Assert.assertEquals(type, 0, getCookieCount());
-            Assert.assertEquals(type, "false", runJavascriptAsync("has" + type + "()"));
+            Assert.assertEquals(type, "false", runJavascriptAsync("has" + type + "Async()"));
 
             // Some types create data by checking for them, so we need to do a cleanup at the end.
             clearBrowsingData(BrowsingDataType.COOKIES, TimePeriod.LAST_HOUR);
@@ -165,12 +170,12 @@
     public void testHistoryDeleted() throws Exception {
         Assert.assertEquals(0, getCookieCount());
         sActivityTestRule.loadUrlInNewTab(mUrl);
-        Assert.assertEquals("false", runJavascriptAsync("hasHistory()"));
+        Assert.assertEquals("false", runJavascriptSync("hasHistory()"));
 
-        runJavascriptAsync("setHistory()");
-        Assert.assertEquals("true", runJavascriptAsync("hasHistory()"));
+        runJavascriptSync("setHistory()");
+        Assert.assertEquals("true", runJavascriptSync("hasHistory()"));
 
         clearBrowsingData(BrowsingDataType.HISTORY, TimePeriod.LAST_HOUR);
-        Assert.assertEquals("false", runJavascriptAsync("hasHistory()"));
+        Assert.assertEquals("false", runJavascriptSync("hasHistory()"));
     }
 }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/incognito/IncognitoStorageLeakageTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/incognito/IncognitoStorageLeakageTest.java
index b74761a..a23bb3b 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/incognito/IncognitoStorageLeakageTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/incognito/IncognitoStorageLeakageTest.java
@@ -105,12 +105,12 @@
 
         // Sets the session storage in tab1
         assertEquals("true",
-                JavaScriptUtils.runJavascriptWithAsyncResult(
+                JavaScriptUtils.executeJavaScriptAndWaitForResult(
                         tab1.getWebContents(), "setSessionStorage()"));
 
         // Checks the sessions storage is set in tab1
         assertEquals("true",
-                JavaScriptUtils.runJavascriptWithAsyncResult(
+                JavaScriptUtils.executeJavaScriptAndWaitForResult(
                         tab1.getWebContents(), "hasSessionStorage()"));
 
         Tab tab2 = activity2.launchUrl(
@@ -121,7 +121,7 @@
         // Checks the session storage in tab2. Session storage set in tab1 should not be accessible.
         // The session storage is per tab basis.
         assertEquals("false",
-                JavaScriptUtils.runJavascriptWithAsyncResult(
+                JavaScriptUtils.executeJavaScriptAndWaitForResult(
                         tab2.getWebContents(), "hasSessionStorage()"));
     }
 
@@ -160,11 +160,11 @@
             // Set the storage in tab1
             assertEquals("true",
                     JavaScriptUtils.runJavascriptWithAsyncResult(
-                            tab1.getWebContents(), "set" + type + "()"));
+                            tab1.getWebContents(), "set" + type + "Async()"));
             // Checks the storage is set in tab1
             assertEquals("true",
                     JavaScriptUtils.runJavascriptWithAsyncResult(
-                            tab1.getWebContents(), "has" + type + "()"));
+                            tab1.getWebContents(), "has" + type + "Async()"));
 
             TestThreadUtils.runOnUiThreadBlocking(
                     () -> tab2.loadIfNeeded(LoadIfNeededCaller.OTHER));
@@ -173,7 +173,7 @@
             // Access the storage from tab2
             assertEquals(expected,
                     JavaScriptUtils.runJavascriptWithAsyncResult(
-                            tab2.getWebContents(), "has" + type + "()"));
+                            tab2.getWebContents(), "has" + type + "Async()"));
         }
     }
 }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/page_info/PageInfoViewTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/page_info/PageInfoViewTest.java
index a0e72c7..5c127156 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/page_info/PageInfoViewTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/page_info/PageInfoViewTest.java
@@ -242,13 +242,13 @@
 
     private void expectHasCookies(boolean hasData) throws TimeoutException {
         for (String type : sCookieDataTypes) {
-            assertEquals(hasData ? "true" : "false", runJavascriptAsync("has" + type + "()"));
+            assertEquals(hasData ? "true" : "false", runJavascriptAsync("has" + type + "Async()"));
         }
     }
 
     private void createCookies() throws TimeoutException {
         for (String type : sCookieDataTypes) {
-            runJavascriptAsync("set" + type + "()");
+            runJavascriptAsync("set" + type + "Async()");
         }
     }
 
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerCoordinatorTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerCoordinatorTest.java
index 5aafb890..e5ac123 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerCoordinatorTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerCoordinatorTest.java
@@ -8,7 +8,6 @@
 import android.view.View;
 import android.widget.FrameLayout;
 
-import androidx.test.core.app.ActivityScenario;
 import androidx.test.ext.junit.rules.ActivityScenarioRule;
 
 import org.junit.Assert;
@@ -80,7 +79,6 @@
     @Mock
     BookmarkModel mBookmarkModel;
 
-    private ActivityScenario<TestActivity> mActivityScenario;
     private Activity mActivity;
     private BookmarkManagerCoordinator mCoordinator;
 
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediatorTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediatorTest.java
index 7d707ac0..197d538 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediatorTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediatorTest.java
@@ -10,15 +10,17 @@
 
 import static org.chromium.ui.test.util.MockitoHelper.doRunnable;
 
-import android.content.Context;
-import android.view.accessibility.AccessibilityManager;
+import android.app.Activity;
 
 import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
 
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
@@ -28,11 +30,23 @@
 import org.chromium.base.supplier.ObservableSupplierImpl;
 import org.chromium.base.test.BaseRobolectricTestRunner;
 import org.chromium.base.test.util.Batch;
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
+import org.chromium.chrome.browser.signin.services.SigninManager;
+import org.chromium.chrome.browser.sync.SyncService;
+import org.chromium.chrome.test.util.browser.Features;
 import org.chromium.components.bookmarks.BookmarkId;
+import org.chromium.components.bookmarks.BookmarkItem;
 import org.chromium.components.bookmarks.BookmarkType;
 import org.chromium.components.browser_ui.widget.selectable_list.SelectableListLayout;
 import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate;
 import org.chromium.components.favicon.LargeIconBridge;
+import org.chromium.components.signin.AccountManagerFacade;
+import org.chromium.components.signin.AccountManagerFacadeProvider;
+import org.chromium.components.signin.identitymanager.IdentityManager;
+import org.chromium.ui.base.TestActivity;
 
 import java.util.Arrays;
 
@@ -40,13 +54,17 @@
 @Batch(Batch.UNIT_TESTS)
 @RunWith(BaseRobolectricTestRunner.class)
 @Config(manifest = Config.NONE)
+@Features.EnableFeatures({ChromeFeatureList.BOOKMARKS_REFRESH, ChromeFeatureList.SHOPPING_LIST})
 public class BookmarkManagerMediatorTest {
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule
+    public ActivityScenarioRule<TestActivity> mActivityScenarioRule =
+            new ActivityScenarioRule<>(TestActivity.class);
+    @Rule
+    public TestRule mFeaturesProcessorRule = new Features.JUnitProcessor();
 
     @Mock
-    Context mContext;
-    @Mock
     BookmarkModel mBookmarkModel;
     @Mock
     BookmarkOpener mBookmarkOpener;
@@ -57,13 +75,23 @@
     @Mock
     RecyclerView mRecyclerView;
     @Mock
-    BookmarkItemsAdapter mBookmarkItemsAdapter;
-    @Mock
     LargeIconBridge mLargeIconBridge;
     @Mock
-    AccessibilityManager mAccessibilityManager;
-    @Mock
     BookmarkUiObserver mBookmarkUiObserver;
+    @Mock
+    Profile mProfile;
+    @Mock
+    SyncService mSyncService;
+    @Mock
+    IdentityServicesProvider mIdentityServicesProvider;
+    @Mock
+    SigninManager mSigninManager;
+    @Mock
+    IdentityManager mIdentityManager;
+    @Mock
+    AccountManagerFacade mAccountManagerFacade;
+    @Mock
+    BookmarkUndoController mBookmarkUndoController;
 
     final ObservableSupplierImpl<Boolean> mBackPressStateSupplier = new ObservableSupplierImpl<>();
     final ObservableSupplierImpl<Boolean> mSelectableListLayoutHandleBackPressChangedSupplier =
@@ -71,35 +99,48 @@
     final BookmarkId mFolderId = new BookmarkId(/*id=*/1, BookmarkType.NORMAL);
     final BookmarkId mFolder2Id = new BookmarkId(/*id=*/2, BookmarkType.NORMAL);
 
-    BookmarkManagerMediator mMediator;
+    private ActivityScenario<TestActivity> mActivityScenario;
+    private Activity mActivity;
+    private BookmarkManagerMediator mMediator;
 
     @Before
     public void setUp() {
-        // Setup Context.
-        doReturn(mAccessibilityManager)
-                .when(mContext)
-                .getSystemService(Context.ACCESSIBILITY_SERVICE);
+        mActivityScenarioRule.getScenario().onActivity((activity) -> {
+            mActivity = activity;
 
-        // Setup BookmarkModel.
-        doReturn(true).when(mBookmarkModel).doesBookmarkExist(any());
-        doReturn(Arrays.asList(mFolder2Id)).when(mBookmarkModel).getChildIDs(mFolderId);
+            // Setup BookmarkModel.
+            doReturn(true).when(mBookmarkModel).doesBookmarkExist(any());
+            doReturn(Arrays.asList(mFolder2Id)).when(mBookmarkModel).getChildIDs(mFolderId);
+            BookmarkItem bookmarkItem =
+                    new BookmarkItem(mFolderId, "Folder", null, true, null, true, false, 0, false);
+            doReturn(bookmarkItem).when(mBookmarkModel).getBookmarkById(any());
 
-        // Setup SelectableListLayout.
-        doReturn(mContext).when(mSelectableListLayout).getContext();
-        doReturn(mSelectableListLayoutHandleBackPressChangedSupplier)
-                .when(mSelectableListLayout)
-                .getHandleBackPressChangedSupplier();
+            // Setup SelectableListLayout.
+            doReturn(mActivity).when(mSelectableListLayout).getContext();
+            doReturn(mSelectableListLayoutHandleBackPressChangedSupplier)
+                    .when(mSelectableListLayout)
+                    .getHandleBackPressChangedSupplier();
 
-        // Setup BookmarkUIObserver.
-        doRunnable(() -> mMediator.removeUiObserver(mBookmarkUiObserver))
-                .when(mBookmarkUiObserver)
-                .onDestroy();
+            // Setup BookmarkUIObserver.
+            doRunnable(() -> mMediator.removeUiObserver(mBookmarkUiObserver))
+                    .when(mBookmarkUiObserver)
+                    .onDestroy();
 
-        mMediator = new BookmarkManagerMediator(mContext, mBookmarkModel, mBookmarkOpener,
-                mSelectableListLayout, mSelectionDelegate, mRecyclerView, mBookmarkItemsAdapter,
-                mLargeIconBridge, /*isDialogUi=*/true, /*isIncognito=*/false,
-                mBackPressStateSupplier);
-        mMediator.addUiObserver(mBookmarkUiObserver);
+            // Setup sync/identify mocks.
+            SyncService.overrideForTests(mSyncService);
+            IdentityServicesProvider.setInstanceForTests(mIdentityServicesProvider);
+            doReturn(mSigninManager).when(mIdentityServicesProvider).getSigninManager(any());
+            doReturn(mIdentityManager).when(mSigninManager).getIdentityManager();
+            AccountManagerFacadeProvider.setInstanceForTests(mAccountManagerFacade);
+
+            BookmarkItemsAdapter bookmarkItemsAdapter =
+                    new BookmarkItemsAdapter(mActivity, (a, b) -> null, (a, b, c) -> {});
+            mMediator = new BookmarkManagerMediator(mActivity, mBookmarkModel, mBookmarkOpener,
+                    mSelectableListLayout, mSelectionDelegate, mRecyclerView, bookmarkItemsAdapter,
+                    mLargeIconBridge, /*isDialogUi=*/true, /*isIncognito=*/false,
+                    mBackPressStateSupplier, mProfile, mBookmarkUndoController);
+            mMediator.addUiObserver(mBookmarkUiObserver);
+        });
     }
 
     void finishLoading() {
@@ -109,7 +150,7 @@
     @Test
     public void initAndLoadBookmarkModel() {
         finishLoading();
-        Assert.assertEquals(BookmarkUiState.STATE_LOADING, mMediator.getCurrentState());
+        Assert.assertEquals(BookmarkUiMode.LOADING, mMediator.getCurrentUiMode());
     }
 
     @Test
@@ -118,15 +159,16 @@
         mMediator.updateForUrl("chrome-native://bookmarks/folder/" + mFolderId.getId());
 
         finishLoading();
-        Assert.assertEquals(BookmarkUiState.STATE_FOLDER, mMediator.getCurrentState());
+        Assert.assertEquals(BookmarkUiMode.FOLDER, mMediator.getCurrentUiMode());
     }
 
     @Test
-    public void destroyUnregistersObservers() {
+    public void testDestroy() {
         finishLoading();
 
         mMediator.onDestroy();
         verify(mBookmarkUiObserver).onDestroy();
+        verify(mBookmarkUndoController).destroy();
     }
 
     @Test
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarMediatorTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarMediatorTest.java
index 5a9403f..f6dad0bf 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarMediatorTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkToolbarMediatorTest.java
@@ -20,6 +20,7 @@
 import org.chromium.base.supplier.OneshotSupplierImpl;
 import org.chromium.base.test.BaseRobolectricTestRunner;
 import org.chromium.base.test.util.Batch;
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
 import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate;
 import org.chromium.ui.modelutil.PropertyModel;
 
@@ -57,8 +58,7 @@
                          .with(BookmarkToolbarProperties.BOOKMARK_MODEL, mBookmarkModel)
                          .with(BookmarkToolbarProperties.BOOKMARK_OPENER, mBookmarkOpener)
                          .with(BookmarkToolbarProperties.SELECTION_DELEGATE, mSelectionDelegate)
-                         .with(BookmarkToolbarProperties.BOOKMARK_UI_STATE,
-                                 BookmarkUiState.STATE_LOADING)
+                         .with(BookmarkToolbarProperties.BOOKMARK_UI_STATE, BookmarkUiMode.LOADING)
                          .with(BookmarkToolbarProperties.IS_DIALOG_UI, false)
                          .with(BookmarkToolbarProperties.DRAG_ENABLED, false)
                          .with(BookmarkToolbarProperties.OPEN_SEARCH_UI_RUNNABLE,
@@ -78,16 +78,16 @@
 
     @Test
     public void onStateChangedUpdatesModel() {
-        mMediator.onStateChanged(BookmarkUiState.STATE_LOADING);
-        Assert.assertEquals(BookmarkUiState.STATE_LOADING,
+        mMediator.onUiModeChanged(BookmarkUiMode.LOADING);
+        Assert.assertEquals(BookmarkUiMode.LOADING,
                 mModel.get(BookmarkToolbarProperties.BOOKMARK_UI_STATE).intValue());
 
-        mMediator.onStateChanged(BookmarkUiState.STATE_SEARCHING);
-        Assert.assertEquals(BookmarkUiState.STATE_SEARCHING,
+        mMediator.onUiModeChanged(BookmarkUiMode.SEARCHING);
+        Assert.assertEquals(BookmarkUiMode.SEARCHING,
                 mModel.get(BookmarkToolbarProperties.BOOKMARK_UI_STATE).intValue());
 
-        mMediator.onStateChanged(BookmarkUiState.STATE_FOLDER);
-        Assert.assertEquals(BookmarkUiState.STATE_FOLDER,
+        mMediator.onUiModeChanged(BookmarkUiMode.FOLDER);
+        Assert.assertEquals(BookmarkUiMode.FOLDER,
                 mModel.get(BookmarkToolbarProperties.BOOKMARK_UI_STATE).intValue());
     }
 
diff --git a/chrome/app/chrome_command_ids.h b/chrome/app/chrome_command_ids.h
index 1099d1b1..885be4b 100644
--- a/chrome/app/chrome_command_ids.h
+++ b/chrome/app/chrome_command_ids.h
@@ -231,7 +231,10 @@
 #define IDC_LACROS_DATA_MIGRATION      40265
 #endif
 
-#define IDC_PERFORMANCE                40266
+#define IDC_PERFORMANCE                             40266
+#define IDC_EXTENSIONS_SUBMENU                         40267
+#define IDC_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS       40268
+#define IDC_EXTENSIONS_SUBMENU_VISIT_CHROME_WEB_STORE  40269
 
 // Spell-check
 // Insert any additional suggestions before _LAST; these have to be consecutive.
diff --git a/chrome/app/chrome_main_delegate.cc b/chrome/app/chrome_main_delegate.cc
index 1169f5c..da238542 100644
--- a/chrome/app/chrome_main_delegate.cc
+++ b/chrome/app/chrome_main_delegate.cc
@@ -108,7 +108,6 @@
 #include "chrome/child/v8_crashpad_support_win.h"
 #include "chrome/chrome_elf/chrome_elf_main.h"
 #include "chrome/common/child_process_logging.h"
-#include "chrome/common/win/delay_load_failure_hook.h"
 #include "sandbox/win/src/sandbox.h"
 #include "sandbox/win/src/sandbox_factory.h"
 #include "ui/base/resource/resource_bundle_win.h"
@@ -722,11 +721,6 @@
   // Initialize the cleaner of left-behind tmp files now that the main thread
   // has its SequencedTaskRunner; see https://crbug.com/1075917.
   base::ImportantFileWriterCleaner::GetInstance().Initialize();
-
-  // For now, do not enable delay load failure hooks for browser process except
-  // in tests, where failures really shouldn't happen.
-  if (invoked_in_browser->is_running_test)
-    chrome::DisableDelayLoadFailureHooksForCurrentModule();
 #endif
 
 #if !BUILDFLAG(IS_FUCHSIA)
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index cb4c892f..bc37292 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -1484,6 +1484,15 @@
         <message name="IDS_SHOW_EXTENSIONS" desc="The show extensions menu in the app menu">
           &amp;Extensions
         </message>
+        <message name="IDS_EXTENSIONS_SUBMENU" desc="The extensions menu in the app menu (title case).">
+          &amp;Extensions
+        </message>
+        <message name="IDS_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS_ITEM" desc="The text for the link in the extensions menu, in the app menu, to visit chrome://extensions to view all extensions installed (title case).">
+          Manage Extensions
+        </message>
+        <message name="IDS_EXTENSIONS_SUBMENU_CHROME_WEBSTORE_ITEM" desc="The text for the link in the extensions menu, in the app menu, to visit the Chrome Web Store to get more extensions (title case).">
+          Visit Chrome Web Store
+        </message>
         <message name="IDS_SHOW_PERFORMANCE" desc="The text label of the Performance menu item">
           Performance
         </message>
@@ -1525,6 +1534,15 @@
         <message name="IDS_SHOW_EXTENSIONS" desc="In Title Case: The show extensions menu in the app menu">
           &amp;Extensions
         </message>
+        <message name="IDS_EXTENSIONS_SUBMENU" desc="In Title Case: The extensions menu in the app menu.">
+          &amp;Extensions
+        </message>
+        <message name="IDS_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS_ITEM" desc="In Title Case: The text for the link in the extensions menu, in the app menu, to visit chrome://extensions to view all extensions installed.">
+          Manage Extensions
+        </message>
+        <message name="IDS_EXTENSIONS_SUBMENU_CHROME_WEBSTORE_ITEM" desc="In Title Case: The text for the link in the extensions menu, in the app menu, to visit the Chrome Web Store to get more extensions.">
+          Visit Chrome Web Store
+        </message>
         <message name="IDS_SHOW_PERFORMANCE" desc="In Title Case: The text label of the Performance menu item">
           Performance
         </message>
@@ -7578,6 +7596,9 @@
       <message name="IDS_READING_MODE_BLUE_COLOR_LABEL" desc="Combobox option for setting the color theme for the reading mode feature.">
         Blue
       </message>
+      <message name="IDS_READ_ANYTHING_LOADING" desc="Message to show while Read Anything is figuring out what text to display.">
+        Getting ready
+      </message>
 
       <!-- User note strings -->
       <message name="IDS_USER_NOTE_TITLE" desc="Title of the User note feature, which gives users access to private user notes.">
diff --git a/chrome/app/generated_resources_grd/IDS_EXTENSIONS_SUBMENU.png.sha1 b/chrome/app/generated_resources_grd/IDS_EXTENSIONS_SUBMENU.png.sha1
new file mode 100644
index 0000000..20355225
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_EXTENSIONS_SUBMENU.png.sha1
@@ -0,0 +1 @@
+1d89c386592ad93c2b63e8fe942ee2adaf909539
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_EXTENSIONS_SUBMENU_CHROME_WEBSTORE_ITEM.png.sha1 b/chrome/app/generated_resources_grd/IDS_EXTENSIONS_SUBMENU_CHROME_WEBSTORE_ITEM.png.sha1
new file mode 100644
index 0000000..9b6d4602
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_EXTENSIONS_SUBMENU_CHROME_WEBSTORE_ITEM.png.sha1
@@ -0,0 +1 @@
+65862db3687d70fd338e38ce370f5de8a5dfcb9b
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS_ITEM.png.sha1 b/chrome/app/generated_resources_grd/IDS_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS_ITEM.png.sha1
new file mode 100644
index 0000000..c0fb2011
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS_ITEM.png.sha1
@@ -0,0 +1 @@
+000a4144865e17b3401913e888436b2efd2de3c2
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_READ_ANYTHING_LOADING.png.sha1 b/chrome/app/generated_resources_grd/IDS_READ_ANYTHING_LOADING.png.sha1
new file mode 100644
index 0000000..0b267dfc
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_READ_ANYTHING_LOADING.png.sha1
@@ -0,0 +1 @@
+b6f6cbc03238587a5de03210a77191512e1fc63e
\ No newline at end of file
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 9fccc44..0ac02bf 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -359,8 +359,6 @@
     "content_settings/page_specific_content_settings_delegate.h",
     "content_settings/sound_content_setting_observer.cc",
     "content_settings/sound_content_setting_observer.h",
-    "crash_upload_list/crash_upload_list.cc",
-    "crash_upload_list/crash_upload_list.h",
     "custom_handlers/chrome_protocol_handler_registry_delegate.cc",
     "custom_handlers/chrome_protocol_handler_registry_delegate.h",
     "custom_handlers/protocol_handler_registry_factory.cc",
@@ -1996,6 +1994,7 @@
     "//chrome/browser/breadcrumbs",
     "//chrome/browser/browsing_data:constants",
     "//chrome/browser/chrome_for_testing:buildflags",
+    "//chrome/browser/crash_upload_list",
     "//chrome/browser/devtools",
     "//chrome/browser/enterprise/identifiers",
     "//chrome/browser/enterprise/platform_auth:features",
@@ -2936,8 +2935,6 @@
       "content_creation/notes/internal/note_service_factory.h",
       "content_settings/request_desktop_site_web_contents_observer_android.cc",
       "content_settings/request_desktop_site_web_contents_observer_android.h",
-      "crash_upload_list/crash_upload_list_android.cc",
-      "crash_upload_list/crash_upload_list_android.h",
       "creator/android/creator_api_bridge.cc",
       "device_reauth/android/device_authenticator_android.cc",
       "device_reauth/android/device_authenticator_android.h",
@@ -5526,8 +5523,6 @@
       "certificate_provider/sign_requests.h",
       "certificate_provider/thread_safe_certificate_map.cc",
       "certificate_provider/thread_safe_certificate_map.h",
-      "crash_upload_list/crash_upload_list_chromeos.cc",
-      "crash_upload_list/crash_upload_list_chromeos.h",
       "download/notification/download_item_notification.cc",
       "download/notification/download_item_notification.h",
       "download/notification/download_notification_manager.cc",
@@ -6264,8 +6259,6 @@
 
   if (is_fuchsia) {
     sources += [
-      "crash_upload_list/crash_upload_list_fuchsia.cc",
-      "crash_upload_list/crash_upload_list_fuchsia.h",
       "download/download_status_updater_fuchsia.cc",
       "first_run/first_run_internal_fuchsia.cc",
       "first_run/upgrade_util_fuchsia.cc",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 533b3bf..79034a7 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -3622,9 +3622,6 @@
     {"calendar-jelly", flag_descriptions::kCalendarJellyName,
      flag_descriptions::kCalendarJellyDescription, kOsCrOS,
      FEATURE_VALUE_TYPE(ash::features::kCalendarJelly)},
-    {"calendar-view", flag_descriptions::kCalendarViewName,
-     flag_descriptions::kCalendarViewDescription, kOsCrOS,
-     FEATURE_VALUE_TYPE(ash::features::kCalendarView)},
     {"calendar-view-debug-mode", flag_descriptions::kCalendarModelDebugModeName,
      flag_descriptions::kCalendarModelDebugModeDescription, kOsCrOS,
      FEATURE_VALUE_TYPE(ash::features::kCalendarModelDebugMode)},
@@ -5179,6 +5176,12 @@
 #endif  // BUILDFLAG(IS_MAC)
 
 #if BUILDFLAG(IS_ANDROID)
+
+    {"omnibox-adapt-narrow-tablet-windows",
+     flag_descriptions::kOmniboxAdaptNarrowTabletWindowsName,
+     flag_descriptions::kOmniboxAdaptNarrowTabletWindowsDescription, kOsAndroid,
+     FEATURE_VALUE_TYPE(chrome::android::kOmniboxAdaptNarrowTabletWindows)},
+
     {"omnibox-assistant-voice-search",
      flag_descriptions::kOmniboxAssistantVoiceSearchName,
      flag_descriptions::kOmniboxAssistantVoiceSearchDescription, kOsAndroid,
diff --git a/chrome/browser/apps/app_service/metrics/app_service_metrics.cc b/chrome/browser/apps/app_service/metrics/app_service_metrics.cc
index 02a0577..2f3ddc8 100644
--- a/chrome/browser/apps/app_service/metrics/app_service_metrics.cc
+++ b/chrome/browser/apps/app_service/metrics/app_service_metrics.cc
@@ -248,6 +248,16 @@
   } else if (app_id == arc::kGoogleTVAppId) {
     RecordDefaultAppLaunch(DefaultAppName::kGoogleTv, launch_source);
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
+  } else if (app_id == web_app::kGoogleCalendarAppId) {
+    RecordDefaultAppLaunch(DefaultAppName::kGoogleCalendar, launch_source);
+  } else if (app_id == web_app::kGoogleChatAppId) {
+    RecordDefaultAppLaunch(DefaultAppName::kGoogleChat, launch_source);
+  } else if (app_id == web_app::kGoogleMeetAppId) {
+    RecordDefaultAppLaunch(DefaultAppName::kGoogleMeet, launch_source);
+  } else if (app_id == web_app::kGoogleMapsAppId) {
+    RecordDefaultAppLaunch(DefaultAppName::kGoogleMaps, launch_source);
+  } else if (app_id == web_app::kMessagesAppId) {
+    RecordDefaultAppLaunch(DefaultAppName::kGoogleMessages, launch_source);
   }
 
   // Above are default apps; below are built-in apps.
diff --git a/chrome/browser/apps/app_service/metrics/app_service_metrics.h b/chrome/browser/apps/app_service/metrics/app_service_metrics.h
index 3f6c0f0..3ef8d05 100644
--- a/chrome/browser/apps/app_service/metrics/app_service_metrics.h
+++ b/chrome/browser/apps/app_service/metrics/app_service_metrics.h
@@ -68,9 +68,14 @@
   kCalculator = 50,
   kFirmwareUpdateApp = 51,
   kGoogleTv = 52,
+  kGoogleCalendar = 53,
+  kGoogleChat = 54,
+  kGoogleMeet = 55,
+  kGoogleMaps = 56,
+  kGoogleMessages = 57,
   // Add any new values above this one, and update kMaxValue to the highest
   // enumerator value.
-  kMaxValue = kGoogleTv,
+  kMaxValue = kGoogleMessages,
 };
 
 // The built-in app's histogram name. This is used for logging so do not change
diff --git a/chrome/browser/ash/BUILD.gn b/chrome/browser/ash/BUILD.gn
index 985291e..4134863 100644
--- a/chrome/browser/ash/BUILD.gn
+++ b/chrome/browser/ash/BUILD.gn
@@ -3761,6 +3761,7 @@
     "//chrome/browser/autofill",
     "//chrome/browser/browsing_data:constants",
     "//chrome/browser/chromeos/drivefs",
+    "//chrome/browser/crash_upload_list",
     "//chrome/browser/devtools",
     "//chrome/browser/favicon",
     "//chrome/browser/google",
diff --git a/chrome/browser/ash/app_restore/arc_app_queue_restore_handler.h b/chrome/browser/ash/app_restore/arc_app_queue_restore_handler.h
index 29df797..4c3e20e1 100644
--- a/chrome/browser/ash/app_restore/arc_app_queue_restore_handler.h
+++ b/chrome/browser/ash/app_restore/arc_app_queue_restore_handler.h
@@ -59,23 +59,6 @@
   kMaxValue = kNotFinish,
 };
 
-// This is usded for logging, so do not remove or reorder existing entries.
-enum class NoGhostWindowReason {
-  kNoHandler = 0,
-  kNoHandlerFromCrash = 1,
-  kNoRootBounds = 2,
-  kNoRootBoundsFromCrash = 3,
-  kNoScreenBounds = 4,
-  kNoScreenBoundsFromCrash = 5,
-  kFlagDisabled = 6,
-  kNotARCVM = 7,
-  kNoExoHelper = 8,
-
-  // Add any new values above this one, and update kMaxValue to the highest
-  // enumerator value.
-  kMaxValue = kNoExoHelper,
-};
-
 // This is used for logging, so do not remove or reorder existing entries.
 enum class ArcRestoreState {
   kSuccess = 0,
diff --git a/chrome/browser/ash/arc/input_overlay/display_overlay_controller.cc b/chrome/browser/ash/arc/input_overlay/display_overlay_controller.cc
index 9e19abb..08289466 100644
--- a/chrome/browser/ash/arc/input_overlay/display_overlay_controller.cc
+++ b/chrome/browser/ash/arc/input_overlay/display_overlay_controller.cc
@@ -38,6 +38,7 @@
 #include "ui/views/controls/focus_ring.h"
 #include "ui/views/controls/highlight_path_generator.h"
 #include "ui/views/widget/widget.h"
+#include "util.h"
 
 namespace arc::input_overlay {
 
@@ -534,6 +535,9 @@
       SetEventTarget(overlay_widget, /*on_overlay=*/false);
       break;
     case DisplayMode::kEdit:
+      // When using Tab to traverse views and enter into the edit mode, it needs
+      // to reset the focus before removing the menu.
+      ResetFocusTo(overlay_widget->GetContentsView());
       RemoveInputMenuView();
       RemoveMenuEntryView();
       RemoveEducationalView();
diff --git a/chrome/browser/ash/events/event_rewriter_delegate_impl.cc b/chrome/browser/ash/events/event_rewriter_delegate_impl.cc
index cc2aa6e..dd5a550 100644
--- a/chrome/browser/ash/events/event_rewriter_delegate_impl.cc
+++ b/chrome/browser/ash/events/event_rewriter_delegate_impl.cc
@@ -4,8 +4,11 @@
 
 #include "chrome/browser/ash/events/event_rewriter_delegate_impl.h"
 
+#include "ash/constants/ash_features.h"
 #include "ash/constants/ash_pref_names.h"
+#include "ash/public/cpp/input_device_settings_controller.h"
 #include "ash/public/cpp/window_properties.h"
+#include "ash/public/mojom/input_device_settings.mojom.h"
 #include "chrome/browser/ash/login/ui/login_display_host.h"
 #include "chrome/browser/ash/notifications/deprecation_notification_controller.h"
 #include "chrome/browser/extensions/extension_commands_global_registry.h"
@@ -23,14 +26,17 @@
     : EventRewriterDelegateImpl(
           activation_client,
           std::make_unique<DeprecationNotificationController>(
-              message_center::MessageCenter::Get())) {}
+              message_center::MessageCenter::Get()),
+          InputDeviceSettingsController::Get()) {}
 
 EventRewriterDelegateImpl::EventRewriterDelegateImpl(
     wm::ActivationClient* activation_client,
-    std::unique_ptr<DeprecationNotificationController> deprecation_controller)
+    std::unique_ptr<DeprecationNotificationController> deprecation_controller,
+    InputDeviceSettingsController* input_device_settings_controller)
     : pref_service_for_testing_(nullptr),
       activation_client_(activation_client),
-      deprecation_controller_(std::move(deprecation_controller)) {}
+      deprecation_controller_(std::move(deprecation_controller)),
+      input_device_settings_controller_(input_device_settings_controller) {}
 
 EventRewriterDelegateImpl::~EventRewriterDelegateImpl() {}
 
@@ -73,11 +79,20 @@
   return true;
 }
 
-bool EventRewriterDelegateImpl::TopRowKeysAreFunctionKeys() const {
-  const PrefService* pref_service = GetPrefService();
-  if (!pref_service)
-    return false;
-  return pref_service->GetBoolean(prefs::kSendFunctionKeys);
+bool EventRewriterDelegateImpl::TopRowKeysAreFunctionKeys(int device_id) const {
+  // When the flag is disabled, `device_id` is unused.
+  if (!ash::features::IsInputDeviceSettingsSplitEnabled()) {
+    const PrefService* pref_service = GetPrefService();
+    if (!pref_service) {
+      return false;
+    }
+    return pref_service->GetBoolean(prefs::kSendFunctionKeys);
+  }
+
+  const mojom::KeyboardSettings* settings =
+      input_device_settings_controller_->GetKeyboardSettings(device_id);
+  // TODO(dpad): Add metric for when settings are not able to be found.
+  return settings && settings->top_row_are_fkeys;
 }
 
 bool EventRewriterDelegateImpl::IsExtensionCommandRegistered(
diff --git a/chrome/browser/ash/events/event_rewriter_delegate_impl.h b/chrome/browser/ash/events/event_rewriter_delegate_impl.h
index 23d04195..9db97ad0 100644
--- a/chrome/browser/ash/events/event_rewriter_delegate_impl.h
+++ b/chrome/browser/ash/events/event_rewriter_delegate_impl.h
@@ -5,6 +5,7 @@
 #ifndef CHROME_BROWSER_ASH_EVENTS_EVENT_REWRITER_DELEGATE_IMPL_H_
 #define CHROME_BROWSER_ASH_EVENTS_EVENT_REWRITER_DELEGATE_IMPL_H_
 
+#include "ash/public/cpp/input_device_settings_controller.h"
 #include "ui/chromeos/events/event_rewriter_chromeos.h"
 #include "ui/wm/public/activation_client.h"
 
@@ -17,9 +18,10 @@
 class EventRewriterDelegateImpl : public ui::EventRewriterChromeOS::Delegate {
  public:
   explicit EventRewriterDelegateImpl(wm::ActivationClient* activation_client);
-  EventRewriterDelegateImpl(wm::ActivationClient* activation_client,
-                            std::unique_ptr<DeprecationNotificationController>
-                                deprecation_controller);
+  EventRewriterDelegateImpl(
+      wm::ActivationClient* activation_client,
+      std::unique_ptr<DeprecationNotificationController> deprecation_controller,
+      InputDeviceSettingsController* input_device_settings_controller);
 
   EventRewriterDelegateImpl(const EventRewriterDelegateImpl&) = delete;
   EventRewriterDelegateImpl& operator=(const EventRewriterDelegateImpl&) =
@@ -36,7 +38,7 @@
   bool RewriteMetaTopRowKeyComboEvents() const override;
   bool GetKeyboardRemappedPrefValue(const std::string& pref_name,
                                     int* result) const override;
-  bool TopRowKeysAreFunctionKeys() const override;
+  bool TopRowKeysAreFunctionKeys(int device_id) const override;
   bool IsExtensionCommandRegistered(ui::KeyboardCode key_code,
                                     int flags) const override;
   bool IsSearchKeyAcceleratorReserved() const override;
@@ -60,6 +62,8 @@
 
   // Tracks whether meta + top row key rewrites should be suppressed or not.
   bool suppress_meta_top_row_key_rewrites_ = false;
+
+  raw_ptr<InputDeviceSettingsController> input_device_settings_controller_;
 };
 
 }  // namespace ash
diff --git a/chrome/browser/ash/events/event_rewriter_unittest.cc b/chrome/browser/ash/events/event_rewriter_unittest.cc
index 9b12e0c..cd35f437 100644
--- a/chrome/browser/ash/events/event_rewriter_unittest.cc
+++ b/chrome/browser/ash/events/event_rewriter_unittest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <memory>
 #include <vector>
 
 #include "ash/accelerators/accelerator_controller_impl.h"
@@ -9,6 +10,9 @@
 #include "ash/accessibility/sticky_keys/sticky_keys_overlay.h"
 #include "ash/constants/ash_features.h"
 #include "ash/constants/ash_pref_names.h"
+#include "ash/public/cpp/input_device_settings_controller.h"
+#include "ash/public/cpp/test/mock_input_device_settings_controller.h"
+#include "ash/public/mojom/input_device_settings.mojom.h"
 #include "ash/shell.h"
 #include "base/command_line.h"
 #include "base/containers/contains.h"
@@ -52,6 +56,7 @@
 #include "ui/events/test/events_test_utils.h"
 #include "ui/events/test/test_event_processor.h"
 #include "ui/events/test/test_event_rewriter_continuation.h"
+#include "ui/events/types/event_type.h"
 #include "ui/message_center/fake_message_center.h"
 #include "ui/wm/core/window_util.h"
 
@@ -204,7 +209,7 @@
         std::make_unique<DeprecationNotificationController>(&message_center_);
     deprecation_controller_ = deprecation_controller.get();
     delegate_ = std::make_unique<EventRewriterDelegateImpl>(
-        nullptr, std::move(deprecation_controller));
+        nullptr, std::move(deprecation_controller), nullptr);
     delegate_->set_pref_service_for_testing(prefs());
     device_data_manager_test_api_.SetKeyboardDevices({});
     rewriter_ = std::make_unique<ui::EventRewriterChromeOS>(
@@ -5337,7 +5342,7 @@
     return true;
   }
 
-  bool TopRowKeysAreFunctionKeys() const override { return false; }
+  bool TopRowKeysAreFunctionKeys(int device_id) const override { return false; }
 
   bool IsExtensionCommandRegistered(ui::KeyboardCode key_code,
                                     int flags) const override {
@@ -5729,4 +5734,53 @@
       modifier_key_usage_mapping_, 0);
 }
 
+class EventRewriterSettingsSplitTest : public EventRewriterTest {
+ public:
+  void SetUp() override {
+    EventRewriterTest::SetUp();
+    scoped_feature_list_.InitAndEnableFeature(
+        ash::features::kInputDeviceSettingsSplit);
+    controller_resetter_ = std::make_unique<
+        InputDeviceSettingsController::ScopedResetterForTest>();
+    mock_controller_ = std::make_unique<MockInputDeviceSettingsController>();
+    auto deprecation_controller =
+        std::make_unique<DeprecationNotificationController>(&message_center_);
+    deprecation_controller_ = deprecation_controller.get();
+    delegate_ = std::make_unique<EventRewriterDelegateImpl>(
+        nullptr, std::move(deprecation_controller), mock_controller_.get());
+    rewriter_ = std::make_unique<ui::EventRewriterChromeOS>(
+        delegate_.get(), nullptr, false, &fake_ime_keyboard_);
+  }
+
+  void TearDown() override {
+    mock_controller_.reset();
+    controller_resetter_.reset();
+    EventRewriterTest::TearDown();
+  }
+
+ protected:
+  std::unique_ptr<InputDeviceSettingsController::ScopedResetterForTest>
+      controller_resetter_;
+  std::unique_ptr<MockInputDeviceSettingsController> mock_controller_;
+};
+
+TEST_F(EventRewriterSettingsSplitTest, TopRowAreFKeys) {
+  mojom::KeyboardSettings settings;
+  EXPECT_CALL(*mock_controller_, GetKeyboardSettings(kKeyboardDeviceId))
+      .WillRepeatedly(testing::Return(&settings));
+
+  settings.top_row_are_fkeys = false;
+  TestExternalGenericKeyboard(
+      {{ui::ET_KEY_PRESSED,
+        {ui::VKEY_F1, ui::DomCode::F1, ui::EF_NONE, ui::DomKey::F1},
+        {ui::VKEY_BROWSER_BACK, ui::DomCode::BROWSER_BACK, ui::EF_NONE,
+         ui::DomKey::BROWSER_BACK}}});
+
+  settings.top_row_are_fkeys = true;
+  TestExternalGenericKeyboard(
+      {{ui::ET_KEY_PRESSED,
+        {ui::VKEY_F1, ui::DomCode::F1, ui::EF_NONE, ui::DomKey::F1},
+        {ui::VKEY_F1, ui::DomCode::F1, ui::EF_NONE, ui::DomKey::F1}}});
+}
+
 }  // namespace ash
diff --git a/chrome/browser/ash/system/tray_accessibility_browsertest.cc b/chrome/browser/ash/system/tray_accessibility_browsertest.cc
index 8858807..97b2375 100644
--- a/chrome/browser/ash/system/tray_accessibility_browsertest.cc
+++ b/chrome/browser/ash/system/tray_accessibility_browsertest.cc
@@ -7,6 +7,8 @@
 #include "ash/constants/ash_switches.h"
 #include "ash/public/cpp/ash_view_ids.h"
 #include "ash/public/cpp/system_tray_test_api.h"
+#include "ash/system/accessibility/accessibility_detailed_view.h"
+#include "ash/system/tray/tray_detailed_view.h"
 #include "base/functional/callback.h"
 #include "base/run_loop.h"
 #include "base/test/scoped_feature_list.h"
@@ -194,7 +196,10 @@
 
   bool IsMenuButtonVisible() {
     bool visible = tray_test_api_->IsBubbleViewVisible(
-        ash::VIEW_ID_ACCESSIBILITY_TRAY_ITEM, true /* open_tray */);
+        base::FeatureList::IsEnabled(ash::features::kQsRevamp)
+            ? ash::VIEW_ID_ACCESSIBILITY_FEATURE_TILE
+            : ash::VIEW_ID_ACCESSIBILITY_TRAY_ITEM,
+        true /* open_tray */);
     tray_test_api_->CloseBubble();
     return visible;
   }
@@ -206,12 +211,18 @@
   void ClickVirtualKeyboardOnDetailMenu() {
     // Scroll the detailed view to show the virtual keyboard option.
     tray_test_api_->ScrollToShowView(
+        tray_test_api_->GetAccessibilityDetailedView()
+            ->scroll_view_for_testing(),
         ash::VIEW_ID_ACCESSIBILITY_VIRTUAL_KEYBOARD);
     tray_test_api_->ClickBubbleView(
         ash::VIEW_ID_ACCESSIBILITY_VIRTUAL_KEYBOARD);
   }
 
   bool IsVirtualKeyboardEnabledOnDetailMenu() const {
+    if (features::IsQsRevampEnabled()) {
+      return tray_test_api_->IsToggleOn(
+          ash::VIEW_ID_ACCESSIBILITY_VIRTUAL_KEYBOARD_ENABLED);
+    }
     return tray_test_api_->IsBubbleViewVisible(
         ash::VIEW_ID_ACCESSIBILITY_VIRTUAL_KEYBOARD_ENABLED,
         false /* open_tray */);
@@ -571,10 +582,6 @@
 }
 
 IN_PROC_BROWSER_TEST_P(TrayAccessibilityTest, KeepMenuVisibilityOnLockScreen) {
-  // TODO: (b/270609503) test the revapmped view.
-  if (base::FeatureList::IsEnabled(ash::features::kQsRevamp)) {
-    return;
-  }
   // Enables high contrast mode.
   EnableHighContrast(true);
   EXPECT_TRUE(IsMenuButtonVisible());
@@ -598,10 +605,6 @@
 // Do not use a feature which requires an enable/disable confirmation dialog
 // here, as the dialogs change focus and close the detail menu.
 IN_PROC_BROWSER_TEST_P(TrayAccessibilityTest, DetailMenuRemainsOpen) {
-  // TODO: (b/270609503) test the revapmped view.
-  if (base::FeatureList::IsEnabled(ash::features::kQsRevamp)) {
-    return;
-  }
   CreateDetailedMenu();
 
   ClickVirtualKeyboardOnDetailMenu();
@@ -640,10 +643,6 @@
 
 IN_PROC_BROWSER_TEST_P(TrayAccessibilityLoginTest,
                        ShowMenuWithShowOnLoginScreen) {
-  // TODO: (b/270609503) test the revapmped view.
-  if (base::FeatureList::IsEnabled(ash::features::kQsRevamp)) {
-    return;
-  }
   EXPECT_FALSE(user_manager::UserManager::Get()->IsUserLoggedIn());
 
   // Confirms that the menu is visible.
diff --git a/chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl.cc b/chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl.cc
index 698fcdb..9bc0486f 100644
--- a/chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl.cc
+++ b/chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl.cc
@@ -24,6 +24,8 @@
 
 namespace ash::personalization_app {
 
+using DisplayType = KeyboardBacklightColorController::DisplayType;
+
 PersonalizationAppKeyboardBacklightProviderImpl::
     PersonalizationAppKeyboardBacklightProviderImpl(content::WebUI* web_ui)
     : profile_(Profile::FromWebUI(web_ui)) {}
@@ -44,8 +46,9 @@
   keyboard_backlight_observer_remote_.reset();
   keyboard_backlight_observer_remote_.Bind(std::move(observer));
 
-  // Call it once to get the status of color preset.
-  NotifyBacklightColorChanged();
+  // Call it once to get the current status of backlight state (backlight color
+  // is either static color or multi-zone colors).
+  NotifyBacklightStateChanged();
 
   // Bind wallpaper observer now that rgb keyboard section is ready and visible
   // to users.
@@ -70,7 +73,10 @@
       ->keyboard_backlight_color_nudge_controller()
       ->SetUserPerformedAction();
 
-  NotifyBacklightColorChanged();
+  // Get the current status of backlight state as backlight color has changed.
+  // Notifies backlight changed to a static color or rainbow color to highlight
+  // the selected state of color icon button.
+  NotifyBacklightStateChanged();
 }
 
 void PersonalizationAppKeyboardBacklightProviderImpl::SetBacklightZoneColor(
@@ -92,11 +98,10 @@
       ->keyboard_backlight_color_nudge_controller()
       ->SetUserPerformedAction();
 
-  // Notifies backlight changed to |kMultizone| to highlight the selected state
-  // of customization button.
-  NotifyBacklightColorChanged();
-
-  // TODO(b/265855838): Notify backlight zone colors have changed.
+  // Get the current status of backlight state as backlight color has changed to
+  // zone colors. Notifies backlight changed to |kMultizone| to highlight the
+  // selected state of customization button.
+  NotifyBacklightStateChanged();
 }
 
 void PersonalizationAppKeyboardBacklightProviderImpl::ShouldShowNudge(
@@ -135,11 +140,40 @@
 }
 
 void PersonalizationAppKeyboardBacklightProviderImpl::
+    NotifyBacklightStateChanged() {
+  const auto displayType =
+      GetKeyboardBacklightColorController()->GetDisplayType(
+          GetAccountId(profile_));
+  switch (displayType) {
+    case DisplayType::kStatic: {
+      NotifyBacklightColorChanged();
+      return;
+    }
+    case DisplayType::kMultiZone: {
+      NotifyBacklightZoneColorsChanged();
+      return;
+    }
+  }
+}
+
+void PersonalizationAppKeyboardBacklightProviderImpl::
     NotifyBacklightColorChanged() {
   DCHECK(keyboard_backlight_observer_remote_.is_bound());
-  keyboard_backlight_observer_remote_->OnBacklightColorChanged(
-      GetKeyboardBacklightColorController()->GetBacklightColor(
-          GetAccountId(profile_)));
+
+  keyboard_backlight_observer_remote_->OnBacklightStateChanged(
+      ash::personalization_app::mojom::CurrentBacklightState::NewColor(
+          GetKeyboardBacklightColorController()->GetBacklightColor(
+              GetAccountId(profile_))));
+}
+
+void PersonalizationAppKeyboardBacklightProviderImpl::
+    NotifyBacklightZoneColorsChanged() {
+  DCHECK(keyboard_backlight_observer_remote_.is_bound());
+
+  keyboard_backlight_observer_remote_->OnBacklightStateChanged(
+      ash::personalization_app::mojom::CurrentBacklightState::NewZoneColors(
+          GetKeyboardBacklightColorController()->GetBacklightZoneColors(
+              GetAccountId(profile_))));
 }
 
 }  // namespace ash::personalization_app
diff --git a/chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl.h b/chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl.h
index 7534762..f0fbf9f1 100644
--- a/chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl.h
+++ b/chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl.h
@@ -64,9 +64,15 @@
  private:
   KeyboardBacklightColorController* GetKeyboardBacklightColorController();
 
+  // Notify webUI the current status of backlight state.
+  void NotifyBacklightStateChanged();
+
   // Notify webUI the current state of backlight color.
   void NotifyBacklightColorChanged();
 
+  // Notify webUI the current state of backlight zone colors.
+  void NotifyBacklightZoneColorsChanged();
+
   // Pointer to profile of user that opened personalization SWA. Not owned.
   raw_ptr<Profile> const profile_ = nullptr;
 
diff --git a/chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl_unittest.cc b/chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl_unittest.cc
index d73140f..4bdb9b0 100644
--- a/chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl_unittest.cc
+++ b/chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl_unittest.cc
@@ -5,6 +5,7 @@
 #include "chrome/browser/ash/web_applications/personalization_app/personalization_app_keyboard_backlight_provider_impl.h"
 
 #include <memory>
+#include <vector>
 
 #include "ash/constants/ash_features.h"
 #include "ash/system/keyboard_brightness/keyboard_backlight_color_controller.h"
@@ -32,8 +33,9 @@
 class TestKeyboardBacklightObserver
     : public ash::personalization_app::mojom::KeyboardBacklightObserver {
  public:
-  void OnBacklightColorChanged(mojom::BacklightColor backlight_color) override {
-    backlight_color_ = backlight_color;
+  void OnBacklightStateChanged(
+      mojom::CurrentBacklightStatePtr current_backlight_state) override {
+    current_backlight_state_ = std::move(current_backlight_state);
   }
 
   void OnWallpaperColorChanged(SkColor wallpaper_color) override {
@@ -50,9 +52,9 @@
     return keyboard_backlight_observer_receiver_.BindNewPipeAndPassRemote();
   }
 
-  mojom::BacklightColor backlight_color() {
+  mojom::CurrentBacklightState* current_backlight_state() {
     keyboard_backlight_observer_receiver_.FlushForTesting();
-    return backlight_color_;
+    return current_backlight_state_.get();
   }
 
   SkColor wallpaper_color() {
@@ -63,8 +65,8 @@
  private:
   mojo::Receiver<ash::personalization_app::mojom::KeyboardBacklightObserver>
       keyboard_backlight_observer_receiver_{this};
-
-  mojom::BacklightColor backlight_color_ = mojom::BacklightColor::kWallpaper;
+  mojom::CurrentBacklightStatePtr current_backlight_state_ =
+      mojom::CurrentBacklightState::NewColor(mojom::BacklightColor::kRed);
   SkColor wallpaper_color_ = SK_ColorTRANSPARENT;
 };
 
@@ -76,7 +78,9 @@
   PersonalizationAppKeyboardBacklightProviderImplTest()
       : scoped_user_manager_(std::make_unique<ash::FakeChromeUserManager>()),
         profile_manager_(TestingBrowserProcess::GetGlobal()) {
-    scoped_feature_list_.InitWithFeatures({ash::features::kRgbKeyboard}, {});
+    scoped_feature_list_.InitWithFeatures(
+        {ash::features::kRgbKeyboard, ash::features::kMultiZoneRgbKeyboard},
+        {});
   }
   PersonalizationAppKeyboardBacklightProviderImplTest(
       const PersonalizationAppKeyboardBacklightProviderImplTest&) = delete;
@@ -140,9 +144,9 @@
         test_keyboard_backlight_observer_.pending_remote());
   }
 
-  mojom::BacklightColor ObservedBacklightColor() {
+  mojom::CurrentBacklightState* ObservedBacklightColor() {
     keyboard_backlight_provider_remote_.FlushForTesting();
-    return test_keyboard_backlight_observer_.backlight_color();
+    return test_keyboard_backlight_observer_.current_backlight_state();
   }
 
   SkColor ObservedWallpaperColor() {
@@ -175,7 +179,9 @@
       mojom::BacklightColor::kBlue);
 
   // Verify JS side is notified.
-  EXPECT_EQ(mojom::BacklightColor::kBlue, ObservedBacklightColor());
+  EXPECT_TRUE(ObservedBacklightColor()->is_color());
+  EXPECT_EQ(mojom::BacklightColor::kBlue,
+            ObservedBacklightColor()->get_color());
   histogram_tester().ExpectBucketCount(
       kPersonalizationKeyboardBacklightColorHistogramName,
       mojom::BacklightColor::kBlue, 1);
diff --git a/chrome/browser/browsing_data/browsing_data_remover_browsertest_base.cc b/chrome/browser/browsing_data/browsing_data_remover_browsertest_base.cc
index d494a6c..0298eba1 100644
--- a/chrome/browser/browsing_data/browsing_data_remover_browsertest_base.cc
+++ b/chrome/browser/browsing_data/browsing_data_remover_browsertest_base.cc
@@ -187,10 +187,7 @@
     const std::string& script,
     content::WebContents* web_contents) {
   EXPECT_TRUE(web_contents);
-  bool data;
-  EXPECT_TRUE(
-      content::ExecuteScriptAndExtractBool(web_contents, script, &data));
-  return data;
+  return content::EvalJs(web_contents, script).ExtractBool();
 }
 
 void BrowsingDataRemoverBrowserTestBase::VerifyDownloadCount(size_t expected,
diff --git a/chrome/browser/chrome_web_platform_security_metrics_browsertest.cc b/chrome/browser/chrome_web_platform_security_metrics_browsertest.cc
index 20f3b875..223c4be 100644
--- a/chrome/browser/chrome_web_platform_security_metrics_browsertest.cc
+++ b/chrome/browser/chrome_web_platform_security_metrics_browsertest.cc
@@ -2136,6 +2136,132 @@
   CheckCounter(WebFeature::kExecutedJavaScriptURLFromFrame, 0);
 }
 
+IN_PROC_BROWSER_TEST_F(ChromeWebPlatformSecurityMetricsBrowserTest,
+                       DanglingMarkupInIframeName) {
+  GURL url = https_server().GetURL("a.test", "/empty.html");
+  EXPECT_TRUE(content::NavigateToURL(web_contents(), url));
+  EXPECT_TRUE(content::ExecJs(web_contents(), R"(
+    new Promise(resolve => {
+      let iframe = document.createElement("iframe");
+      iframe.src = '/empty.html';
+      iframe.name = "<\n>";
+      iframe.onload = resolve;
+      document.body.appendChild(iframe);
+    });
+  )"));
+  CheckCounter(WebFeature::kDanglingMarkupInWindowName, 1);
+  CheckCounter(WebFeature::kDanglingMarkupInWindowNameNotEndsWithNewLineOrGT,
+               0);
+  CheckCounter(WebFeature::kDanglingMarkupInWindowNameNotEndsWithGT, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInTarget, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInTargetNotEndsWithGT, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInTargetNotEndsWithNewLineOrGT, 0);
+}
+
+IN_PROC_BROWSER_TEST_F(ChromeWebPlatformSecurityMetricsBrowserTest,
+                       DanglingMarkupInNameWithGreaterThan) {
+  GURL url = https_server().GetURL("a.test", "/empty.html");
+  EXPECT_TRUE(content::NavigateToURL(web_contents(), url));
+  EXPECT_TRUE(content::ExecJs(web_contents(), R"(
+    new Promise(resolve => {
+      let iframe = document.createElement("iframe");
+      iframe.src = '/empty.html';
+      iframe.name = "<\n";
+      iframe.onload = resolve;
+      document.body.appendChild(iframe);
+    });
+  )"));
+  CheckCounter(WebFeature::kDanglingMarkupInWindowName, 1);
+  CheckCounter(WebFeature::kDanglingMarkupInWindowNameNotEndsWithNewLineOrGT,
+               0);
+  CheckCounter(WebFeature::kDanglingMarkupInWindowNameNotEndsWithGT, 1);
+  CheckCounter(WebFeature::kDanglingMarkupInTarget, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInTargetNotEndsWithGT, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInTargetNotEndsWithNewLineOrGT, 0);
+}
+
+IN_PROC_BROWSER_TEST_F(ChromeWebPlatformSecurityMetricsBrowserTest,
+                       DanglingMarkupInNameWithNewLineOrGreaterThan) {
+  GURL url = https_server().GetURL("a.test", "/empty.html");
+  EXPECT_TRUE(content::NavigateToURL(web_contents(), url));
+  EXPECT_TRUE(content::ExecJs(web_contents(), R"(
+    new Promise(resolve => {
+      let iframe = document.createElement("iframe");
+      iframe.src = '/empty.html';
+      iframe.name = "<\ntest";
+      iframe.onload = resolve;
+      document.body.appendChild(iframe);
+    });
+  )"));
+
+  CheckCounter(WebFeature::kDanglingMarkupInWindowName, 1);
+  CheckCounter(WebFeature::kDanglingMarkupInWindowNameNotEndsWithNewLineOrGT,
+               1);
+  CheckCounter(WebFeature::kDanglingMarkupInWindowNameNotEndsWithGT, 1);
+  CheckCounter(WebFeature::kDanglingMarkupInTarget, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInTargetNotEndsWithGT, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInTargetNotEndsWithNewLineOrGT, 0);
+}
+
+IN_PROC_BROWSER_TEST_F(ChromeWebPlatformSecurityMetricsBrowserTest,
+                       DanglingMarkupInTarget) {
+  GURL url = https_server().GetURL("a.test", "/empty.html");
+  EXPECT_TRUE(content::NavigateToURL(web_contents(), url));
+  EXPECT_TRUE(content::ExecJs(web_contents(), R"(
+    let link = document.createElement("a");
+    link.href = '/empty.html';
+    link.target = "<\n>";
+    document.body.appendChild(link);
+    link.click();
+  )"));
+
+  CheckCounter(WebFeature::kDanglingMarkupInWindowName, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInWindowNameNotEndsWithNewLineOrGT,
+               0);
+  CheckCounter(WebFeature::kDanglingMarkupInWindowNameNotEndsWithGT, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInTarget, 1);
+  CheckCounter(WebFeature::kDanglingMarkupInTargetNotEndsWithGT, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInTargetNotEndsWithNewLineOrGT, 0);
+}
+
+IN_PROC_BROWSER_TEST_F(ChromeWebPlatformSecurityMetricsBrowserTest,
+                       DanglingMarkupInTargetWithNewLineOrGreaterThan) {
+  GURL url = https_server().GetURL("a.test", "/empty.html");
+  EXPECT_TRUE(content::NavigateToURL(web_contents(), url));
+  EXPECT_TRUE(content::ExecJs(web_contents(), R"(
+    let link = document.createElement("a");
+    link.href = '/empty.html';
+    link.target = "<\n";
+    document.body.appendChild(link);
+    link.click();
+  )"));
+
+  CheckCounter(WebFeature::kDanglingMarkupInWindowName, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInWindowNameNotEndsWithNewLineOrGT,
+               0);
+  CheckCounter(WebFeature::kDanglingMarkupInWindowNameNotEndsWithGT, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInTarget, 1);
+  CheckCounter(WebFeature::kDanglingMarkupInTargetNotEndsWithGT, 1);
+  CheckCounter(WebFeature::kDanglingMarkupInTargetNotEndsWithNewLineOrGT, 0);
+
+  EXPECT_TRUE(content::ExecJs(web_contents(), R"(
+    let base = document.createElement("base");
+    base.target = "<\ntest";
+    document.body.appendChild(base);
+    let link = document.createElement("a");
+    link.href = '/empty.html';
+    document.body.appendChild(link);
+    link.click();
+  )"));
+  CheckCounter(WebFeature::kDanglingMarkupInWindowName, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInWindowNameNotEndsWithNewLineOrGT,
+               0);
+  CheckCounter(WebFeature::kDanglingMarkupInWindowNameNotEndsWithGT, 0);
+  CheckCounter(WebFeature::kDanglingMarkupInTarget, 2);
+  CheckCounter(WebFeature::kDanglingMarkupInTargetNotEndsWithGT, 2);
+  CheckCounter(WebFeature::kDanglingMarkupInTargetNotEndsWithNewLineOrGT, 1);
+}
+
 // TODO(arthursonzogni): Add basic test(s) for the WebFeatures:
 // [ ] CrossOriginOpenerPolicySameOrigin
 // [ ] CrossOriginOpenerPolicySameOriginAllowPopups
diff --git a/chrome/browser/content_settings/content_settings_browsertest.cc b/chrome/browser/content_settings/content_settings_browsertest.cc
index 050838a..1763b88 100644
--- a/chrome/browser/content_settings/content_settings_browsertest.cc
+++ b/chrome/browser/content_settings/content_settings_browsertest.cc
@@ -1645,8 +1645,7 @@
       "LocalStorage",     "SessionStorage", "CacheStorage", "FileSystem",
       "FileSystemAccess", "IndexedDb",      "SharedWorker", "ServiceWorker"};
   for (auto storage_type : storage_types_to_test) {
-    EXPECT_TRUE(content::EvalJs(fenced_frame, "set" + storage_type + "();",
-                                content::EXECUTE_SCRIPT_USE_MANUAL_REPLY)
+    EXPECT_TRUE(content::EvalJs(fenced_frame, "set" + storage_type + "();")
                     .ExtractBool());
   }
 
diff --git a/chrome/browser/content_settings/page_specific_content_settings_delegate.cc b/chrome/browser/content_settings/page_specific_content_settings_delegate.cc
index 3111c4d..fc14ee8 100644
--- a/chrome/browser/content_settings/page_specific_content_settings_delegate.cc
+++ b/chrome/browser/content_settings/page_specific_content_settings_delegate.cc
@@ -22,6 +22,7 @@
 #include "chrome/common/renderer_configuration.mojom.h"
 #include "components/content_settings/browser/page_specific_content_settings.h"
 #include "components/content_settings/core/common/content_settings_utils.h"
+#include "components/permissions/permission_recovery_success_rate_tracker.h"
 #if BUILDFLAG(ENABLE_EXTENSIONS)
 #include "components/guest_view/browser/guest_view_base.h"
 #endif
@@ -80,6 +81,33 @@
 
 void PageSpecificContentSettingsDelegate::UpdateLocationBar() {
   content_settings::UpdateLocationBarUiForWebContents(web_contents());
+
+  PageSpecificContentSettings* pscs = PageSpecificContentSettings::GetForFrame(
+      web_contents()->GetPrimaryMainFrame());
+
+  if (pscs == nullptr) {
+    // There are cases, e.g. MPArch, where there is no active instance of
+    // PageSpecificContentSettings for a frame.
+    return;
+  }
+
+  PageSpecificContentSettings::MicrophoneCameraState state =
+      pscs->GetMicrophoneCameraState();
+
+  if ((state & PageSpecificContentSettings::CAMERA_ACCESSED) ||
+      (state & PageSpecificContentSettings::MICROPHONE_ACCESSED)) {
+    auto* permission_tracker =
+        permissions::PermissionRecoverySuccessRateTracker::FromWebContents(
+            web_contents());
+
+    if (state & PageSpecificContentSettings::MICROPHONE_ACCESSED) {
+      permission_tracker->TrackUsage(ContentSettingsType::MEDIASTREAM_MIC);
+    }
+
+    if (state & PageSpecificContentSettings::CAMERA_ACCESSED) {
+      permission_tracker->TrackUsage(ContentSettingsType::MEDIASTREAM_CAMERA);
+    }
+  }
 }
 
 PrefService* PageSpecificContentSettingsDelegate::GetPrefs() {
diff --git a/chrome/browser/content_settings/page_specific_content_settings_unittest.cc b/chrome/browser/content_settings/page_specific_content_settings_unittest.cc
index cd9b7e5..15dd255 100644
--- a/chrome/browser/content_settings/page_specific_content_settings_unittest.cc
+++ b/chrome/browser/content_settings/page_specific_content_settings_unittest.cc
@@ -9,6 +9,7 @@
 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
 #include "chrome/browser/content_settings/page_specific_content_settings_delegate.h"
 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "components/permissions/permission_recovery_success_rate_tracker.h"
 #include "content/public/browser/web_contents.h"
 #include "testing/gmock/include/gmock/gmock-matchers.h"
 
@@ -26,6 +27,9 @@
         web_contents(),
         std::make_unique<chrome::PageSpecificContentSettingsDelegate>(
             web_contents()));
+
+    permissions::PermissionRecoverySuccessRateTracker::CreateForWebContents(
+        web_contents());
   }
 };
 
diff --git a/chrome/browser/crash_upload_list/BUILD.gn b/chrome/browser/crash_upload_list/BUILD.gn
new file mode 100644
index 0000000..da014a87
--- /dev/null
+++ b/chrome/browser/crash_upload_list/BUILD.gn
@@ -0,0 +1,70 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/buildflag_header.gni")
+import("//extensions/buildflags/buildflags.gni")
+import("//testing/test.gni")
+
+static_library("crash_upload_list") {
+  sources = [
+    "crash_upload_list.cc",
+    "crash_upload_list.h",
+  ]
+
+  if (is_android) {
+    sources += [
+      "crash_upload_list_android.cc",
+      "crash_upload_list_android.h",
+    ]
+  }
+
+  if (is_chromeos) {
+    sources += [
+      "crash_upload_list_chromeos.cc",
+      "crash_upload_list_chromeos.h",
+    ]
+  }
+
+  if (is_fuchsia) {
+    sources += [
+      "crash_upload_list_fuchsia.cc",
+      "crash_upload_list_fuchsia.h",
+    ]
+  }
+
+  deps = [
+    "//chrome/common:constants",
+    "//components/crash/core/browser:browser",
+    "//components/upload_list",
+    "//ui/base:base",
+  ]
+
+  if (is_android) {
+    deps += [ "//chrome/android:chrome_jni_headers" ]
+  }
+
+  if (!is_fuchsia) {
+    deps += [ "//components/crash/core/app:app" ]
+  }
+
+  public_deps = [
+    "//base",
+    "//third_party/abseil-cpp:absl",
+  ]
+}
+
+source_set("unit_tests") {
+  testonly = true
+
+  if (is_chromeos) {
+    sources = [ "crash_upload_list_chromeos_unittest.cc" ]
+  }
+
+  deps = [
+    ":crash_upload_list",
+    "//base/test:test_support",
+    "//testing/gmock",
+    "//testing/gtest",
+  ]
+}
diff --git a/chrome/browser/error_reporting/BUILD.gn b/chrome/browser/error_reporting/BUILD.gn
index 7eff1312..8b041701 100644
--- a/chrome/browser/error_reporting/BUILD.gn
+++ b/chrome/browser/error_reporting/BUILD.gn
@@ -72,6 +72,7 @@
     ":error_reporting",
     ":test_support",
     "//base",
+    "//chrome/browser/crash_upload_list",
     "//chrome/test:test_support",
     "//components/crash/content/browser/error_reporting",
     "//components/crash/content/browser/error_reporting:mock_crash_endpoint",
diff --git a/chrome/browser/extensions/api/history/history_apitest.cc b/chrome/browser/extensions/api/history/history_apitest.cc
index b26ec518..a204862 100644
--- a/chrome/browser/extensions/api/history/history_apitest.cc
+++ b/chrome/browser/extensions/api/history/history_apitest.cc
@@ -13,23 +13,11 @@
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/test_navigation_observer.h"
+#include "extensions/browser/background_script_executor.h"
 #include "extensions/browser/process_manager.h"
 #include "extensions/test/extension_test_message_listener.h"
 #include "net/dns/mock_host_resolver.h"
 
-namespace {
-
-std::string RunScriptAndReturnResult(const extensions::ExtensionHost* host,
-                                     const std::string& script) {
-  std::string result;
-  EXPECT_TRUE(content::ExecuteScriptAndExtractString(host->host_contents(),
-                                                     script, &result))
-      << script;
-  return result;
-}
-
-}  // namespace
-
 namespace extensions {
 
 class HistoryApiTest : public ExtensionApiTest {
@@ -40,50 +28,49 @@
     host_resolver()->AddRule("www.a.com", "127.0.0.1");
     host_resolver()->AddRule("www.b.com", "127.0.0.1");
   }
+
+  static std::string ExecuteScript(const ExtensionId& extension_id,
+                                   content::BrowserContext* context,
+                                   const std::string& script) {
+    base::Value result = BackgroundScriptExecutor::ExecuteScript(
+        context, extension_id, script,
+        BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
+    EXPECT_TRUE(result.is_string());
+    return result.is_string() ? result.GetString() : std::string();
+  }
 };
 
 IN_PROC_BROWSER_TEST_F(HistoryApiTest, MiscSearch) {
   ASSERT_TRUE(StartEmbeddedTestServer());
-  ASSERT_TRUE(RunExtensionTest("history/regular",
-                               {.extension_url = "misc_search.html"}))
-      << message_;
+  ASSERT_TRUE(RunExtensionTest("history/regular/misc_search")) << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(HistoryApiTest, TimedSearch) {
   ASSERT_TRUE(StartEmbeddedTestServer());
-  ASSERT_TRUE(RunExtensionTest("history/regular",
-                               {.extension_url = "timed_search.html"}))
-      << message_;
+  ASSERT_TRUE(RunExtensionTest("history/regular/timed_search")) << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(HistoryApiTest, Delete) {
   ASSERT_TRUE(StartEmbeddedTestServer());
-  ASSERT_TRUE(
-      RunExtensionTest("history/regular", {.extension_url = "delete.html"}))
-      << message_;
+  ASSERT_TRUE(RunExtensionTest("history/regular/delete")) << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(HistoryApiTest, DeleteProhibited) {
   browser()->profile()->GetPrefs()->
       SetBoolean(prefs::kAllowDeletingBrowserHistory, false);
   ASSERT_TRUE(StartEmbeddedTestServer());
-  ASSERT_TRUE(RunExtensionTest("history/regular",
-                               {.extension_url = "delete_prohibited.html"}))
+  ASSERT_TRUE(RunExtensionTest("history/regular/delete_prohibited"))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(HistoryApiTest, GetVisits) {
   ASSERT_TRUE(StartEmbeddedTestServer());
-  ASSERT_TRUE(
-      RunExtensionTest("history/regular", {.extension_url = "get_visits.html"}))
-      << message_;
+  ASSERT_TRUE(RunExtensionTest("history/regular/get_visits")) << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(HistoryApiTest, SearchAfterAdd) {
   ASSERT_TRUE(StartEmbeddedTestServer());
-  ASSERT_TRUE(RunExtensionTest("history/regular",
-                               {.extension_url = "search_after_add.html"}))
-      << message_;
+  ASSERT_TRUE(RunExtensionTest("history/regular/search_after_add")) << message_;
 }
 
 // Test when History API is used from incognito mode, it has access to the
@@ -92,6 +79,7 @@
   ASSERT_TRUE(StartEmbeddedTestServer());
   // Setup.
   Browser* incognito_browser = CreateIncognitoBrowser(browser()->profile());
+  Profile* incognito_profile = incognito_browser->profile();
   ExtensionTestMessageListener regular_listener("regular ready");
   ExtensionTestMessageListener incognito_listener("incognito ready");
   const Extension* extension =
@@ -101,32 +89,23 @@
   ASSERT_TRUE(regular_listener.WaitUntilSatisfied());
   ASSERT_TRUE(incognito_listener.WaitUntilSatisfied());
 
-  ExtensionHost* on_the_record_background_page =
-      ProcessManager::Get(browser()->profile())
-          ->GetBackgroundHostForExtension(extension->id());
-  ASSERT_TRUE(on_the_record_background_page);
-
-  ExtensionHost* incognito_background_page =
-      ProcessManager::Get(incognito_browser->profile())
-          ->GetBackgroundHostForExtension(extension->id());
-  ASSERT_TRUE(incognito_background_page);
-  EXPECT_NE(incognito_background_page, on_the_record_background_page);
+  const ExtensionId& extension_id = extension->id();
 
   // Check if history is empty in regular mode.
-  EXPECT_EQ("0", RunScriptAndReturnResult(on_the_record_background_page,
-                                          "countItemsInHistory()"));
+  EXPECT_EQ("0",
+            ExecuteScript(extension_id, profile(), "countItemsInHistory()"));
 
   // Insert an item in incognito mode.
-  EXPECT_EQ(std::string("success"),
-            RunScriptAndReturnResult(incognito_background_page, "addItem()"));
+  EXPECT_EQ("success",
+            ExecuteScript(extension_id, incognito_profile, "addItem()"));
 
   // Check history in incognito mode.
-  EXPECT_EQ("1", RunScriptAndReturnResult(on_the_record_background_page,
-                                          "countItemsInHistory()"));
+  EXPECT_EQ("1", ExecuteScript(extension_id, incognito_profile,
+                               "countItemsInHistory()"));
 
   // Check history in regular mode.
-  EXPECT_EQ("1", RunScriptAndReturnResult(on_the_record_background_page,
-                                          "countItemsInHistory()"));
+  EXPECT_EQ("1",
+            ExecuteScript(extension_id, profile(), "countItemsInHistory()"));
 
   // Perform navigation in incognito mode.
   const GURL b_com =
@@ -137,13 +116,13 @@
   EXPECT_TRUE(incognito_observer.last_navigation_succeeded());
 
   // Check history in regular mode is not modified by incognito navigation.
-  EXPECT_EQ("1", RunScriptAndReturnResult(on_the_record_background_page,
-                                          "countItemsInHistory()"));
+  EXPECT_EQ("1",
+            ExecuteScript(extension_id, profile(), "countItemsInHistory()"));
 
   // Check that history in incognito mode is not modified by navigation as
   // incognito navigations are not recorded in history.
-  EXPECT_EQ("1", RunScriptAndReturnResult(incognito_background_page,
-                                          "countItemsInHistory()"));
+  EXPECT_EQ("1", ExecuteScript(extension_id, incognito_profile,
+                               "countItemsInHistory()"));
 
   // Perform navigation in regular mode.
   content::TestNavigationObserver regular_observer(
@@ -152,12 +131,12 @@
   EXPECT_TRUE(regular_observer.last_navigation_succeeded());
 
   // Check history in regular mode is modified by navigation.
-  EXPECT_EQ("2", RunScriptAndReturnResult(on_the_record_background_page,
-                                          "countItemsInHistory()"));
+  EXPECT_EQ("2",
+            ExecuteScript(extension_id, profile(), "countItemsInHistory()"));
 
   // Check history in incognito mode is modified by navigation.
-  EXPECT_EQ("2", RunScriptAndReturnResult(incognito_background_page,
-                                          "countItemsInHistory()"));
+  EXPECT_EQ("2", ExecuteScript(extension_id, incognito_profile,
+                               "countItemsInHistory()"));
 }
 
 }  // namespace extensions
diff --git a/chrome/browser/extensions/api/permissions/permissions_api_helpers_unittest.cc b/chrome/browser/extensions/api/permissions/permissions_api_helpers_unittest.cc
index 77e14b49..b565437 100644
--- a/chrome/browser/extensions/api/permissions/permissions_api_helpers_unittest.cc
+++ b/chrome/browser/extensions/api/permissions/permissions_api_helpers_unittest.cc
@@ -36,8 +36,6 @@
 TEST(ExtensionPermissionsAPIHelpers, Pack) {
   APIPermissionSet apis;
   apis.insert(APIPermissionID::kTab);
-  apis.insert(APIPermissionID::kFileBrowserHandler);
-  // Note: kFileBrowserHandler implies kFileBrowserHandlerInternal.
 
   URLPatternSet explicit_hosts(
       {URLPattern(Extension::kValidHostPermissionSchemes, "http://a.com/*"),
@@ -52,9 +50,7 @@
                     std::move(explicit_hosts), std::move(scriptable_hosts))));
   ASSERT_TRUE(pack_result);
   ASSERT_TRUE(pack_result->permissions);
-  EXPECT_THAT(*pack_result->permissions,
-              testing::UnorderedElementsAre("tabs", "fileBrowserHandler",
-                                            "fileBrowserHandlerInternal"));
+  EXPECT_THAT(*pack_result->permissions, testing::UnorderedElementsAre("tabs"));
 
   ASSERT_TRUE(pack_result->origins);
   EXPECT_THAT(*pack_result->origins, testing::UnorderedElementsAre(
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 0025e3f..7141e8a3 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -874,11 +874,6 @@
     "expiry_milestone": 118
   },
   {
-    "name": "calendar-view",
-    "owners": ["jiamingc", "cros-status-area-eng@google.com"],
-    "expiry_milestone" : 106
-  },
-  {
     "name": "calendar-view-debug-mode",
     "owners": [ "rtinkoff" ],
     "expiry_milestone": 113
@@ -4399,11 +4394,6 @@
     "expiry_milestone": 95
   },
   {
-    "name": "interest-feed-v2-clicks-and-views-cond-upload",
-    "owners": [ "//chrome/android/feed/OWNERS", "feed@chromium.org", "edchin@chromium.org" ],
-    "expiry_milestone": 95
-  },
-  {
     "name": "ios-autofill-branding",
     "owners": [ "ginnyhuang@google.com", "bling-flags@google.com" ],
     "expiry_milestone": 115
@@ -5141,6 +5131,11 @@
     "expiry_milestone": 120
   },
   {
+    "name": "omnibox-adapt-narrow-tablet-windows",
+    "owners": [ "pnoland", "chrome-omnibox-team@google.com" ],
+    "expiry_milestone": 120
+  },
+  {
     "name": "omnibox-adaptive-suggestions-count",
     "owners": [ "stkhapugin", "chrome-omnibox-team@google.com" ],
     "expiry_milestone": 120
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index ea95fa7..df8b613 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -2034,6 +2034,12 @@
     "flag to adjust the limit of offered suggestions. The number of shown "
     "suggestions will be no less than the platform default limit.";
 
+const char kOmniboxAdaptNarrowTabletWindowsName[] =
+    "Omnibox adapts to narrow tablet windows";
+const char kOmniboxAdaptNarrowTabletWindowsDescription[] =
+    "When enabled the omnibox uses a phone-like appearance for windows that "
+    "are less than 600dp wide, regardless of the overall display width.";
+
 const char kOmniboxAssistantVoiceSearchName[] =
     "Omnibox Assistant Voice Search";
 const char kOmniboxAssistantVoiceSearchDescription[] =
@@ -4972,12 +4978,6 @@
 const char kCalendarJellyDescription[] =
     "Enables Jelly changes for the sys tray Calendar views.";
 
-const char kCalendarViewName[] =
-    "Productivity experiment: Monthly Calendar View";
-const char kCalendarViewDescription[] =
-    "Show Monthly Calendar View with Google Calendar events to increase "
-    "productivity by helping users view their schedules more quickly.";
-
 const char kCaptureModeDemoToolsName[] =
     "Enable demo tools feature in screen capture";
 const char kCaptureModeDemoToolsDescription[] =
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 8bbcc8e3..895b160 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1146,6 +1146,9 @@
 extern const char kOmniboxAdaptiveSuggestionsCountName[];
 extern const char kOmniboxAdaptiveSuggestionsCountDescription[];
 
+extern const char kOmniboxAdaptNarrowTabletWindowsName[];
+extern const char kOmniboxAdaptNarrowTabletWindowsDescription[];
+
 extern const char kOmniboxAssistantVoiceSearchName[];
 extern const char kOmniboxAssistantVoiceSearchDescription[];
 
@@ -2859,9 +2862,6 @@
 extern const char kCalendarJellyName[];
 extern const char kCalendarJellyDescription[];
 
-extern const char kCalendarViewName[];
-extern const char kCalendarViewDescription[];
-
 extern const char kCalendarModelDebugModeName[];
 extern const char kCalendarModelDebugModeDescription[];
 
diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc
index b23a4da..0784bd4a 100644
--- a/chrome/browser/flags/android/chrome_feature_list.cc
+++ b/chrome/browser/flags/android/chrome_feature_list.cc
@@ -245,6 +245,7 @@
     &kBackGestureRefactorActivityAndroid,
     &kBackGestureRefactorAndroid,
     &kOmahaMinSdkVersionAndroid,
+    &kOmniboxAdaptNarrowTabletWindows,
     &kOmniboxConsumesImeInsets,
     &kOmniboxModernizeVisualUpdate,
     &kOpaqueOriginForIncomingIntents,
@@ -784,6 +785,10 @@
              "OmahaMinSdkVersionAndroid",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+BASE_FEATURE(kOmniboxAdaptNarrowTabletWindows,
+             "OmniboxAdaptNarrowTabletWindows",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 BASE_FEATURE(kOmniboxConsumesImeInsets,
              "OmniboxConsumesImeInsets",
              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 f777837..02f0266 100644
--- a/chrome/browser/flags/android/chrome_feature_list.h
+++ b/chrome/browser/flags/android/chrome_feature_list.h
@@ -106,6 +106,7 @@
 BASE_DECLARE_FEATURE(kNotificationPermissionVariant);
 BASE_DECLARE_FEATURE(kNotificationPermissionBottomSheet);
 BASE_DECLARE_FEATURE(kOmahaMinSdkVersionAndroid);
+BASE_DECLARE_FEATURE(kOmniboxAdaptNarrowTabletWindows);
 BASE_DECLARE_FEATURE(kOmniboxConsumesImeInsets);
 BASE_DECLARE_FEATURE(kOmniboxModernizeVisualUpdate);
 BASE_DECLARE_FEATURE(kOptimizeGeolocationHeaderGeneration);
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 b3d4715..053e8b2 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
@@ -377,6 +377,8 @@
     public static final String OFFLINE_PAGES_LIVE_PAGE_SHARING = "OfflinePagesLivePageSharing";
     public static final String OFFLINE_PAGES_PREFETCHING = "OfflinePagesPrefetching";
     public static final String OMAHA_MIN_SDK_VERSION_ANDROID = "OmahaMinSdkVersionAndroid";
+    public static final String OMNIBOX_ADAPT_NARROW_TABLET_WINDOWS =
+            "OmniboxAdaptNarrowTabletWindows";
     public static final String OMNIBOX_ASSISTANT_VOICE_SEARCH = "OmniboxAssistantVoiceSearch";
     public static final String OMNIBOX_CONSUMERS_IME_INSETS = "OmniboxConsumesImeInsets";
     public static final String OMNIBOX_MATCH_TOOLBAR_AND_STATUS_BAR_COLOR =
diff --git a/chrome/browser/media/router/mojo/media_router_debugger_impl_unittest.cc b/chrome/browser/media/router/mojo/media_router_debugger_impl_unittest.cc
index 8d43983..b9d8628 100644
--- a/chrome/browser/media/router/mojo/media_router_debugger_impl_unittest.cc
+++ b/chrome/browser/media/router/mojo/media_router_debugger_impl_unittest.cc
@@ -7,7 +7,6 @@
 #include "base/test/gmock_callback_support.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/mock_callback.h"
-#include "base/test/scoped_feature_list.h"
 #include "chrome/browser/media/router/mojo/media_router_mojo_impl.h"
 #include "chrome/browser/media/router/test/provider_test_helpers.h"
 #include "chrome/test/base/testing_profile.h"
@@ -114,8 +113,6 @@
 
 TEST_F(MediaRouterDebuggerImplTest, NonMirroringRoutes) {
   debugger()->EnableRtcpReports();
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitWithFeatures({media::kEnableRtcpReporting}, {});
 
   const std::vector<MediaRoute> routes{CreateMediaRoute()};
   EXPECT_CALL(observer_, OnMirroringStatsUpdated(_)).Times(0);
@@ -124,8 +121,6 @@
 
 TEST_F(MediaRouterDebuggerImplTest, FetchMirroringStats) {
   debugger()->EnableRtcpReports();
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitWithFeatures({media::kEnableRtcpReporting}, {});
 
   const std::vector<MediaRoute> routes{CreateTabMirroringMediaRoute()};
   EXPECT_CALL(observer_, OnMirroringStatsUpdated(_)).Times(1);
diff --git a/chrome/browser/navigation_predictor/anchor_element_preloader_browsertest.cc b/chrome/browser/navigation_predictor/anchor_element_preloader_browsertest.cc
index 5974afb..f6e6f45 100644
--- a/chrome/browser/navigation_predictor/anchor_element_preloader_browsertest.cc
+++ b/chrome/browser/navigation_predictor/anchor_element_preloader_browsertest.cc
@@ -1,6 +1,8 @@
 // Copyright 2022 The Chromium Authors
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
+
+#include "base/metrics/field_trial_params.h"
 #include "base/run_loop.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/metrics/histogram_tester.h"
@@ -41,13 +43,16 @@
   static constexpr char kOrigin1[] = "https://www.origin1.com/";
   static constexpr char kOrigin2[] = "https://www.origin2.com/";
 
-  virtual void SetFeatures() {
-    feature_list_.InitAndEnableFeature(
-        blink::features::kAnchorElementInteraction);
+  virtual base::FieldTrialParams GetAnchorElementInteractionFieldTrialParams() {
+    return {};
   }
 
   void SetUp() override {
-    SetFeatures();
+    feature_list_.InitWithFeaturesAndParameters(
+        {{blink::features::kAnchorElementInteraction,
+          GetAnchorElementInteractionFieldTrialParams()},
+         {blink::features::kSpeculationRulesPointerDownHeuristics, {}}},
+        {blink::features::kSpeculationRulesPointerHoverHeuristics});
     https_server_ = std::make_unique<net::EmbeddedTestServer>(
         net::EmbeddedTestServer::TYPE_HTTPS);
     https_server_->ServeFilesFromSourceDirectory("chrome/test/data/preload");
@@ -370,10 +375,9 @@
 class AnchorElementPreloaderHoldbackBrowserTest
     : public AnchorElementPreloaderBrowserTest {
  public:
-  void SetFeatures() override {
-    feature_list_.InitAndEnableFeatureWithParameters(
-        blink::features::kAnchorElementInteraction,
-        {{"preconnect_holdback", "true"}});
+  base::FieldTrialParams GetAnchorElementInteractionFieldTrialParams()
+      override {
+    return {{"preconnect_holdback", "true"}};
   }
 };
 
@@ -422,10 +426,9 @@
 class AnchorElementPreloaderLimitedBrowserTest
     : public AnchorElementPreloaderBrowserTest {
  public:
-  void SetFeatures() override {
-    feature_list_.InitAndEnableFeatureWithParameters(
-        blink::features::kAnchorElementInteraction,
-        {{"max_preloading_attempts", "1"}});
+  base::FieldTrialParams GetAnchorElementInteractionFieldTrialParams()
+      override {
+    return {{"max_preloading_attempts", "1"}};
   }
 };
 
diff --git a/chrome/browser/net/storage_test_utils.cc b/chrome/browser/net/storage_test_utils.cc
index 756b97f..8532453 100644
--- a/chrome/browser/net/storage_test_utils.cc
+++ b/chrome/browser/net/storage_test_utils.cc
@@ -55,9 +55,7 @@
   base::flat_map<std::string, bool> expected;
   for (const auto& data_type : GetStorageTypesForFrame(include_cookies)) {
     actual[data_type] =
-        content::EvalJs(frame, "set" + data_type + "()",
-                        content::EXECUTE_SCRIPT_USE_MANUAL_REPLY)
-            .ExtractBool();
+        content::EvalJs(frame, "set" + data_type + "()").ExtractBool();
     if (frame->GetLastCommittedOrigin() !=
             frame->GetMainFrame()->GetLastCommittedOrigin() &&
         data_type == "WebSql") {
@@ -75,9 +73,7 @@
   base::flat_map<std::string, bool> expected;
   for (const auto& data_type : kStorageTypesForWorker) {
     actual[data_type] =
-        content::EvalJs(frame, "set" + data_type + "()",
-                        content::EXECUTE_SCRIPT_USE_MANUAL_REPLY)
-            .ExtractBool();
+        content::EvalJs(frame, "set" + data_type + "()").ExtractBool();
     expected[data_type] = true;
   }
   EXPECT_THAT(actual, testing::UnorderedElementsAreArray(expected));
@@ -90,9 +86,7 @@
   base::flat_map<std::string, bool> expected_elts;
   for (const auto& data_type : GetStorageTypesForFrame(include_cookies)) {
     actual[data_type] =
-        content::EvalJs(frame, "has" + data_type + "();",
-                        content::EXECUTE_SCRIPT_USE_MANUAL_REPLY)
-            .ExtractBool();
+        content::EvalJs(frame, "has" + data_type + "();").ExtractBool();
     if (frame->GetLastCommittedOrigin() !=
             frame->GetMainFrame()->GetLastCommittedOrigin() &&
         data_type == "WebSql") {
@@ -110,9 +104,7 @@
   base::flat_map<std::string, bool> expected_elts;
   for (const auto& data_type : kStorageTypesForWorker) {
     actual[data_type] =
-        content::EvalJs(frame, "has" + data_type + "();",
-                        content::EXECUTE_SCRIPT_USE_MANUAL_REPLY)
-            .ExtractBool();
+        content::EvalJs(frame, "has" + data_type + "();").ExtractBool();
     expected_elts[data_type] = expected;
   }
   EXPECT_THAT(actual, testing::UnorderedElementsAreArray(expected_elts));
@@ -123,9 +115,7 @@
   base::flat_map<std::string, bool> expected;
   for (const auto& data_type : kCrossTabCommunicationTypes) {
     actual[data_type] =
-        content::EvalJs(frame, "set" + data_type + "()",
-                        content::EXECUTE_SCRIPT_USE_MANUAL_REPLY)
-            .ExtractBool();
+        content::EvalJs(frame, "set" + data_type + "()").ExtractBool();
     expected[data_type] = true;
   }
   EXPECT_THAT(actual, testing::UnorderedElementsAreArray(expected));
@@ -137,9 +127,7 @@
   base::flat_map<std::string, bool> expected_elts;
   for (const auto& data_type : kCrossTabCommunicationTypes) {
     actual[data_type] =
-        content::EvalJs(frame, "has" + data_type + "();",
-                        content::EXECUTE_SCRIPT_USE_MANUAL_REPLY)
-            .ExtractBool();
+        content::EvalJs(frame, "has" + data_type + "();").ExtractBool();
     expected_elts[data_type] = expected;
   }
   EXPECT_THAT(actual, testing::UnorderedElementsAreArray(expected_elts));
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille/liblouis.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille/liblouis.js
index c4552f55..3e458e0d 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille/liblouis.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille/liblouis.js
@@ -50,6 +50,17 @@
     this.loadOrReload_(opt_loadCallback);
   }
 
+  /**
+   * Convenience method to wait for the constructor to resolve its callback.
+   * @param {string} wasmPath Path to .wasm file for the module.
+   * @param {string=} opt_tablesDir Path to tables directory.
+   * @return {!Promise<LibLouis>}
+   */
+  static async create(wasmPath, opt_tablesDir) {
+    return new Promise(
+        resolve => new LibLouis(wasmPath, opt_tablesDir, resolve));
+  }
+
   isLoaded() {
     return this.isLoaded_;
   }
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille/liblouis_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille/liblouis_test.js
index 4b2128297..0b2e37e 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille/liblouis_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille/liblouis_test.js
@@ -21,13 +21,6 @@
         'BrailleKeyEvent', '/chromevox/common/braille/braille_key_types.js');
   }
 
-  createLiblouis() {
-    return new LibLouis(
-        chrome.extension.getURL(
-            'chromevox/background/braille/liblouis_wrapper.js'),
-        '', () => {});
-  }
-
   withTranslator(liblouis, tableNames, callback) {
     liblouis.getTranslator(tableNames, this.newCallback(callback));
   }
@@ -45,13 +38,14 @@
 function LIBLOUIS_TEST_F(testName, testFunc, opt_preamble) {
   // This needs to stay a function - don't convert to arrow function.
   const wrappedTestFunc = function() {
-    const liblouis = new LibLouis(
-        chrome.extension.getURL(
-            'chromevox/background/braille/liblouis_wrapper.js'),
-        // This needs to stay bound - don't convert to arrow function.
-        '', testFunc.bind(this));
+    LibLouis
+        .create(
+            chrome.extension.getURL(
+                'chromevox/background/braille/liblouis_wrapper.js'),
+            '')
+        .then(liblouis => testFunc(liblouis));
   };
-  TEST_F('ChromeVoxLibLouisTest', testName, wrappedTestFunc, opt_preamble);
+  AX_TEST_F('ChromeVoxLibLouisTest', testName, wrappedTestFunc, opt_preamble);
 }
 
 function LIBLOUIS_TEST_F_WITH_PREAMBLE(preamble, testName, testFunc) {
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/common/command_store.js b/chrome/browser/resources/chromeos/accessibility/chromevox/common/command_store.js
index 57a2b56f..99b88202 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/common/command_store.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/common/command_store.js
@@ -29,7 +29,7 @@
    * @return {string|undefined} The message id, if any.
    */
   static messageForCommand(command) {
-    return (CommandStore.COMMAND_DATA[command] || {}).msgId;
+    return CommandStore.COMMAND_DATA[command]?.msgId;
   }
 
   /**
@@ -38,7 +38,7 @@
    * @return {string|undefined} The category, if any.
    */
   static categoryForCommand(command) {
-    return (CommandStore.COMMAND_DATA[command] || {}).category;
+    return CommandStore.COMMAND_DATA[command]?.category;
   }
 
   /**
@@ -292,13 +292,10 @@
 
 /**
  * @typedef {{
- *     announce: boolean,
  *     category: (undefined|!CommandCategory),
  *     msgId: (undefined|string),
  *     denySignedOut: (undefined|boolean)
  * }}
- *  announce: Whether to call finishNavCommand and announce the current
- *            position after the command is done.
  *  category: The command's category.
  *  msgId: The message resource describing the command.
  *  denySignedOut: Explicitly denies this command when on chrome://oobe/* or
@@ -312,76 +309,62 @@
  */
 CommandStore.COMMAND_DATA = {
   [Command.TOGGLE_STICKY_MODE]: {
-    announce: false,
     msgId: 'toggle_sticky_mode',
     category: CommandCategory.MODIFIER_KEYS,
   },
   [Command.PASS_THROUGH_MODE]: {
-    announce: false,
     msgId: 'pass_through_key_description',
     category: CommandCategory.MODIFIER_KEYS,
   },
 
   [Command.STOP_SPEECH]: {
-    announce: false,
     msgId: 'stop_speech_key',
     category: CommandCategory.CONTROLLING_SPEECH,
   },
-  [Command.OPEN_CHROMEVOX_MENUS]: {announce: false, msgId: 'menus_title'},
+  [Command.OPEN_CHROMEVOX_MENUS]: {msgId: 'menus_title'},
   [Command.RESET_TEXT_TO_SPEECH_SETTINGS]: {
-    announce: false,
     msgId: 'reset_tts_settings',
     category: CommandCategory.CONTROLLING_SPEECH,
   },
   [Command.DECREASE_TTS_RATE]: {
-    announce: false,
     msgId: 'decrease_tts_rate',
     category: CommandCategory.CONTROLLING_SPEECH,
   },
   [Command.INCREASE_TTS_RATE]: {
-    announce: false,
     msgId: 'increase_tts_rate',
     category: CommandCategory.CONTROLLING_SPEECH,
   },
   [Command.DECREASE_TTS_PITCH]: {
-    announce: false,
     msgId: 'decrease_tts_pitch',
     category: CommandCategory.CONTROLLING_SPEECH,
   },
   [Command.INCREASE_TTS_PITCH]: {
-    announce: false,
     msgId: 'increase_tts_pitch',
     category: CommandCategory.CONTROLLING_SPEECH,
   },
   [Command.DECREASE_TTS_VOLUME]: {
-    announce: false,
     msgId: 'decrease_tts_volume',
     category: CommandCategory.CONTROLLING_SPEECH,
   },
   [Command.INCREASE_TTS_VOLUME]: {
-    announce: false,
     msgId: 'increase_tts_volume',
     category: CommandCategory.CONTROLLING_SPEECH,
   },
   [Command.CYCLE_PUNCTUATION_ECHO]: {
-    announce: false,
     msgId: 'cycle_punctuation_echo',
     category: CommandCategory.CONTROLLING_SPEECH,
   },
   [Command.CYCLE_TYPING_ECHO]: {
-    announce: false,
     msgId: 'cycle_typing_echo',
     category: CommandCategory.CONTROLLING_SPEECH,
   },
 
   [Command.TOGGLE_DICTATION]: {
-    announce: false,
     msgId: 'toggle_dictation',
     category: CommandCategory.ACTIONS,
   },
 
   [Command.TOGGLE_EARCONS]: {
-    announce: true,
     msgId: 'toggle_earcons',
     category: CommandCategory.CONTROLLING_SPEECH,
   },
@@ -400,204 +383,166 @@
     category: CommandCategory.NAVIGATION,
   },
   [Command.FORWARD]: {
-    announce: true,
     msgId: 'forward',
     category: CommandCategory.NAVIGATION,
   },
   [Command.BACKWARD]: {
-    announce: true,
     msgId: 'backward',
     category: CommandCategory.NAVIGATION,
   },
   [Command.RIGHT]: {
-    announce: true,
     msgId: 'right',
     category: CommandCategory.NAVIGATION,
   },
   [Command.LEFT]: {
-    announce: true,
     msgId: 'left',
     category: CommandCategory.NAVIGATION,
   },
   [Command.PREVIOUS_GRANULARITY]: {
-    announce: true,
     msgId: 'previous_granularity',
     category: CommandCategory.NAVIGATION,
   },
   [Command.NEXT_GRANULARITY]: {
-    announce: true,
     msgId: 'next_granularity',
     category: CommandCategory.NAVIGATION,
   },
   [Command.PREVIOUS_AT_GRANULARITY]: {
-    announce: true,
     msgId: 'previous_at_granularity',
     category: CommandCategory.NAVIGATION,
   },
   [Command.NEXT_AT_GRANULARITY]: {
-    announce: true,
     msgId: 'next_at_granularity',
     category: CommandCategory.NAVIGATION,
   },
 
   [Command.PREVIOUS_CHARACTER]: {
-    announce: true,
     msgId: 'previous_character',
     category: CommandCategory.NAVIGATION,
   },
   [Command.NEXT_CHARACTER]: {
-    announce: true,
     msgId: 'next_character',
     category: CommandCategory.NAVIGATION,
   },
   [Command.PREVIOUS_WORD]: {
-    announce: true,
     msgId: 'previous_word',
     category: CommandCategory.NAVIGATION,
   },
   [Command.NEXT_WORD]: {
-    announce: true,
     msgId: 'next_word',
     category: CommandCategory.NAVIGATION,
   },
   [Command.PREVIOUS_LINE]: {
-    announce: true,
     msgId: 'previous_line',
     category: CommandCategory.NAVIGATION,
   },
   [Command.NEXT_LINE]: {
-    announce: true,
     msgId: 'next_line',
     category: CommandCategory.NAVIGATION,
   },
   [Command.PREVIOUS_SENTENCE]: {
-    announce: true,
     msgId: 'previous_sentence',
     category: CommandCategory.NAVIGATION,
   },
   [Command.NEXT_SENTENCE]: {
-    announce: true,
     msgId: 'next_sentence',
     category: CommandCategory.NAVIGATION,
   },
   [Command.PREVIOUS_OBJECT]: {
-    announce: true,
     msgId: 'previous_object',
     category: CommandCategory.NAVIGATION,
   },
   [Command.NEXT_OBJECT]: {
-    announce: true,
     msgId: 'next_object',
     category: CommandCategory.NAVIGATION,
   },
   [Command.PREVIOUS_GROUP]: {
-    announce: true,
     msgId: 'previous_group',
     category: CommandCategory.NAVIGATION,
   },
   [Command.NEXT_GROUP]: {
-    announce: true,
     msgId: 'next_group',
     category: CommandCategory.NAVIGATION,
   },
   [Command.PREVIOUS_SIMILAR_ITEM]: {
-    announce: true,
     msgId: 'previous_similar_item',
     category: CommandCategory.NAVIGATION,
   },
   [Command.NEXT_SIMILAR_ITEM]: {
-    announce: true,
     msgId: 'next_similar_item',
     category: CommandCategory.NAVIGATION,
   },
   [Command.PREVIOUS_INVALID_ITEM]: {
-    announce: true,
     msgId: 'previous_invalid_item',
     category: CommandCategory.NAVIGATION,
   },
   [Command.NEXT_INVALID_ITEM]: {
-    announce: true,
     msgId: 'next_invalid_item',
     category: CommandCategory.NAVIGATION,
   },
 
   [Command.JUMP_TO_TOP]: {
-    announce: true,
     msgId: 'jump_to_top',
     category: CommandCategory.NAVIGATION,
   },
   [Command.JUMP_TO_BOTTOM]: {
-    announce: true,
     msgId: 'jump_to_bottom',
     category: CommandCategory.NAVIGATION,
   },
   // Intentionally uncategorized.
-  [Command.MOVE_TO_START_OF_LINE]: {announce: true},
-  [Command.MOVE_TO_END_OF_LINE]: {announce: true},
+  [Command.MOVE_TO_START_OF_LINE]: {},
+  [Command.MOVE_TO_END_OF_LINE]: {},
 
   [Command.JUMP_TO_DETAILS]: {
-    announce: false,
     msgId: 'jump_to_details',
     category: CommandCategory.NAVIGATION,
   },
 
   [Command.READ_FROM_HERE]: {
-    announce: false,
     msgId: 'read_from_here',
     category: CommandCategory.NAVIGATION,
   },
 
   [Command.FORCE_CLICK_ON_CURRENT_ITEM]: {
-    announce: true,
     msgId: 'force_click_on_current_item',
     category: CommandCategory.ACTIONS,
   },
   [Command.FORCE_LONG_CLICK_ON_CURRENT_ITEM]: {
-    announce: true,
     msgId: 'force_long_click_on_current_item',
   },
-  [Command.FORCE_DOUBLE_CLICK_ON_CURRENT_ITEM]: {announce: true},
+  [Command.FORCE_DOUBLE_CLICK_ON_CURRENT_ITEM]: {},
 
   [Command.READ_LINK_URL]: {
-    announce: false,
     msgId: 'read_link_url',
     category: CommandCategory.INFORMATION,
   },
   [Command.READ_CURRENT_TITLE]: {
-    announce: false,
     msgId: 'read_current_title',
     category: CommandCategory.INFORMATION,
   },
   [Command.READ_CURRENT_URL]: {
-    announce: false,
     msgId: 'read_current_url',
     category: CommandCategory.INFORMATION,
   },
 
   [Command.FULLY_DESCRIBE]: {
-    announce: false,
     msgId: 'fully_describe',
     category: CommandCategory.INFORMATION,
   },
   [Command.SPEAK_TIME_AND_DATE]: {
-    announce: false,
     msgId: 'speak_time_and_date',
     category: CommandCategory.INFORMATION,
   },
   [Command.TOGGLE_SELECTION]: {
-    announce: true,
     msgId: 'toggle_selection',
     category: CommandCategory.ACTIONS,
   },
 
   [Command.TOGGLE_SEARCH_WIDGET]: {
-    announce: false,
     msgId: 'toggle_search_widget',
     category: CommandCategory.INFORMATION,
   },
 
   [Command.TOGGLE_SCREEN]: {
-    announce: false,
     msgId: 'toggle_screen',
     category: CommandCategory.MODIFIER_KEYS,
   },
@@ -606,84 +551,69 @@
       {msgId: 'toggle_braille_table', category: CommandCategory.HELP_COMMANDS},
 
   [Command.TOGGLE_KEYBOARD_HELP]: {
-    announce: false,
     msgId: 'show_panel_menu',
     category: CommandCategory.HELP_COMMANDS,
   },
   [Command.SHOW_PANEL_MENU_MOST_RECENT]: {
-    announce: false,
     msgId: 'show_panel_menu',
     category: CommandCategory.HELP_COMMANDS,
   },
   [Command.HELP]: {
-    announce: false,
     msgId: 'help',
     category: CommandCategory.HELP_COMMANDS,
   },
   [Command.CONTEXT_MENU]: {
-    announce: false,
     msgId: 'show_context_menu',
     category: CommandCategory.INFORMATION,
   },
 
   [Command.SHOW_OPTIONS_PAGE]: {
-    announce: false,
     msgId: 'show_options_page',
     denySignedOut: true,
     category: CommandCategory.HELP_COMMANDS,
   },
   [Command.SHOW_LOG_PAGE]: {
-    announce: false,
     msgId: 'show_log_page',
     denySignedOut: true,
     category: CommandCategory.HELP_COMMANDS,
   },
   [Command.SHOW_LEARN_MODE_PAGE]: {
-    announce: false,
     msgId: 'show_kb_explorer_page',
     denySignedOut: true,
     category: CommandCategory.HELP_COMMANDS,
   },
   [Command.SHOW_TTS_SETTINGS]: {
-    announce: false,
     msgId: 'show_tts_settings',
     category: CommandCategory.HELP_COMMANDS,
     denySignedOut: true,
   },
   [Command.TOGGLE_BRAILLE_CAPTIONS]: {
-    announce: false,
     msgId: 'braille_captions',
     category: CommandCategory.HELP_COMMANDS,
   },
   [Command.REPORT_ISSUE]: {
-    announce: false,
     denySignedOut: true,
     msgId: 'panel_menu_item_report_issue',
     category: CommandCategory.HELP_COMMANDS,
   },
 
   [Command.SHOW_FORMS_LIST]: {
-    announce: false,
     msgId: 'show_forms_list',
     category: CommandCategory.OVERVIEW,
   },
   [Command.SHOW_HEADINGS_LIST]: {
-    announce: false,
     msgId: 'show_headings_list',
     category: CommandCategory.OVERVIEW,
   },
   [Command.SHOW_LANDMARKS_LIST]: {
-    announce: false,
     msgId: 'show_landmarks_list',
     category: CommandCategory.OVERVIEW,
   },
   [Command.SHOW_LINKS_LIST]: {
-    announce: false,
     msgId: 'show_links_list',
     category: CommandCategory.OVERVIEW,
   },
   [Command.SHOW_TABLES_LIST]: {
-    announce: false,
     msgId: 'show_tables_list',
     category: CommandCategory.OVERVIEW,
   },
@@ -881,100 +811,82 @@
 
   // Table Actions.
   [Command.ANNOUNCE_HEADERS]: {
-    announce: false,
     msgId: 'announce_headers',
     category: CommandCategory.TABLES,
   },
   [Command.SPEAK_TABLE_LOCATION]: {
-    announce: false,
     msgId: 'speak_table_location',
     category: CommandCategory.TABLES,
   },
   [Command.GO_TO_FIRST_CELL]: {
-    announce: true,
     msgId: 'skip_to_beginning',
     category: CommandCategory.TABLES,
   },
   [Command.GO_TO_LAST_CELL]:
-      {announce: true, msgId: 'skip_to_end', category: CommandCategory.TABLES},
+      {msgId: 'skip_to_end', category: CommandCategory.TABLES},
   [Command.GO_TO_ROW_FIRST_CELL]: {
-    announce: true,
     msgId: 'skip_to_row_beginning',
     category: CommandCategory.TABLES,
   },
   [Command.GO_TO_ROW_LAST_CELL]: {
-    announce: true,
     msgId: 'skip_to_row_end',
     category: CommandCategory.TABLES,
   },
   [Command.GO_TO_COL_FIRST_CELL]: {
-    announce: true,
     msgId: 'skip_to_col_beginning',
     category: CommandCategory.TABLES,
   },
   [Command.GO_TO_COL_LAST_CELL]: {
-    announce: true,
     msgId: 'skip_to_col_end',
     category: CommandCategory.TABLES,
   },
   [Command.PREVIOUS_ROW]: {
-    announce: true,
     msgId: 'skip_to_prev_row',
     category: CommandCategory.TABLES,
   },
   [Command.PREVIOUS_COL]: {
-    announce: true,
     msgId: 'skip_to_prev_col',
     category: CommandCategory.TABLES,
   },
   [Command.NEXT_ROW]: {
-    announce: true,
     msgId: 'skip_to_next_row',
     category: CommandCategory.TABLES,
   },
   [Command.NEXT_COL]: {
-    announce: true,
     msgId: 'skip_to_next_col',
     category: CommandCategory.TABLES,
   },
 
   // Generic Actions.
   [Command.ENTER_SHIFTER]: {
-    announce: true,
     msgId: 'enter_content',
     category: CommandCategory.NAVIGATION,
   },
   [Command.EXIT_SHIFTER]: {
-    announce: true,
     msgId: 'exit_content',
     category: CommandCategory.NAVIGATION,
   },
-  [Command.EXIT_SHIFTER_CONTENT]: {announce: true},
+  [Command.EXIT_SHIFTER_CONTENT]: {},
 
   [Command.OPEN_LONG_DESC]: {
-    announce: false,
     msgId: 'open_long_desc',
     category: CommandCategory.INFORMATION,
   },
 
   [Command.PAUSE_ALL_MEDIA]: {
-    announce: false,
     msgId: 'pause_all_media',
     category: CommandCategory.INFORMATION,
   },
 
   [Command.ANNOUNCE_BATTERY_DESCRIPTION]: {
-    announce: true,
     msgId: 'announce_battery_description',
     category: CommandCategory.INFORMATION,
   },
   [Command.ANNOUNCE_RICH_TEXT_DESCRIPTION]: {
-    announce: true,
     msgId: 'announce_rich_text_description',
     category: CommandCategory.INFORMATION,
   },
   [Command.READ_PHONETIC_PRONUNCIATION]: {
-    announce: true,
     msgId: 'read_phonetic_pronunciation',
     category: CommandCategory.INFORMATION,
   },
@@ -985,65 +897,55 @@
 
   // Math specific commands.
   [Command.TOGGLE_SEMANTICS]: {
-    announce: false,
     msgId: 'toggle_semantics',
     category: CommandCategory.INFORMATION,
   },
 
   // Braille specific commands.
   [Command.ROUTING]: {
-    announce: false,
     msgId: 'braille_routing',
     category: CommandCategory.BRAILLE,
   },
   [Command.PAN_LEFT]: {
-    announce: true,
     msgId: 'braille_pan_left',
     category: CommandCategory.BRAILLE,
   },
   [Command.PAN_RIGHT]: {
-    announce: true,
     msgId: 'braille_pan_right',
     category: CommandCategory.BRAILLE,
   },
   [Command.LINE_UP]: {
-    announce: true,
     msgId: 'braille_line_up',
     category: CommandCategory.BRAILLE,
   },
   [Command.LINE_DOWN]: {
-    announce: true,
     msgId: 'braille_line_down',
     category: CommandCategory.BRAILLE,
   },
   [Command.TOP]: {
-    announce: true,
     msgId: 'braille_top',
     category: CommandCategory.BRAILLE,
   },
   [Command.BOTTOM]: {
-    announce: true,
     msgId: 'braille_bottom',
     category: CommandCategory.BRAILLE,
   },
   [Command.VIEW_GRAPHIC_AS_BRAILLE]: {
-    announce: true,
     msgId: 'view_graphic_as_braille',
     category: CommandCategory.BRAILLE,
   },
 
   // Developer commands.
   [Command.ENABLE_CONSOLE_TTS]: {
-    announce: false,
     msgId: 'enable_tts_log',
     category: CommandCategory.DEVELOPER,
   },
 
-  [Command.START_HISTORY_RECORDING]: {announce: false},
-  [Command.STOP_HISTORY_RECORDING]: {announce: false},
-  [Command.AUTORUNNER]: {announce: false},
+  [Command.START_HISTORY_RECORDING]: {},
+  [Command.STOP_HISTORY_RECORDING]: {},
+  [Command.AUTORUNNER]: {},
 
-  [Command.DEBUG]: {announce: false},
+  [Command.DEBUG]: {},
 
-  [Command.NOP]: {announce: false},
+  [Command.NOP]: {},
 };
diff --git a/chrome/browser/resources/chromeos/accessibility/common/testing/common.js b/chrome/browser/resources/chromeos/accessibility/common/testing/common.js
index 61441aed..7badfa8 100644
--- a/chrome/browser/resources/chromeos/accessibility/common/testing/common.js
+++ b/chrome/browser/resources/chromeos/accessibility/common/testing/common.js
@@ -12,11 +12,12 @@
  * @param {string} testFixture Fixture name.
  * @param {string} testName Test name.
  * @param {function} testFunction The test impl.
+ * @param {string=} preamble C++ code to execute before the test.
  */
-function AX_TEST_F(testFixture, testName, testFunction) {
+function AX_TEST_F(testFixture, testName, testFunction, preamble) {
   TEST_F(testFixture, testName, function() {
     this.newCallback(testFunction)();
-  });
+  }, preamble);
 }
 
 // var is used to export this function alias outside of the current context
diff --git a/chrome/browser/resources/new_tab_page/modules/history_clusters/module.html b/chrome/browser/resources/new_tab_page/modules/history_clusters/module.html
index aa23bda..2934cf1e 100644
--- a/chrome/browser/resources/new_tab_page/modules/history_clusters/module.html
+++ b/chrome/browser/resources/new_tab_page/modules/history_clusters/module.html
@@ -55,12 +55,11 @@
 </style>
 <div id="content">
   <ntp-module-header
-    disable-text="[[i18n('modulesDisableButtonText', title_)]]"
-    show-dismiss-button
-    dismiss-text="[[i18n('modulesDismissButtonText', title_)]]"
-    on-disable-button-click="onDisableButtonClick_"
-    show-info-button on-info-button-click="onInfoButtonClick_"
-    icon-src="chrome://resources/images/icon_journeys.svg">
+      disable-text="[[i18n('modulesDisableButtonText', title_)]]"
+      dismiss-text="[[i18n('modulesDismissButtonText', title_)]]"
+      show-info-button show-dismiss-button
+      on-dismiss-button-click="onDismissButtonClick_"
+      icon-src="chrome://resources/images/icon_journeys.svg">
     [[i18n('modulesJourneysResumeJourney', title_)]]
     <button id="showAllButton" type="button" on-click="onShowAllClick_"
             slot="title-actions">
diff --git a/chrome/browser/resources/new_tab_page/modules/history_clusters/module.ts b/chrome/browser/resources/new_tab_page/modules/history_clusters/module.ts
index 2c2128947..f7550ed0d 100644
--- a/chrome/browser/resources/new_tab_page/modules/history_clusters/module.ts
+++ b/chrome/browser/resources/new_tab_page/modules/history_clusters/module.ts
@@ -9,7 +9,7 @@
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {Cluster, URLVisit} from '../../history_cluster_types.mojom-webui.js';
-import {I18nMixin} from '../../i18n_setup.js';
+import {I18nMixin, loadTimeData} from '../../i18n_setup.js';
 import {ModuleDescriptor} from '../module_descriptor.js';
 
 import {HistoryClustersProxyImpl} from './history_clusters_proxy.js';
@@ -74,6 +74,19 @@
     return type === this.layoutType;
   }
 
+  private onDismissButtonClick_() {
+    HistoryClustersProxyImpl.getInstance().handler.dismissCluster(
+        [this.searchResultPage, ...this.cluster.visits]);
+    this.dispatchEvent(new CustomEvent('dismiss-module', {
+      bubbles: true,
+      composed: true,
+      detail: {
+        message:
+            loadTimeData.getStringF('dismissModuleToastMessage', this.title_),
+      },
+    }));
+  }
+
   private onShowAllClick_() {
     HistoryClustersProxyImpl.getInstance().handler.showJourneysSidePanel(
         this.cluster.label || '');
diff --git a/chrome/browser/resources/new_tab_page/modules/modules.html b/chrome/browser/resources/new_tab_page/modules/modules.html
index a6b95b05..cbe4b08 100644
--- a/chrome/browser/resources/new_tab_page/modules/modules.html
+++ b/chrome/browser/resources/new_tab_page/modules/modules.html
@@ -136,11 +136,13 @@
 </div>
 <cr-toast id="removeModuleToast" duration="10000">
   <div id="removeModuleToastMessage">[[removedModuleData_.message]]</div>
-  <cr-button id="undoRemoveModuleButton"
-      aria-label="$i18n{undoDescription}"
-      on-click="onUndoRemoveModuleButtonClick_">
-    $i18n{undo}
-  </cr-button>
+  <template is="dom-if" if="[[removedModuleData_.undo]]">
+    <cr-button id="undoRemoveModuleButton"
+        aria-label="$i18n{undoDescription}"
+        on-click="onUndoRemoveModuleButtonClick_">
+      $i18n{undo}
+    </cr-button>
+  </template>
 </cr-toast>
 <cr-toast id="removeModuleFreToast" duration="10000">
   <div id="removeModuleFreToastMessage">
diff --git a/chrome/browser/resources/new_tab_page/modules/modules.ts b/chrome/browser/resources/new_tab_page/modules/modules.ts
index 49ac3a6d..4d2b758 100644
--- a/chrome/browser/resources/new_tab_page/modules/modules.ts
+++ b/chrome/browser/resources/new_tab_page/modules/modules.ts
@@ -21,7 +21,7 @@
 import {getTemplate} from './modules.html.js';
 
 export type DismissModuleEvent =
-    CustomEvent<{message: string, restoreCallback: () => void}>;
+    CustomEvent<{message: string, restoreCallback?: () => void}>;
 export type DisableModuleEvent = DismissModuleEvent;
 
 declare global {
@@ -151,7 +151,7 @@
   private modulesRedesignedLayoutEnabled_: boolean;
   private modulesShownToUser: boolean;
   private modulesVisibilityDetermined_: boolean;
-  private removedModuleData_: {message: string, undo: () => void}|null;
+  private removedModuleData_: {message: string, undo?: () => void}|null;
 
   private setDisabledModulesListenerId_: number|null = null;
   private setModulesFreVisibilityListenerId_: number|null = null;
@@ -363,11 +363,14 @@
     const restoreCallback = e.detail.restoreCallback;
     this.removedModuleData_ = {
       message: e.detail.message,
-      undo: () => {
-        this.splice('dismissedModules_', this.dismissedModules_.indexOf(id), 1);
-        restoreCallback();
-        NewTabPageProxy.getInstance().handler.onRestoreModule(id);
-      },
+      undo: restoreCallback ?
+          () => {
+            this.splice(
+                'dismissedModules_', this.dismissedModules_.indexOf(id), 1);
+            restoreCallback();
+            NewTabPageProxy.getInstance().handler.onRestoreModule(id);
+          } :
+          undefined,
     };
     if (!this.dismissedModules_.includes(id)) {
       this.push('dismissedModules_', id);
@@ -419,7 +422,7 @@
     }
 
     // Restore the module.
-    this.removedModuleData_.undo();
+    this.removedModuleData_.undo!();
 
     // Notify the user.
     this.$.removeModuleToast.hide();
diff --git a/chrome/browser/resources/settings/chromeos/internet_page/esim_install_error_dialog.html b/chrome/browser/resources/settings/chromeos/internet_page/esim_install_error_dialog.html
index e94fd47..cd081f4f 100644
--- a/chrome/browser/resources/settings/chromeos/internet_page/esim_install_error_dialog.html
+++ b/chrome/browser/resources/settings/chromeos/internet_page/esim_install_error_dialog.html
@@ -1,4 +1,4 @@
-<style include="iron-flex iron-positioning">
+<style include="settings-shared iron-flex iron-positioning">
   :host {
     --cr-dialog-width: 372px;
   }
diff --git a/chrome/browser/resources/settings/chromeos/internet_page/esim_install_error_dialog.ts b/chrome/browser/resources/settings/chromeos/internet_page/esim_install_error_dialog.ts
index 676ddb5..9215022b 100644
--- a/chrome/browser/resources/settings/chromeos/internet_page/esim_install_error_dialog.ts
+++ b/chrome/browser/resources/settings/chromeos/internet_page/esim_install_error_dialog.ts
@@ -10,6 +10,7 @@
 import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
 import 'chrome://resources/cr_elements/cr_input/cr_input.js';
 import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
+import '../../settings_shared.css.js';
 
 import {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
 import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
diff --git a/chrome/browser/resources/settings/chromeos/internet_page/esim_remove_profile_dialog.html b/chrome/browser/resources/settings/chromeos/internet_page/esim_remove_profile_dialog.html
index 4f9e79b..ea4d973 100644
--- a/chrome/browser/resources/settings/chromeos/internet_page/esim_remove_profile_dialog.html
+++ b/chrome/browser/resources/settings/chromeos/internet_page/esim_remove_profile_dialog.html
@@ -1,4 +1,4 @@
-<style>
+<style include="settings-shared">
   :host {
     --cr-dialog-width: 416px;
   }
diff --git a/chrome/browser/resources/settings/chromeos/internet_page/esim_remove_profile_dialog.ts b/chrome/browser/resources/settings/chromeos/internet_page/esim_remove_profile_dialog.ts
index 43258b1..dec3ec1 100644
--- a/chrome/browser/resources/settings/chromeos/internet_page/esim_remove_profile_dialog.ts
+++ b/chrome/browser/resources/settings/chromeos/internet_page/esim_remove_profile_dialog.ts
@@ -10,6 +10,7 @@
 import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
 import 'chrome://resources/cr_elements/cr_button/cr_button.js';
 import 'chrome://resources/cr_elements/cr_input/cr_input.js';
+import '../../settings_shared.css.js';
 
 import {getESimProfile} from 'chrome://resources/ash/common/cellular_setup/esim_manager_utils.js';
 import {OncMojo} from 'chrome://resources/ash/common/network/onc_mojo.js';
diff --git a/chrome/browser/resources/settings/chromeos/internet_page/esim_rename_dialog.html b/chrome/browser/resources/settings/chromeos/internet_page/esim_rename_dialog.html
index c27c5f62..132b42a 100644
--- a/chrome/browser/resources/settings/chromeos/internet_page/esim_rename_dialog.html
+++ b/chrome/browser/resources/settings/chromeos/internet_page/esim_rename_dialog.html
@@ -1,4 +1,4 @@
-<style include="iron-positioning">
+<style include="settings-shared iron-positioning">
   :host {
     --cr-dialog-width: 416px;
     --cr-dialog-title-slot-padding-bottom: 10px;
diff --git a/chrome/browser/resources/settings/chromeos/internet_page/esim_rename_dialog.ts b/chrome/browser/resources/settings/chromeos/internet_page/esim_rename_dialog.ts
index 8158f38..faad5bb8 100644
--- a/chrome/browser/resources/settings/chromeos/internet_page/esim_rename_dialog.ts
+++ b/chrome/browser/resources/settings/chromeos/internet_page/esim_rename_dialog.ts
@@ -10,6 +10,7 @@
 import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
 import 'chrome://resources/cr_elements/cr_input/cr_input.js';
 import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
+import '../../settings_shared.css.js';
 
 import {getESimProfile} from 'chrome://resources/ash/common/cellular_setup/esim_manager_utils.js';
 import {OncMojo} from 'chrome://resources/ash/common/network/onc_mojo.js';
diff --git a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_contact_visibility_dialog.html b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_contact_visibility_dialog.html
index 714a754..cd5aa34b 100644
--- a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_contact_visibility_dialog.html
+++ b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_contact_visibility_dialog.html
@@ -1,4 +1,4 @@
-<style>
+<style include="settings-shared">
   .title {
     color: var(--cr-primary-text-color);
     font-family: 'Google Sans';
diff --git a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_contact_visibility_dialog.ts b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_contact_visibility_dialog.ts
index e4fe3e1..da311644 100644
--- a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_contact_visibility_dialog.ts
+++ b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_contact_visibility_dialog.ts
@@ -13,6 +13,7 @@
 import '/shared/nearby_contact_visibility.js';
 import '/shared/nearby_onboarding_page.js';
 import '/shared/nearby_visibility_page.js';
+import '../../settings_shared.css.js';
 
 import {NearbyContactVisibilityElement} from '/shared/nearby_contact_visibility.js';
 import {NearbySettings} from '/shared/nearby_share_settings_mixin.js';
diff --git a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_data_usage_dialog.html b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_data_usage_dialog.html
index 05f0f8a..00d4748 100644
--- a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_data_usage_dialog.html
+++ b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_data_usage_dialog.html
@@ -1,4 +1,4 @@
-<style>
+<style include="settings-shared">
   :host {
     --cr-dialog-width: 340px;
   }
diff --git a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_data_usage_dialog.ts b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_data_usage_dialog.ts
index 60c8377..8480697c 100644
--- a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_data_usage_dialog.ts
+++ b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_data_usage_dialog.ts
@@ -12,6 +12,7 @@
 import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
 import 'chrome://resources/cr_elements/cr_radio_button/cr_radio_button.js';
 import 'chrome://resources/cr_elements/cr_radio_group/cr_radio_group.js';
+import '../../settings_shared.css.js';
 
 import {getNearbyShareSettings} from '/shared/nearby_share_settings.js';
 import {NearbySettings} from '/shared/nearby_share_settings_mixin.js';
diff --git a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_device_name_dialog.html b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_device_name_dialog.html
index 7dce630..3871f66 100644
--- a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_device_name_dialog.html
+++ b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_device_name_dialog.html
@@ -1,4 +1,4 @@
-<style>
+<style include="settings-shared">
   :host {
     --cr-dialog-width: 340px;
   }
diff --git a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_device_name_dialog.ts b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_device_name_dialog.ts
index b08a014..81226a0 100644
--- a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_device_name_dialog.ts
+++ b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_device_name_dialog.ts
@@ -11,6 +11,7 @@
 import 'chrome://resources/cr_elements/cr_button/cr_button.js';
 import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
 import 'chrome://resources/cr_elements/cr_input/cr_input.js';
+import '../../settings_shared.css.js';
 
 import {getNearbyShareSettings} from '/shared/nearby_share_settings.js';
 import {NearbySettings} from '/shared/nearby_share_settings_mixin.js';
diff --git a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_receive_dialog.html b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_receive_dialog.html
index 593bebe..04d9ff4a 100644
--- a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_receive_dialog.html
+++ b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_receive_dialog.html
@@ -1,4 +1,4 @@
-<style>
+<style include="settings-shared">
   cr-dialog::part(dialog) {
     height: 420px;
     width: 512px;
diff --git a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_receive_dialog.ts b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_receive_dialog.ts
index d3bb1a0..b4d3ecf 100644
--- a/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_receive_dialog.ts
+++ b/chrome/browser/resources/settings/chromeos/nearby_share_page/nearby_share_receive_dialog.ts
@@ -23,6 +23,7 @@
 import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
 import 'chrome://resources/cr_elements/cr_view_manager/cr_view_manager.js';
 import '../../prefs/prefs.js';
+import '../../settings_shared.css.js';
 import '/shared/nearby_onboarding_one_page.js';
 import '/shared/nearby_onboarding_page.js';
 import '/shared/nearby_visibility_page.js';
diff --git a/chrome/browser/resources/settings/chromeos/os_a11y_page/change_dictation_locale_dialog.ts b/chrome/browser/resources/settings/chromeos/os_a11y_page/change_dictation_locale_dialog.ts
index 28a0db19..8454c67e 100644
--- a/chrome/browser/resources/settings/chromeos/os_a11y_page/change_dictation_locale_dialog.ts
+++ b/chrome/browser/resources/settings/chromeos/os_a11y_page/change_dictation_locale_dialog.ts
@@ -10,11 +10,11 @@
 import 'chrome://resources/cr_elements/cr_button/cr_button.js';
 import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
 import 'chrome://resources/cr_elements/cr_search_field/cr_search_field.js';
-import 'chrome://resources/cr_elements/cr_shared_style.css.js';
 import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
 import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
 import 'chrome://resources/polymer/v3_0/paper-ripple/paper-ripple.js';
 import '../../settings_shared.css.js';
+import '../os_languages_page/shared_style.css.js';
 
 import {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
 import {CrSearchFieldElement} from 'chrome://resources/cr_elements/cr_search_field/cr_search_field.js';
diff --git a/chrome/browser/resources/settings/chromeos/os_apps_page/app_management_page/supported_links_overlapping_apps_dialog.html b/chrome/browser/resources/settings/chromeos/os_apps_page/app_management_page/supported_links_overlapping_apps_dialog.html
index 35f00862..093094b 100644
--- a/chrome/browser/resources/settings/chromeos/os_apps_page/app_management_page/supported_links_overlapping_apps_dialog.html
+++ b/chrome/browser/resources/settings/chromeos/os_apps_page/app_management_page/supported_links_overlapping_apps_dialog.html
@@ -1,3 +1,4 @@
+<style include="settings-shared"></style>
 <cr-dialog show-on-attach id="dialog" close-text="close">
   <div slot="title">$i18n{appManagementIntentOverlapDialogTitle}</div>
   <div slot="body">[[getBodyText_(apps_)]]</div>
diff --git a/chrome/browser/resources/settings/chromeos/os_apps_page/app_management_page/supported_links_overlapping_apps_dialog.ts b/chrome/browser/resources/settings/chromeos/os_apps_page/app_management_page/supported_links_overlapping_apps_dialog.ts
index 3a5bf06..59277e0e 100644
--- a/chrome/browser/resources/settings/chromeos/os_apps_page/app_management_page/supported_links_overlapping_apps_dialog.ts
+++ b/chrome/browser/resources/settings/chromeos/os_apps_page/app_management_page/supported_links_overlapping_apps_dialog.ts
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
+import '../../../settings_shared.css.js';
 
 import {App} from 'chrome://resources/cr_components/app_management/app_management.mojom-webui.js';
 import {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
diff --git a/chrome/browser/resources/settings/chromeos/os_bluetooth_page/os_bluetooth_pairing_dialog.html b/chrome/browser/resources/settings/chromeos/os_bluetooth_page/os_bluetooth_pairing_dialog.html
index d2a8af5..1840f0f 100644
--- a/chrome/browser/resources/settings/chromeos/os_bluetooth_page/os_bluetooth_pairing_dialog.html
+++ b/chrome/browser/resources/settings/chromeos/os_bluetooth_page/os_bluetooth_pairing_dialog.html
@@ -1,4 +1,4 @@
-<style>
+<style include="settings-shared">
   #dialog {
     --cr-dialog-top-container-min-height: 0;
   }
@@ -8,4 +8,4 @@
     <bluetooth-pairing-ui on-finished="closeDialog_">
     </bluetooth-pairing-ui>
   </div>
-</cr-dialog>
\ No newline at end of file
+</cr-dialog>
diff --git a/chrome/browser/resources/settings/chromeos/os_bluetooth_page/os_bluetooth_pairing_dialog.ts b/chrome/browser/resources/settings/chromeos/os_bluetooth_page/os_bluetooth_pairing_dialog.ts
index 8ac8bf6..df3deb6 100644
--- a/chrome/browser/resources/settings/chromeos/os_bluetooth_page/os_bluetooth_pairing_dialog.ts
+++ b/chrome/browser/resources/settings/chromeos/os_bluetooth_page/os_bluetooth_pairing_dialog.ts
@@ -8,6 +8,7 @@
  */
 import 'chrome://resources/ash/common/bluetooth/bluetooth_pairing_ui.js';
 import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
+import '../../settings_shared.css.js';
 
 import {BluetoothUiSurface, recordBluetoothUiSurfaceMetrics} from 'chrome://resources/ash/common/bluetooth/bluetooth_metrics_utils.js';
 import {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
diff --git a/chrome/browser/resources/side_panel/read_anything/app.html b/chrome/browser/resources/side_panel/read_anything/app.html
index 5dc4eff..9852629a 100644
--- a/chrome/browser/resources/side_panel/read_anything/app.html
+++ b/chrome/browser/resources/side_panel/read_anything/app.html
@@ -25,9 +25,9 @@
 <div id="container" hidden="[[!hasContent_]]"></div>
 <div id="empty-state-container" hidden="[[hasContent_]]">
   <sp-empty-state
-      image-path="./images/empty_state.svg"
-      dark-image-path="./images/empty_state.svg"
-      heading="$i18n{emptyStateHeader}"
-      body="$i18n{emptyStateSubheader}">
+      image-path="[[emptyStateImagePath_]]"
+      dark-image-path="[[emptyStateDarkImagePath_]]"
+      heading="[[emptyStateHeading_]]"
+      body="[[emptyStateSubheading_]]">
   </sp-empty-state>
 </div>
diff --git a/chrome/browser/resources/side_panel/read_anything/app.ts b/chrome/browser/resources/side_panel/read_anything/app.ts
index 8afe9fd..a4b7452 100644
--- a/chrome/browser/resources/side_panel/read_anything/app.ts
+++ b/chrome/browser/resources/side_panel/read_anything/app.ts
@@ -9,6 +9,7 @@
 import {WebUiListenerMixin} from 'chrome://resources/cr_elements/web_ui_listener_mixin.js';
 import {assert} from 'chrome://resources/js/assert_ts.js';
 import {rgbToSkColor, skColorToRgba} from 'chrome://resources/js/color_utils.js';
+import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
 import {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
@@ -75,6 +76,12 @@
     assert(readAnythingApp);
     readAnythingApp.updateTheme();
   };
+
+  chrome.readAnything.showLoading = () => {
+    const readAnythingApp = document.querySelector('read-anything-app');
+    assert(readAnythingApp);
+    readAnythingApp.showLoading();
+  };
 }
 
 export class ReadAnythingElement extends ReadAnythingElementBase {
@@ -107,6 +114,10 @@
   private domNodeToAxNodeIdMap_: TwoWayMap = new TwoWayMap();
 
   private hasContent_: boolean;
+  private emptyStateImagePath_: string;
+  private emptyStateDarkImagePath_: string;
+  private emptyStateHeading_: string;
+  private emptyStateSubheading_: string;
 
   override connectedCallback() {
     super.connectedCallback();
@@ -114,6 +125,8 @@
       chrome.readAnything.onConnected();
     }
 
+    this.showLoading();
+
     document.onselectionchange = () => {
       const shadowRoot = this.shadowRoot;
       assert(shadowRoot);
@@ -194,6 +207,16 @@
     return parentElement;
   }
 
+  showLoading() {
+    this.emptyStateImagePath_ = 'chrome://resources/images/throbber_small.svg';
+    this.emptyStateDarkImagePath_ =
+        'chrome://resources/images/throbber_small_dark.svg';
+    this.emptyStateHeading_ =
+        loadTimeData.getString('readAnythingLoadingMessage');
+    this.emptyStateSubheading_ = '';
+    this.hasContent_ = false;
+  }
+
   updateContent() {
     const shadowRoot = this.shadowRoot;
     assert(shadowRoot);
@@ -220,6 +243,11 @@
     // If there is no content to show, the empty state container will be shown.
     const node = this.buildSubtree_(rootId);
     if (!node.textContent) {
+      this.emptyStateImagePath_ = './images/empty_state.svg';
+      this.emptyStateDarkImagePath_ = './images/empty_state.svg';
+      this.emptyStateHeading_ = loadTimeData.getString('emptyStateHeader');
+      this.emptyStateSubheading_ =
+          loadTimeData.getString('emptyStateSubheader');
       this.hasContent_ = false;
       return;
     }
diff --git a/chrome/browser/resources/side_panel/read_anything/read_anything.d.ts b/chrome/browser/resources/side_panel/read_anything/read_anything.d.ts
index 32a2af96f0..38cd9a22 100644
--- a/chrome/browser/resources/side_panel/read_anything/read_anything.d.ts
+++ b/chrome/browser/resources/side_panel/read_anything/read_anything.d.ts
@@ -105,6 +105,9 @@
     // Implemented in read_anything/app.ts and called by native c++.
     ////////////////////////////////////////////////////////////////
 
+    // Display a loading screen to tell the user we are distilling the page.
+    function showLoading(): void;
+
     // Ping that an AXTree has been distilled for the active tab's render frame
     // and is available to consume.
     function updateContent(): void;
diff --git a/chrome/browser/resources/side_panel/read_anything/read_anything.html b/chrome/browser/resources/side_panel/read_anything/read_anything.html
index e392507..02d87e4 100644
--- a/chrome/browser/resources/side_panel/read_anything/read_anything.html
+++ b/chrome/browser/resources/side_panel/read_anything/read_anything.html
@@ -25,6 +25,5 @@
 <body>
   <read-anything-app></read-anything-app>
   <script type="module" src="app.js"></script>
-  </script>
 </body>
 </html>
diff --git a/chrome/browser/safe_browsing/chrome_client_side_detection_host_delegate_unittest.cc b/chrome/browser/safe_browsing/chrome_client_side_detection_host_delegate_unittest.cc
index 4fc6381..f877ddc 100644
--- a/chrome/browser/safe_browsing/chrome_client_side_detection_host_delegate_unittest.cc
+++ b/chrome/browser/safe_browsing/chrome_client_side_detection_host_delegate_unittest.cc
@@ -63,13 +63,12 @@
 
 TEST_F(ChromeClientSideDetectionHostDelegateTest, GetReferrerChain) {
   base::Time now = base::Time::Now();
-  base::Time one_hour_ago =
-      base::Time::FromDoubleT(now.ToDoubleT() - 60.0 * 60.0);
+  base::Time one_second_ago = base::Time::FromDoubleT(now.ToDoubleT() - 1.0);
 
   std::unique_ptr<NavigationEvent> first_navigation =
       std::make_unique<NavigationEvent>();
   first_navigation->original_request_url = GURL("http://a.com/");
-  first_navigation->last_updated = one_hour_ago;
+  first_navigation->last_updated = one_second_ago;
   first_navigation->navigation_initiation =
       ReferrerChainEntry::BROWSER_INITIATED;
   navigation_event_list()->RecordNavigationEvent(std::move(first_navigation));
@@ -101,13 +100,12 @@
 
 TEST_F(ChromeClientSideDetectionHostDelegateTest, NoNavigationObserverManager) {
   base::Time now = base::Time::Now();
-  base::Time one_hour_ago =
-      base::Time::FromDoubleT(now.ToDoubleT() - 60.0 * 60.0);
+  base::Time one_second_ago = base::Time::FromDoubleT(now.ToDoubleT() - 1.0);
 
   std::unique_ptr<NavigationEvent> first_navigation =
       std::make_unique<NavigationEvent>();
   first_navigation->original_request_url = GURL("http://a.com/");
-  first_navigation->last_updated = one_hour_ago;
+  first_navigation->last_updated = one_second_ago;
   first_navigation->navigation_initiation =
       ReferrerChainEntry::BROWSER_INITIATED;
   navigation_event_list()->RecordNavigationEvent(std::move(first_navigation));
diff --git a/chrome/browser/safe_browsing/download_protection/download_request_maker.cc b/chrome/browser/safe_browsing/download_protection/download_request_maker.cc
index 81f982f5..d585d97 100644
--- a/chrome/browser/safe_browsing/download_protection/download_request_maker.cc
+++ b/chrome/browser/safe_browsing/download_protection/download_request_maker.cc
@@ -18,6 +18,7 @@
 #include "chrome/browser/safe_browsing/chrome_user_population_helper.h"
 #include "chrome/browser/safe_browsing/download_protection/download_protection_service.h"
 #include "chrome/browser/safe_browsing/download_protection/download_protection_util.h"
+#include "components/safe_browsing/core/common/features.h"
 #include "components/safe_browsing/core/common/proto/csd.pb.h"
 #include "components/safe_browsing/core/common/utils.h"
 #include "content/public/browser/browser_context.h"
@@ -165,6 +166,10 @@
                      ->IsUnderAdvancedProtection();
 
   *request_->mutable_population() = GetUserPopulationForProfile(profile);
+  if (base::FeatureList::IsEnabled(kNestedArchives)) {
+    request_->mutable_population()->add_finch_active_groups(
+        "SafeBrowsingArchiveImprovements.Enabled");
+  }
   request_->set_request_ap_verdicts(is_under_advanced_protection);
   request_->set_locale(g_browser_process->GetApplicationLocale());
   request_->set_file_basename(target_file_path_.BaseName().AsUTF8Unsafe());
diff --git a/chrome/browser/safe_browsing/safe_browsing_navigation_observer_browsertest.cc b/chrome/browser/safe_browsing/safe_browsing_navigation_observer_browsertest.cc
index cde5de20..33fb396 100644
--- a/chrome/browser/safe_browsing/safe_browsing_navigation_observer_browsertest.cc
+++ b/chrome/browser/safe_browsing/safe_browsing_navigation_observer_browsertest.cc
@@ -549,8 +549,8 @@
       auto* nav_event =
           observer_manager_->navigation_event_list()->GetNavigationEvent(
               *nav_event_index);
-      observer_manager_->AddToReferrerChain(referrer_chain, nav_event, GURL(),
-                                            ReferrerChainEntry::EVENT_URL);
+      observer_manager_->MaybeAddToReferrerChain(
+          referrer_chain, nav_event, GURL(), ReferrerChainEntry::EVENT_URL);
     }
   }
 
diff --git a/chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/share_sheet/ChromeProvidedSharingOptionsProvider.java b/chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/share_sheet/ChromeProvidedSharingOptionsProvider.java
index 721e336..5b2fd2f 100644
--- a/chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/share_sheet/ChromeProvidedSharingOptionsProvider.java
+++ b/chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/share_sheet/ChromeProvidedSharingOptionsProvider.java
@@ -148,7 +148,7 @@
 
     private boolean hideBottomSheetContentOnTap(FirstPartyOption firstPartyOption) {
         if (USER_ACTION_SCREENSHOT_SELECTED.equals(firstPartyOption.featureNameForMetrics)
-                || USER_ACTION_WEB_STYLE_NOTES_SELECTED.equals(
+                || USER_ACTION_LONG_SCREENSHOT_SELECTED.equals(
                         firstPartyOption.featureNameForMetrics)) {
             return false;
         }
diff --git a/chrome/browser/shared_highlighting/DIR_METADATA b/chrome/browser/shared_highlighting/DIR_METADATA
deleted file mode 100644
index 3ccc9071..0000000
--- a/chrome/browser/shared_highlighting/DIR_METADATA
+++ /dev/null
@@ -1,3 +0,0 @@
-monorail {
-  component: "UI>Browser>Creation"
-}
\ No newline at end of file
diff --git a/chrome/browser/shared_highlighting/OWNERS b/chrome/browser/shared_highlighting/OWNERS
deleted file mode 100644
index 6eda829..0000000
--- a/chrome/browser/shared_highlighting/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-gayane@chromium.org
-sebsg@chromium.org
diff --git a/chrome/browser/shared_highlighting/shared_highlighting_browsertest.cc b/chrome/browser/shared_highlighting/shared_highlighting_browsertest.cc
deleted file mode 100644
index bd46439..0000000
--- a/chrome/browser/shared_highlighting/shared_highlighting_browsertest.cc
+++ /dev/null
@@ -1,391 +0,0 @@
-// Copyright 2021 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "base/memory/raw_ptr.h"
-#include "base/strings/string_util.h"
-#include "base/strings/utf_string_conversions.h"
-#include "build/build_config.h"
-#include "build/chromeos_buildflags.h"
-#include "chrome/app/chrome_command_ids.h"
-#include "chrome/browser/renderer_context_menu/link_to_text_menu_observer.h"
-#include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
-#include "chrome/browser/ui/browser.h"
-#include "chrome/browser/ui/tabs/tab_strip_model.h"
-#include "chrome/test/base/in_process_browser_test.h"
-#include "chrome/test/base/ui_test_utils.h"
-#include "components/feature_engagement/public/feature_constants.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/fenced_frame_test_util.h"
-#include "net/http/http_status_code.h"
-#include "net/test/embedded_test_server/embedded_test_server.h"
-#include "net/test/embedded_test_server/http_request.h"
-#include "net/test/embedded_test_server/http_response.h"
-#include "third_party/blink/public/mojom/link_to_text/link_to_text.mojom-test-utils.h"
-#include "third_party/blink/public/mojom/link_to_text/link_to_text.mojom.h"
-#include "ui/base/clipboard/clipboard.h"
-
-namespace shared_highlighting {
-namespace {
-
-using net::test_server::BasicHttpResponse;
-using net::test_server::HttpRequest;
-using net::test_server::HttpResponse;
-
-}  // namespace
-
-// Wait until text fragment matches are received.
-class GetMatchesWaiter {
- public:
-  GetMatchesWaiter() = default;
-  ~GetMatchesWaiter() = default;
-
-  void Wait() { run_loop_.Run(); }
-
-  void OnMatchesRecieved(const std::vector<std::string>& matches) {
-    matches_ = matches;
-    run_loop_.Quit();
-  }
-
-  std::vector<std::string> GetMatches() { return matches_; }
-
- private:
-  std::vector<std::string> matches_;
-  base::RunLoop run_loop_;
-};
-
-class SharedHighlightingBrowserTest : public InProcessBrowserTest {
- public:
-  SharedHighlightingBrowserTest(const SharedHighlightingBrowserTest&) = delete;
-  SharedHighlightingBrowserTest& operator=(
-      const SharedHighlightingBrowserTest&) = delete;
-
- protected:
-  SharedHighlightingBrowserTest() = default;
-  ~SharedHighlightingBrowserTest() override = default;
-
-  // InProcessBrowserTest
-  void SetUpCommandLine(base::CommandLine* command_line) override;
-
-  // BrowserTestBase
-  void SetUpOnMainThread() override;
-
-  bool SelectTextInCurrentTab();
-  int GetSelectionMidX();
-  int GetSelectionMidY();
-  std::u16string GetClipboardText();
-  void OpenInNewTab(GURL url);
-  std::string GetFirstHighlightedText();
-
-  content::WebContents* GetCurrentTab();
-
- private:
-  std::unique_ptr<HttpResponse> HandleRequest(const HttpRequest& request);
-
-  std::string html_content_ = R"HTML(
-    <!DOCTYPE html>
-    <style>
-      body {
-        height: 2200px;
-      }
-      #first {
-        position: absolute;
-        top: 1000px;
-      }
-      #second {
-        position: absolute;
-        top: 2000px;
-      }
-    </style>
-    <p id="selected">This is a test page</p>
-    <p id="second">With some more text</p>
-  )HTML";
-};
-
-void SharedHighlightingBrowserTest::SetUpCommandLine(
-    base::CommandLine* command_line) {}
-
-void SharedHighlightingBrowserTest::SetUpOnMainThread() {
-  InProcessBrowserTest::SetUpOnMainThread();
-
-  embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
-      &SharedHighlightingBrowserTest::HandleRequest, base::Unretained(this)));
-  ASSERT_TRUE(embedded_test_server()->Start());
-}
-
-bool SharedHighlightingBrowserTest::SelectTextInCurrentTab() {
-  return content::ExecuteScript(
-      GetCurrentTab(),
-      "var node = document.getElementById('selected');"
-      "if (document.body.createTextRange) {"
-      "  const range = document.body.createTextRange();"
-      "  range.moveToElementText(node);"
-      "  range.select();"
-      "} else if (window.getSelection) {"
-      "  const selection = window.getSelection();"
-      "  const range = document.createRange();"
-      "  range.selectNodeContents(node);"
-      "  selection.removeAllRanges();"
-      "  selection.addRange(range);"
-      "}");
-}
-
-int SharedHighlightingBrowserTest::GetSelectionMidX() {
-  int x;
-  EXPECT_TRUE(content::ExecuteScriptAndExtractInt(
-      GetCurrentTab(),
-      "var bounds = document.getElementById('selected')"
-      ".getBoundingClientRect();"
-      "domAutomationController.send("
-      "    Math.floor(bounds.left + bounds.width / 2));",
-      &x));
-  return x;
-}
-
-int SharedHighlightingBrowserTest::GetSelectionMidY() {
-  int y;
-  EXPECT_TRUE(content::ExecuteScriptAndExtractInt(
-      GetCurrentTab(),
-      "var bounds = document.getElementById('selected')"
-      ".getBoundingClientRect();"
-      "domAutomationController.send("
-      "    Math.floor(bounds.top + bounds.height / 2));",
-      &y));
-  return y;
-}
-
-std::u16string SharedHighlightingBrowserTest::GetClipboardText() {
-  ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread();
-  std::u16string result;
-  clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr,
-                      &result);
-  return result;
-}
-
-void SharedHighlightingBrowserTest::OpenInNewTab(GURL url) {
-  ui_test_utils::NavigateToURLWithDisposition(
-      browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
-      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
-}
-
-std::string SharedHighlightingBrowserTest::GetFirstHighlightedText() {
-  GetMatchesWaiter get_matches_waiter;
-  mojo::Remote<blink::mojom::TextFragmentReceiver> remote;
-  browser()
-      ->tab_strip_model()
-      ->GetActiveWebContents()
-      ->GetPrimaryMainFrame()
-      ->GetRemoteInterfaces()
-      ->GetInterface(remote.BindNewPipeAndPassReceiver());
-  remote->ExtractTextFragmentsMatches(
-      base::BindOnce(&GetMatchesWaiter::OnMatchesRecieved,
-                     base::Unretained(&get_matches_waiter)));
-  get_matches_waiter.Wait();
-
-  return get_matches_waiter.GetMatches()[0];
-}
-
-content::WebContents* SharedHighlightingBrowserTest::GetCurrentTab() {
-  return browser()->tab_strip_model()->GetActiveWebContents();
-}
-
-std::unique_ptr<HttpResponse> SharedHighlightingBrowserTest::HandleRequest(
-    const HttpRequest& request) {
-  auto response = std::make_unique<BasicHttpResponse>();
-  response->set_code(net::HTTP_OK);
-  response->set_content(html_content_);
-  response->set_content_type("text/html; charset=utf-8");
-  return std::move(response);
-}
-
-// Wait for a link to text generation completion. If successful "Copy link to
-// text" menu options will be enabled.
-class GenerationCompleteObserver {
- public:
-  GenerationCompleteObserver() {
-    LinkToTextMenuObserver::RegisterGenerationCompleteCallbackForTesting(
-        base::BindOnce(&GenerationCompleteObserver::OnGenerationComplete,
-                       base::Unretained(this)));
-  }
-  ~GenerationCompleteObserver() = default;
-
-  void Wait() { run_loop_.Run(); }
-
-  std::string GetSelector() const { return selector_; }
-
- private:
-  void OnGenerationComplete(const std::string& selector) {
-    selector_ = selector;
-    run_loop_.Quit();
-  }
-
-  std::string selector_;
-  base::RunLoop run_loop_;
-};
-
-// Wait for context menu to be shown and use the menu handler to execute menu
-// option commands.
-class ContextMenuObserver {
- public:
-  ContextMenuObserver() {
-    RenderViewContextMenu::RegisterMenuShownCallbackForTesting(base::BindOnce(
-        &ContextMenuObserver::MenuShown, base::Unretained(this)));
-  }
-
-  ~ContextMenuObserver() = default;
-
-  void WaitForMenuShown() { run_loop_.Run(); }
-
-  void ExecuteCommand(int command_to_execute) {
-    context_menu_->ExecuteCommand(command_to_execute, 0);
-  }
-
- private:
-  void MenuShown(RenderViewContextMenu* context_menu) {
-    context_menu_ = context_menu;
-    run_loop_.Quit();
-  }
-
-  raw_ptr<RenderViewContextMenu> context_menu_;
-  base::RunLoop run_loop_;
-};
-
-#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS) || \
-    BUILDFLAG(IS_LINUX)
-// Disabled because it fails for mac specific context menu:
-// TODO(crbug.com/1275253): Flakily crashes under Windows and Mac & Linux.
-// TODO(crbug.com/1276463): Flakily crashes under lacros.
-#define MAYBE_LinkGenerationTest DISABLED_LinkGenerationTest
-#else
-#define MAYBE_LinkGenerationTest LinkGenerationTest
-#endif
-IN_PROC_BROWSER_TEST_F(SharedHighlightingBrowserTest,
-                       MAYBE_LinkGenerationTest) {
-  // Load the URL.
-  auto url = embedded_test_server()->GetURL("/test.html");
-  ASSERT_NO_FATAL_FAILURE(
-      ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)));
-
-  // Select the text, with element id 'selected'.
-  ASSERT_TRUE(SelectTextInCurrentTab());
-
-  // Find the coordinates to click at to show the context menu
-  int x = GetSelectionMidX();
-  int y = GetSelectionMidY();
-
-  // Right-click on selection and wait for context menu to show up.
-  GenerationCompleteObserver generation_waiter;
-  ContextMenuObserver menu_waiter;
-  content::SimulateMouseClickAt(GetCurrentTab(), /*modifiers=*/0,
-                                blink::WebMouseEvent::Button::kRight,
-                                gfx::Point(x, y));
-  menu_waiter.WaitForMenuShown();
-
-  // Wait until link to text generation is complete and "Copy link to text" menu
-  // option is enabled to execute that option.
-  generation_waiter.Wait();
-  ASSERT_NE(std::string(), generation_waiter.GetSelector());
-  menu_waiter.ExecuteCommand(IDC_CONTENT_CONTEXT_COPYLINKTOTEXT);
-
-  // Get the generated link to text from clipboard.
-  std::u16string link_to_text_str = GetClipboardText();
-  EXPECT_TRUE(
-      base::StartsWith(link_to_text_str, base::UTF8ToUTF16(url.spec())));
-
-  // Navigate to link to text in a new tab.
-  OpenInNewTab(GURL(link_to_text_str));
-
-  // Extract and check that highlighted text matches the selected text.
-  EXPECT_EQ("This is a test page", GetFirstHighlightedText());
-}
-
-class MockTextFragmentReceiver
-    : public blink::mojom::TextFragmentReceiverInterceptorForTesting {
- public:
-  MockTextFragmentReceiver() = default;
-  ~MockTextFragmentReceiver() override = default;
-
-  MockTextFragmentReceiver(const MockTextFragmentReceiver&) = delete;
-  MockTextFragmentReceiver& operator=(const MockTextFragmentReceiver&) = delete;
-
-  TextFragmentReceiver* GetForwardingInterface() override { return this; }
-
-  void Bind(mojo::ScopedMessagePipeHandle handle) {
-    bound_ = true;
-    receiver_.Bind(mojo::PendingReceiver<blink::mojom::TextFragmentReceiver>(
-        std::move(handle)));
-  }
-
-  bool bound() { return bound_; }
-
-  MOCK_METHOD1(GetExistingSelectors, void(GetExistingSelectorsCallback));
-
- private:
-  mojo::Receiver<blink::mojom::TextFragmentReceiver> receiver_{this};
-  bool bound_ = false;
-};
-
-class SharedHighlightingFencedFrameBrowserTest
-    : public SharedHighlightingBrowserTest {
- public:
-  SharedHighlightingFencedFrameBrowserTest() {
-    feature_list_.InitAndEnableFeatures(
-        {feature_engagement::kIPHDesktopSharedHighlightingFeature});
-  }
-  ~SharedHighlightingFencedFrameBrowserTest() override = default;
-
-  void SetUpOnMainThread() override {
-    InProcessBrowserTest::SetUpOnMainThread();
-    ASSERT_TRUE(embedded_test_server()->Start());
-  }
-
-  content::WebContents* web_contents() {
-    return browser()->tab_strip_model()->GetActiveWebContents();
-  }
-
-  content::test::FencedFrameTestHelper& fenced_frame_test_helper() {
-    return fenced_frame_helper_;
-  }
-
- private:
-  content::test::FencedFrameTestHelper fenced_frame_helper_;
-  feature_engagement::test::ScopedIphFeatureList feature_list_;
-};
-
-IN_PROC_BROWSER_TEST_F(
-    SharedHighlightingFencedFrameBrowserTest,
-    EnsureTextFragmentReceiverMojoMethodIsNotCalledForFencedFrame) {
-  GURL initial_url(embedded_test_server()->GetURL("/empty.html"));
-  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), initial_url));
-
-  GURL fenced_frame_url =
-      embedded_test_server()->GetURL("/fenced_frames/title1.html");
-  content::RenderFrameHost* fenced_frame_host =
-      fenced_frame_test_helper().CreateFencedFrame(
-          web_contents()->GetPrimaryMainFrame(), fenced_frame_url);
-  ASSERT_TRUE(fenced_frame_host);
-
-  // Intercept TextFragmentReceiver Mojo connection.
-  MockTextFragmentReceiver text_fragment_receiver;
-  service_manager::InterfaceProvider::TestApi interface_overrider(
-      web_contents()->GetPrimaryMainFrame()->GetRemoteInterfaces());
-  interface_overrider.SetBinderForName(
-      blink::mojom::TextFragmentReceiver::Name_,
-      base::BindRepeating(&MockTextFragmentReceiver::Bind,
-                          base::Unretained(&text_fragment_receiver)));
-
-  // GetExistingSelectors method should not be called by the navigation on a
-  // fenced frame.
-  EXPECT_CALL(text_fragment_receiver, GetExistingSelectors(testing::_))
-      .Times(0);
-
-  GURL text_fragment_url =
-      embedded_test_server()->GetURL("/fenced_frames/title2.html#:~:text=");
-  fenced_frame_test_helper().NavigateFrameInFencedFrameTree(fenced_frame_host,
-                                                            text_fragment_url);
-  EXPECT_FALSE(text_fragment_receiver.bound());
-}
-
-}  // namespace shared_highlighting
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 1703002..a2e65c9 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -1345,6 +1345,8 @@
       "tabs/saved_tab_groups/saved_tab_group_service_factory.h",
       "tabs/saved_tab_groups/saved_tab_group_utils.cc",
       "tabs/saved_tab_groups/saved_tab_group_utils.h",
+      "tabs/saved_tab_groups/saved_tab_group_web_contents_listener.cc",
+      "tabs/saved_tab_groups/saved_tab_group_web_contents_listener.h",
       "tabs/tab.h",
       "tabs/tab_change_type.h",
       "tabs/tab_group.cc",
@@ -3520,7 +3522,10 @@
   } else {
     allow_circular_includes_from +=
         [ "//chrome/browser/ui/webui/bluetooth_internals" ]
-    deps += [ "//chrome/browser/ui/webui/bluetooth_internals" ]
+    deps += [
+      "//chrome/browser/crash_upload_list",
+      "//chrome/browser/ui/webui/bluetooth_internals",
+    ]
     sources += [
       "webui/crashes_ui.cc",
       "webui/crashes_ui.h",
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxFeatures.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxFeatures.java
index f33a129..7ad5b67 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxFeatures.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxFeatures.java
@@ -32,6 +32,9 @@
 
     private static final MutableFlagWithSafeDefault sOmniboxConsumesImeInsets =
             new MutableFlagWithSafeDefault(ChromeFeatureList.OMNIBOX_CONSUMERS_IME_INSETS, false);
+    private static final MutableFlagWithSafeDefault sShouldAdaptToNarrowTabletWindows =
+            new MutableFlagWithSafeDefault(
+                    ChromeFeatureList.OMNIBOX_ADAPT_NARROW_TABLET_WINDOWS, false);
 
     /**
      * @param context The activity context.
@@ -43,6 +46,14 @@
     }
 
     /**
+     * Returns whether the omnibox dropdown should be switched to a phone-like appearance when the
+     * window width is <600dp.
+     */
+    public static boolean shouldAdaptToNarrowTabletWindows() {
+        return sShouldAdaptToNarrowTabletWindows.isEnabled();
+    }
+
+    /**
      * @return Whether to show an active color for Omnibox which has a different background color
      *         than toolbar.
      */
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxSuggestionsDropdownEmbedderImpl.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxSuggestionsDropdownEmbedderImpl.java
index c196dd9a..38c51e9b 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxSuggestionsDropdownEmbedderImpl.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxSuggestionsDropdownEmbedderImpl.java
@@ -92,7 +92,7 @@
 
     @Override
     public boolean isTablet() {
-        if (OmniboxFeatures.shouldShowModernizeVisualUpdate(mContext)) {
+        if (OmniboxFeatures.shouldAdaptToNarrowTabletWindows()) {
             return mWindowWidthDp >= DeviceFormFactor.MINIMUM_TABLET_WIDTH_DP;
         } else {
             return DeviceFormFactor.isWindowOnTablet(mWindowAndroid);
@@ -155,7 +155,7 @@
         mWindowWidthDp = windowWidth;
         mWindowHeightDp = windowHeight;
 
-        if (OmniboxFeatures.shouldShowModernizeVisualUpdate(mContext)
+        if (OmniboxFeatures.shouldAdaptToNarrowTabletWindows()
                 || OmniboxFeatures.omniboxConsumesImeInsets()) {
             recalculateOmniboxAlignment();
         }
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxSuggestionsDropdownEmbedderImplTest.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxSuggestionsDropdownEmbedderImplTest.java
index 871846b..c68b8da 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxSuggestionsDropdownEmbedderImplTest.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxSuggestionsDropdownEmbedderImplTest.java
@@ -153,7 +153,8 @@
 
     @Test
     @Config(qualifiers = "w600dp-h820dp")
-    @EnableFeatures({ChromeFeatureList.OMNIBOX_MODERNIZE_VISUAL_UPDATE})
+    @EnableFeatures({ChromeFeatureList.OMNIBOX_MODERNIZE_VISUAL_UPDATE,
+            ChromeFeatureList.OMNIBOX_ADAPT_NARROW_TABLET_WINDOWS})
     @CommandLineFlags.
     Add({"enable-features=" + ChromeFeatureList.OMNIBOX_MODERNIZE_VISUAL_UPDATE + "<Study",
             "force-fieldtrials=Study/Group",
@@ -179,7 +180,8 @@
     }
 
     @Test
-    @EnableFeatures({ChromeFeatureList.OMNIBOX_MODERNIZE_VISUAL_UPDATE})
+    @EnableFeatures({ChromeFeatureList.OMNIBOX_MODERNIZE_VISUAL_UPDATE,
+            ChromeFeatureList.OMNIBOX_ADAPT_NARROW_TABLET_WINDOWS})
     @CommandLineFlags.
     Add({"enable-features=" + ChromeFeatureList.OMNIBOX_MODERNIZE_VISUAL_UPDATE + "<Study",
             "force-fieldtrials=Study/Group",
@@ -205,6 +207,27 @@
     }
 
     @Test
+    @EnableFeatures({ChromeFeatureList.OMNIBOX_ADAPT_NARROW_TABLET_WINDOWS})
+    @Config(qualifiers = "w600dp-h820dp")
+    public void testRecalculateOmniboxAlignment_tabletToPhoneSwitch_revampDisabled() {
+        doReturn(mAnchorView).when(mHorizontalAlignmentView).getParent();
+        doReturn(40).when(mHorizontalAlignmentView).getLeft();
+        mImpl.recalculateOmniboxAlignment();
+        OmniboxAlignment alignment = mImpl.getCurrentAlignment();
+        assertEquals(new OmniboxAlignment(0, ANCHOR_HEIGHT + ANCHOR_TOP, ANCHOR_WIDTH, 0, 40,
+                             ANCHOR_WIDTH - ALIGNMENT_WIDTH - 40),
+                alignment);
+
+        Configuration newConfig = new Configuration();
+        newConfig.smallestScreenWidthDp = DeviceFormFactor.MINIMUM_TABLET_WIDTH_DP - 1;
+        mImpl.onConfigurationChanged(newConfig);
+        assertFalse(mImpl.isTablet());
+        OmniboxAlignment newAlignment = mImpl.getCurrentAlignment();
+        assertEquals(new OmniboxAlignment(0, ANCHOR_HEIGHT + ANCHOR_TOP, ANCHOR_WIDTH, 0, 0, 0),
+                newAlignment);
+    }
+
+    @Test
     @Config(qualifiers = "w600dp-h820dp")
     @EnableFeatures({ChromeFeatureList.OMNIBOX_MODERNIZE_VISUAL_UPDATE})
     @CommandLineFlags.
diff --git a/chrome/browser/ui/browser_command_controller.cc b/chrome/browser/ui/browser_command_controller.cc
index e906ce45..645f50d3 100644
--- a/chrome/browser/ui/browser_command_controller.cc
+++ b/chrome/browser/ui/browser_command_controller.cc
@@ -795,6 +795,14 @@
     case IDC_MANAGE_EXTENSIONS:
       ShowExtensions(browser_->GetBrowserForOpeningWebUi());
       break;
+    case IDC_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS:
+      CHECK(base::FeatureList::IsEnabled(features::kExtensionsMenuInAppMenu));
+      ShowExtensions(browser_->GetBrowserForOpeningWebUi());
+      break;
+    case IDC_EXTENSIONS_SUBMENU_VISIT_CHROME_WEB_STORE:
+      CHECK(base::FeatureList::IsEnabled(features::kExtensionsMenuInAppMenu));
+      ShowWebStore(browser_);
+      break;
     case IDC_PERFORMANCE:
       ShowSettingsSubPage(browser_->GetBrowserForOpeningWebUi(),
                           chrome::kPerformanceSubPage);
@@ -1254,6 +1262,7 @@
   UpdateCommandsForContentRestrictionState();
   UpdateCommandsForBookmarkEditing();
   UpdateCommandsForIncognitoAvailability();
+  UpdateCommandsForExtensionsMenu();
   UpdateCommandsForTabKeyboardFocus(GetKeyboardFocusedTabIndex(browser_));
   UpdateCommandsForWebContentsFocus();
 }
@@ -1314,6 +1323,22 @@
   }
 }
 
+void BrowserCommandController::UpdateCommandsForExtensionsMenu() {
+  // TODO(crbug.com/401026): Talk with isandrk@chromium.org about whether this
+  // is necessary for the experiment or not.
+  if (is_locked_fullscreen_) {
+    return;
+  }
+
+  if (base::FeatureList::IsEnabled(features::kExtensionsMenuInAppMenu)) {
+    command_updater_.UpdateCommandEnabled(
+        IDC_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS,
+        /*state=*/true);
+    command_updater_.UpdateCommandEnabled(
+        IDC_EXTENSIONS_SUBMENU_VISIT_CHROME_WEB_STORE, /*state=*/true);
+  }
+}
+
 void BrowserCommandController::UpdateCommandsForTabState() {
   if (is_locked_fullscreen_)
     return;
diff --git a/chrome/browser/ui/browser_command_controller.h b/chrome/browser/ui/browser_command_controller.h
index 448cabe..6cf9112a 100644
--- a/chrome/browser/ui/browser_command_controller.h
+++ b/chrome/browser/ui/browser_command_controller.h
@@ -156,6 +156,9 @@
   // app windows.
   void UpdateCommandsForHostedAppAvailability();
 
+  // Update commands that are used in the Extensions menu in the app menu.
+  void UpdateCommandsForExtensionsMenu();
+
 #if BUILDFLAG(IS_CHROMEOS)
   // Update commands whose state depends on whether the window is in locked
   // fullscreen mode or not.
diff --git a/chrome/browser/ui/cocoa/l10n_util.mm b/chrome/browser/ui/cocoa/l10n_util.mm
index 665aea7..09c6ab6 100644
--- a/chrome/browser/ui/cocoa/l10n_util.mm
+++ b/chrome/browser/ui/cocoa/l10n_util.mm
@@ -8,7 +8,6 @@
 #include "base/mac/mac_util.h"
 #include "base/strings/string_util.h"
 #include "base/strings/sys_string_conversions.h"
-#import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
 
 namespace cocoa_l10n_util {
 
diff --git a/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc b/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
index 4173d07..672c3518 100644
--- a/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
+++ b/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
@@ -41,6 +41,7 @@
 #include "components/infobars/content/content_infobar_manager.h"
 #include "components/infobars/core/infobar_delegate.h"
 #include "components/permissions/permission_decision_auto_blocker.h"
+#include "components/permissions/permission_recovery_success_rate_tracker.h"
 #include "components/permissions/permission_result.h"
 #include "components/prefs/pref_service.h"
 #include "components/strings/grit/components_strings.h"
@@ -79,6 +80,9 @@
         std::make_unique<chrome::PageSpecificContentSettingsDelegate>(
             web_contents()));
     infobars::ContentInfoBarManager::CreateForWebContents(web_contents());
+
+    permissions::PermissionRecoverySuccessRateTracker::CreateForWebContents(
+        web_contents());
   }
 
   TestingProfile::TestingFactories GetTestingFactories() const override {
diff --git a/chrome/browser/ui/content_settings/content_setting_media_image_model_unittest.mm b/chrome/browser/ui/content_settings/content_setting_media_image_model_unittest.mm
index f723849..b0cdf8bd 100644
--- a/chrome/browser/ui/content_settings/content_setting_media_image_model_unittest.mm
+++ b/chrome/browser/ui/content_settings/content_setting_media_image_model_unittest.mm
@@ -21,6 +21,7 @@
 #include "components/content_settings/browser/page_specific_content_settings.h"
 #include "components/infobars/content/content_infobar_manager.h"
 #include "components/no_state_prefetch/browser/no_state_prefetch_manager.h"
+#include "components/permissions/permission_recovery_success_rate_tracker.h"
 #include "components/prefs/pref_service.h"
 #include "components/vector_icons/vector_icons.h"
 #include "content/public/test/web_contents_tester.h"
@@ -66,6 +67,9 @@
         std::make_unique<chrome::PageSpecificContentSettingsDelegate>(
             web_contents()));
     infobars::ContentInfoBarManager::CreateForWebContents(web_contents());
+
+    permissions::PermissionRecoverySuccessRateTracker::CreateForWebContents(
+        web_contents());
   }
 
   std::string GetDefaultAudioDevice() {
diff --git a/chrome/browser/ui/page_info/page_info_unittest.cc b/chrome/browser/ui/page_info/page_info_unittest.cc
index d328d60a..e1094a5 100644
--- a/chrome/browser/ui/page_info/page_info_unittest.cc
+++ b/chrome/browser/ui/page_info/page_info_unittest.cc
@@ -39,6 +39,7 @@
 #include "components/page_info/core/features.h"
 #include "components/page_info/page_info_ui.h"
 #include "components/permissions/features.h"
+#include "components/permissions/permission_recovery_success_rate_tracker.h"
 #include "components/safe_browsing/buildflags.h"
 #include "components/security_interstitials/content/stateful_ssl_host_state_delegate.h"
 #include "components/strings/grit/components_strings.h"
@@ -178,6 +179,9 @@
         std::make_unique<chrome::PageSpecificContentSettingsDelegate>(
             web_contents()));
 
+    permissions::PermissionRecoverySuccessRateTracker::CreateForWebContents(
+        web_contents());
+
     // Setup mock ui.
     ResetMockUI();
   }
diff --git a/chrome/browser/ui/quick_answers/ui/rich_answers_view.cc b/chrome/browser/ui/quick_answers/ui/rich_answers_view.cc
index de2c3953..ebe83b3 100644
--- a/chrome/browser/ui/quick_answers/ui/rich_answers_view.cc
+++ b/chrome/browser/ui/quick_answers/ui/rich_answers_view.cc
@@ -37,6 +37,9 @@
 constexpr int kSettingsButtonSizeDip = 14;
 constexpr int kSettingsButtonBorderDip = 3;
 
+// Border corner radius.
+constexpr int kBorderCornerRadius = 12;
+
 }  // namespace
 
 // RichAnswersView -----------------------------------------------------------
@@ -80,8 +83,12 @@
 
 void RichAnswersView::OnThemeChanged() {
   views::View::OnThemeChanged();
-  SetBackground(views::CreateSolidBackground(
+  SetBorder(views::CreateRoundedRectBorder(
+      /*thickness=*/2, kBorderCornerRadius,
       GetColorProvider()->GetColor(ui::kColorPrimaryBackground)));
+  SetBackground(views::CreateRoundedRectBackground(
+      GetColorProvider()->GetColor(ui::kColorPrimaryBackground),
+      kBorderCornerRadius, /*for_border_thickness=*/2));
   if (settings_button_) {
     settings_button_->SetImage(
         views::Button::ButtonState::STATE_NORMAL,
@@ -125,6 +132,7 @@
   params.shadow_type = views::Widget::InitParams::ShadowType::kDrop;
   params.type = views::Widget::InitParams::TYPE_POPUP;
   params.z_order = ui::ZOrderLevel::kFloatingUIElement;
+  params.corner_radius = kBorderCornerRadius;
 
   views::Widget* widget = new views::Widget();
   widget->Init(std::move(params));
diff --git a/chrome/browser/ui/tab_helpers.cc b/chrome/browser/ui/tab_helpers.cc
index 49f2349..dab3659 100644
--- a/chrome/browser/ui/tab_helpers.cc
+++ b/chrome/browser/ui/tab_helpers.cc
@@ -129,6 +129,7 @@
 #include "components/performance_manager/embedder/performance_manager_registry.h"
 #include "components/performance_manager/public/features.h"
 #include "components/permissions/features.h"
+#include "components/permissions/permission_recovery_success_rate_tracker.h"
 #include "components/permissions/permission_request_manager.h"
 #include "components/permissions/unused_site_permissions_service.h"
 #include "components/safe_browsing/content/browser/safe_browsing_navigation_observer.h"
@@ -375,6 +376,8 @@
     pm_registry->SetPageType(web_contents, performance_manager::PageType::kTab);
   }
   permissions::PermissionRequestManager::CreateForWebContents(web_contents);
+  permissions::PermissionRecoverySuccessRateTracker::CreateForWebContents(
+      web_contents);
   // The PopupBlockerTabHelper has an implicit dependency on
   // ChromeSubresourceFilterClient being available in its constructor.
   blocked_content::PopupBlockerTabHelper::CreateForWebContents(web_contents);
diff --git a/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_model_listener.cc b/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_model_listener.cc
index f9cddf7..eeaf5bb838 100644
--- a/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_model_listener.cc
+++ b/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_model_listener.cc
@@ -7,7 +7,6 @@
 #include "base/containers/flat_set.h"
 #include "base/memory/raw_ptr.h"
 #include "base/ranges/algorithm.h"
-#include "chrome/browser/favicon/favicon_utils.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_list.h"
@@ -21,37 +20,6 @@
 #include "content/public/browser/web_contents.h"
 #include "ui/base/page_transition_types.h"
 
-SavedTabGroupWebContentsListener::SavedTabGroupWebContentsListener(
-    content::WebContents* web_contents,
-    base::Token token,
-    SavedTabGroupModel* model)
-    : token_(token), web_contents_(web_contents), model_(model) {
-  Observe(web_contents_);
-}
-
-SavedTabGroupWebContentsListener::~SavedTabGroupWebContentsListener() = default;
-
-void SavedTabGroupWebContentsListener::DidFinishNavigation(
-    content::NavigationHandle* navigation_handle) {
-  ui::PageTransition page_transition = navigation_handle->GetPageTransition();
-  if (!ui::IsValidPageTransitionType(page_transition) ||
-      ui::PageTransitionIsRedirect(page_transition) ||
-      !ui::PageTransitionIsMainFrame(page_transition)) {
-    return;
-  }
-
-  SavedTabGroup* group = model_->GetGroupContainingTab(token_);
-  if (!group) {
-    return;
-  }
-
-  SavedTabGroupTab* tab = group->GetTab(token_);
-  tab->SetTitle(web_contents_->GetTitle());
-  tab->SetURL(web_contents_->GetURL());
-  tab->SetFavicon(favicon::TabFaviconFromWebContents(web_contents_));
-  model_->UpdateTabInGroup(group->saved_guid(), *tab);
-}
-
 // TODO(crbug/1376259): Update SavedTabGroupModel state with any groups that
 // should be in the SavedTabGroupModel.
 SavedTabGroupBrowserListener::SavedTabGroupBrowserListener(
diff --git a/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_model_listener.h b/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_model_listener.h
index 13843fd..837cdca 100644
--- a/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_model_listener.h
+++ b/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_model_listener.h
@@ -8,36 +8,16 @@
 #include "base/containers/flat_set.h"
 #include "base/memory/raw_ptr.h"
 #include "chrome/browser/ui/browser_list_observer.h"
+#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_web_contents_listener.h"
 #include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
 #include "components/tab_groups/tab_group_id.h"
 #include "content/public/browser/navigation_handle.h"
 #include "content/public/browser/web_contents.h"
-#include "content/public/browser/web_contents_observer.h"
 
 class SavedTabGroupModel;
 class TabStripModel;
 class Profile;
 
-class SavedTabGroupWebContentsListener : public content::WebContentsObserver {
- public:
-  SavedTabGroupWebContentsListener(content::WebContents* web_contents,
-                                   base::Token token,
-                                   SavedTabGroupModel* model);
-  ~SavedTabGroupWebContentsListener() override;
-
-  // content::WebContentsObserver
-  void DidFinishNavigation(
-      content::NavigationHandle* navigation_handle) override;
-
-  base::Token token() { return token_; }
-  content::WebContents* web_contents() { return web_contents_; }
-
- private:
-  base::Token token_;
-  raw_ptr<content::WebContents> web_contents_;
-  raw_ptr<SavedTabGroupModel> model_;
-};
-
 // Manages the listening state for each individual tabstrip.
 class SavedTabGroupBrowserListener : public TabStripModelObserver {
  public:
diff --git a/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_web_contents_listener.cc b/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_web_contents_listener.cc
new file mode 100644
index 0000000..eb2f4ef
--- /dev/null
+++ b/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_web_contents_listener.cc
@@ -0,0 +1,44 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_web_contents_listener.h"
+
+#include "chrome/browser/favicon/favicon_utils.h"
+#include "components/saved_tab_groups/saved_tab_group.h"
+#include "components/saved_tab_groups/saved_tab_group_model.h"
+#include "components/saved_tab_groups/saved_tab_group_tab.h"
+#include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/web_contents.h"
+#include "ui/base/page_transition_types.h"
+
+SavedTabGroupWebContentsListener::SavedTabGroupWebContentsListener(
+    content::WebContents* web_contents,
+    base::Token token,
+    SavedTabGroupModel* model)
+    : token_(token), web_contents_(web_contents), model_(model) {
+  Observe(web_contents_);
+}
+
+SavedTabGroupWebContentsListener::~SavedTabGroupWebContentsListener() = default;
+
+void SavedTabGroupWebContentsListener::DidFinishNavigation(
+    content::NavigationHandle* navigation_handle) {
+  ui::PageTransition page_transition = navigation_handle->GetPageTransition();
+  if (!ui::IsValidPageTransitionType(page_transition) ||
+      ui::PageTransitionIsRedirect(page_transition) ||
+      !ui::PageTransitionIsMainFrame(page_transition)) {
+    return;
+  }
+
+  SavedTabGroup* group = model_->GetGroupContainingTab(token_);
+  if (!group) {
+    return;
+  }
+
+  SavedTabGroupTab* tab = group->GetTab(token_);
+  tab->SetTitle(web_contents_->GetTitle());
+  tab->SetURL(web_contents_->GetURL());
+  tab->SetFavicon(favicon::TabFaviconFromWebContents(web_contents_));
+  model_->UpdateTabInGroup(group->saved_guid(), *tab);
+}
diff --git a/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_web_contents_listener.h b/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_web_contents_listener.h
new file mode 100644
index 0000000..40c0782
--- /dev/null
+++ b/chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_web_contents_listener.h
@@ -0,0 +1,38 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_TABS_SAVED_TAB_GROUPS_SAVED_TAB_GROUP_WEB_CONTENTS_LISTENER_H_
+#define CHROME_BROWSER_UI_TABS_SAVED_TAB_GROUPS_SAVED_TAB_GROUP_WEB_CONTENTS_LISTENER_H_
+
+#include "base/token.h"
+#include "content/public/browser/web_contents_observer.h"
+
+class SavedTabGroupModel;
+
+namespace content {
+class NavigationHandle;
+class WebContents;
+}  // namespace content
+
+class SavedTabGroupWebContentsListener : public content::WebContentsObserver {
+ public:
+  SavedTabGroupWebContentsListener(content::WebContents* web_contents,
+                                   base::Token token,
+                                   SavedTabGroupModel* model);
+  ~SavedTabGroupWebContentsListener() override;
+
+  // content::WebContentsObserver
+  void DidFinishNavigation(
+      content::NavigationHandle* navigation_handle) override;
+
+  base::Token token() { return token_; }
+  content::WebContents* web_contents() { return web_contents_; }
+
+ private:
+  base::Token token_;
+  raw_ptr<content::WebContents> web_contents_;
+  raw_ptr<SavedTabGroupModel> model_;
+};
+
+#endif  // CHROME_BROWSER_UI_TABS_SAVED_TAB_GROUPS_SAVED_TAB_GROUP_WEB_CONTENTS_LISTENER_H_
diff --git a/chrome/browser/ui/toolbar/app_menu_model.cc b/chrome/browser/ui/toolbar/app_menu_model.cc
index 655ce235..cf694aa6 100644
--- a/chrome/browser/ui/toolbar/app_menu_model.cc
+++ b/chrome/browser/ui/toolbar/app_menu_model.cc
@@ -110,9 +110,14 @@
 
 DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(AppMenuModel, kDownloadsMenuItem);
 DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(AppMenuModel, kHistoryMenuItem);
+DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(AppMenuModel, kExtensionsMenuItem);
 DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(AppMenuModel, kMoreToolsMenuItem);
 DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(AppMenuModel, kIncognitoMenuItem);
 DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ToolsMenuModel, kPerformanceMenuItem);
+DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ExtensionsMenuModel,
+                                      kManageExtensionsMenuItem);
+DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ExtensionsMenuModel,
+                                      kVisitChromeWebStoreMenuItem);
 
 namespace {
 
@@ -246,7 +251,9 @@
 
   AddSeparator(ui::NORMAL_SEPARATOR);
   AddItemWithStringId(IDC_CLEAR_BROWSING_DATA, IDS_CLEAR_BROWSING_DATA);
-  AddItemWithStringId(IDC_MANAGE_EXTENSIONS, IDS_SHOW_EXTENSIONS);
+  if (!base::FeatureList::IsEnabled(features::kExtensionsMenuInAppMenu)) {
+    AddItemWithStringId(IDC_MANAGE_EXTENSIONS, IDS_SHOW_EXTENSIONS);
+  }
   if (base::FeatureList::IsEnabled(
           performance_manager::features::kHighEfficiencyModeAvailable) ||
       base::FeatureList::IsEnabled(
@@ -270,6 +277,36 @@
 }
 
 ////////////////////////////////////////////////////////////////////////////////
+// ExtensionsMenuModel
+
+ExtensionsMenuModel::ExtensionsMenuModel(
+    ui::SimpleMenuModel::Delegate* delegate,
+    Browser* browser)
+    : SimpleMenuModel(delegate) {
+  Build(browser);
+}
+
+ExtensionsMenuModel::~ExtensionsMenuModel() = default;
+
+// Extensions (sub)menu is constructed as follows:
+// - An overflow with two items:
+//   - An item to manage extensions at chrome://extensions
+//   - An item to visit the Chrome Web Store
+void ExtensionsMenuModel::Build(Browser* browser) {
+  AddItemWithStringId(IDC_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS,
+                      IDS_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS_ITEM);
+  SetElementIdentifierAt(
+      GetIndexOfCommandId(IDC_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS).value(),
+      kManageExtensionsMenuItem);
+  AddItemWithStringId(IDC_EXTENSIONS_SUBMENU_VISIT_CHROME_WEB_STORE,
+                      IDS_EXTENSIONS_SUBMENU_CHROME_WEBSTORE_ITEM);
+  SetElementIdentifierAt(
+      GetIndexOfCommandId(IDC_EXTENSIONS_SUBMENU_VISIT_CHROME_WEB_STORE)
+          .value(),
+      kVisitChromeWebStoreMenuItem);
+}
+
+////////////////////////////////////////////////////////////////////////////////
 // AppMenuModel
 
 // static
@@ -478,7 +515,24 @@
       }
       LogMenuAction(MENU_ACTION_PIN_TO_START_SCREEN);
       break;
-
+    // Extensions menu.
+    case IDC_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS:
+      CHECK(base::FeatureList::IsEnabled(features::kExtensionsMenuInAppMenu));
+      // Logging the original histograms for experiment comparison purposes.
+      if (!uma_action_recorded_) {
+        UMA_HISTOGRAM_MEDIUM_TIMES("WrenchMenu.TimeToAction.ManageExtensions",
+                                   delta);
+      }
+      LogMenuAction(MENU_ACTION_MANAGE_EXTENSIONS);
+      break;
+    case IDC_EXTENSIONS_SUBMENU_VISIT_CHROME_WEB_STORE:
+      CHECK(base::FeatureList::IsEnabled(features::kExtensionsMenuInAppMenu));
+      if (!uma_action_recorded_) {
+        UMA_HISTOGRAM_MEDIUM_TIMES(
+            "WrenchMenu.TimeToAction.VisitChromeWebStore", delta);
+      }
+      LogMenuAction(MENU_ACTION_VISIT_CHROME_WEB_STORE);
+      break;
     // Recent tabs menu.
     case IDC_RESTORE_TAB:
       if (!uma_action_recorded_)
@@ -899,6 +953,15 @@
                            bookmark_sub_menu_model_.get());
   }
 
+  if (base::FeatureList::IsEnabled(features::kExtensionsMenuInAppMenu)) {
+    // Extensions sub menu.
+    sub_menus_.push_back(std::make_unique<ExtensionsMenuModel>(this, browser_));
+    AddSubMenuWithStringId(IDC_EXTENSIONS_SUBMENU, IDS_EXTENSIONS_SUBMENU,
+                           sub_menus_.back().get());
+    SetElementIdentifierAt(GetIndexOfCommandId(IDC_EXTENSIONS_SUBMENU).value(),
+                           kExtensionsMenuItem);
+  }
+
   AddSeparator(ui::LOWER_SEPARATOR);
   CreateZoomMenu();
   AddSeparator(ui::UPPER_SEPARATOR);
diff --git a/chrome/browser/ui/toolbar/app_menu_model.h b/chrome/browser/ui/toolbar/app_menu_model.h
index fa3ceed4..5d39d301 100644
--- a/chrome/browser/ui/toolbar/app_menu_model.h
+++ b/chrome/browser/ui/toolbar/app_menu_model.h
@@ -84,6 +84,9 @@
   MENU_ACTION_CHROME_WHATS_NEW = 54,
   MENU_ACTION_LACROS_DATA_MIGRATION = 55,
   MENU_ACTION_MENU_OPENED = 56,
+  // Only used by ExtensionsMenuModel sub menu.
+  MENU_ACTION_VISIT_CHROME_WEB_STORE = 57,
+  MENU_ACTION_MANAGE_EXTENSIONS_VIA_EXT_MENU = 58,
   LIMIT_MENU_ACTION
 };
 
@@ -121,6 +124,23 @@
   void Build(Browser* browser);
 };
 
+class ExtensionsMenuModel : public ui::SimpleMenuModel {
+ public:
+  DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kManageExtensionsMenuItem);
+  DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kVisitChromeWebStoreMenuItem);
+
+  ExtensionsMenuModel(ui::SimpleMenuModel::Delegate* delegate,
+                      Browser* browser);
+
+  ExtensionsMenuModel(const ExtensionsMenuModel&) = delete;
+  ExtensionsMenuModel& operator=(const ExtensionsMenuModel&) = delete;
+
+  ~ExtensionsMenuModel() override;
+
+ private:
+  void Build(Browser* browser);
+};
+
 // A menu model that builds the contents of the app menu.
 class AppMenuModel : public ui::SimpleMenuModel,
                      public ui::SimpleMenuModel::Delegate,
@@ -130,6 +150,7 @@
  public:
   DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kDownloadsMenuItem);
   DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kHistoryMenuItem);
+  DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kExtensionsMenuItem);
   DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kMoreToolsMenuItem);
   DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kIncognitoMenuItem);
 
diff --git a/chrome/browser/ui/toolbar/app_menu_model_interactive_uitest.cc b/chrome/browser/ui/toolbar/app_menu_model_interactive_uitest.cc
index 616a2987..95ae0a9 100644
--- a/chrome/browser/ui/toolbar/app_menu_model_interactive_uitest.cc
+++ b/chrome/browser/ui/toolbar/app_menu_model_interactive_uitest.cc
@@ -5,6 +5,7 @@
 #include "base/logging.h"
 #include "base/test/bind.h"
 #include "base/test/gtest_util.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/accelerator_utils.h"
@@ -13,6 +14,7 @@
 #include "chrome/browser/ui/browser_list.h"
 #include "chrome/browser/ui/browser_window.h"
 #include "chrome/browser/ui/toolbar/app_menu_model.h"
+#include "chrome/browser/ui/ui_features.h"
 #include "chrome/common/webui_url_constants.h"
 #include "chrome/test/base/interactive_test_utils.h"
 #include "chrome/test/base/ui_test_utils.h"
@@ -21,6 +23,7 @@
 #include "chrome/test/interaction/webcontents_interaction_test_util.h"
 #include "components/performance_manager/public/features.h"
 #include "content/public/test/browser_test.h"
+#include "extensions/common/extension_urls.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/base/interaction/element_identifier.h"
 #include "ui/base/interaction/element_tracker.h"
@@ -70,6 +73,8 @@
       return new_browser->profile()->IsIncognitoProfile();
     }));
   }
+
+  base::test::ScopedFeatureList scoped_feature_list_;
 };
 
 IN_PROC_BROWSER_TEST_F(AppMenuModelInteractiveTest, PerformanceNavigation) {
@@ -97,3 +102,105 @@
       SendAccelerator(kAppMenuButtonElementId, incognito_accelerator),
       CheckInconitoWindowOpened());
 }
+
+class ExtensionsMenuModelInteractiveTest : public AppMenuModelInteractiveTest {
+ public:
+  explicit ExtensionsMenuModelInteractiveTest(bool enable_feature = true) {
+    scoped_feature_list_.InitWithFeatureState(
+        features::kExtensionsMenuInAppMenu, enable_feature);
+  }
+  ~ExtensionsMenuModelInteractiveTest() override = default;
+  ExtensionsMenuModelInteractiveTest(
+      const ExtensionsMenuModelInteractiveTest&) = delete;
+  void operator=(const ExtensionsMenuModelInteractiveTest&) = delete;
+
+  void SetUp() override {
+    set_open_about_blank_on_browser_launch(true);
+    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
+    InteractiveBrowserTest::SetUp();
+  }
+
+ protected:
+  base::HistogramTester histograms;
+};
+
+class ExtensionsMenuModelPresenceTest
+    : public ExtensionsMenuModelInteractiveTest,
+      public testing::WithParamInterface<bool> {
+ public:
+  ExtensionsMenuModelPresenceTest()
+      : ExtensionsMenuModelInteractiveTest(/*enable_feature=*/GetParam()) {}
+  ~ExtensionsMenuModelPresenceTest() override = default;
+  ExtensionsMenuModelPresenceTest(const ExtensionsMenuModelPresenceTest&) =
+      delete;
+  void operator=(const ExtensionsMenuModelPresenceTest&) = delete;
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    ExtensionsMenuModelPresenceTest,
+    /* features::kNewExtensionsTopLevelMenu status */ testing::Bool());
+
+// Test to confirm that the structure of the Extensions menu is present but that
+// no histograms are logged since it isn't interacted with.
+IN_PROC_BROWSER_TEST_P(ExtensionsMenuModelPresenceTest, MenuPresence) {
+  if (GetParam()) {  // Menu enabled
+    RunTestSequence(
+        InstrumentTab(kPrimaryTabPageElementId),
+        PressButton(kAppMenuButtonElementId),
+        EnsurePresent(AppMenuModel::kExtensionsMenuItem),
+        SelectMenuItem(AppMenuModel::kExtensionsMenuItem),
+        EnsurePresent(ExtensionsMenuModel::kManageExtensionsMenuItem),
+        EnsurePresent(ExtensionsMenuModel::kVisitChromeWebStoreMenuItem));
+  } else {
+    RunTestSequence(InstrumentTab(kPrimaryTabPageElementId),
+                    PressButton(kAppMenuButtonElementId),
+                    EnsureNotPresent(AppMenuModel::kExtensionsMenuItem));
+  }
+
+  histograms.ExpectTotalCount("WrenchMenu.TimeToAction.VisitChromeWebStore", 0);
+  histograms.ExpectTotalCount("WrenchMenu.TimeToAction.ManageExtensions", 0);
+  histograms.ExpectBucketCount("WrenchMenu.MenuAction",
+                               MENU_ACTION_MANAGE_EXTENSIONS, 0);
+  histograms.ExpectBucketCount("WrenchMenu.MenuAction",
+                               MENU_ACTION_VISIT_CHROME_WEB_STORE, 0);
+}
+
+// Test to confirm that the manage extensions menu item navigates when selected
+// and emite histograms that it did so.
+IN_PROC_BROWSER_TEST_F(ExtensionsMenuModelInteractiveTest, ManageExtensions) {
+  RunTestSequence(
+      InstrumentTab(kPrimaryTabPageElementId),
+      PressButton(kAppMenuButtonElementId),
+      SelectMenuItem(AppMenuModel::kExtensionsMenuItem),
+      SelectMenuItem(ExtensionsMenuModel::kManageExtensionsMenuItem),
+      WaitForWebContentsNavigation(kPrimaryTabPageElementId,
+                                   GURL(chrome::kChromeUIExtensionsURL)));
+
+  histograms.ExpectTotalCount("WrenchMenu.TimeToAction.ManageExtensions", 1);
+  histograms.ExpectTotalCount("WrenchMenu.TimeToAction.VisitChromeWebStore", 0);
+  histograms.ExpectBucketCount("WrenchMenu.MenuAction",
+                               MENU_ACTION_MANAGE_EXTENSIONS, 1);
+  histograms.ExpectBucketCount("WrenchMenu.MenuAction",
+                               MENU_ACTION_VISIT_CHROME_WEB_STORE, 0);
+}
+
+// Test to confirm that the visit Chome Web Store menu item navigates when
+// selected and emits histograms that it did so.
+IN_PROC_BROWSER_TEST_F(ExtensionsMenuModelInteractiveTest,
+                       VisitChromeWebStore) {
+  RunTestSequence(
+      InstrumentTab(kPrimaryTabPageElementId),
+      PressButton(kAppMenuButtonElementId),
+      SelectMenuItem(AppMenuModel::kExtensionsMenuItem),
+      SelectMenuItem(ExtensionsMenuModel::kVisitChromeWebStoreMenuItem),
+      WaitForWebContentsNavigation(kPrimaryTabPageElementId,
+                                   extension_urls::GetWebstoreLaunchURL()));
+
+  histograms.ExpectTotalCount("WrenchMenu.TimeToAction.VisitChromeWebStore", 1);
+  histograms.ExpectTotalCount("WrenchMenu.TimeToAction.ManageExtensions", 0);
+  histograms.ExpectBucketCount("WrenchMenu.MenuAction",
+                               MENU_ACTION_VISIT_CHROME_WEB_STORE, 1);
+  histograms.ExpectBucketCount("WrenchMenu.MenuAction",
+                               MENU_ACTION_MANAGE_EXTENSIONS, 0);
+}
diff --git a/chrome/browser/ui/toolbar/app_menu_model_unittest.cc b/chrome/browser/ui/toolbar/app_menu_model_unittest.cc
index a7f0d08f..e7b711d 100644
--- a/chrome/browser/ui/toolbar/app_menu_model_unittest.cc
+++ b/chrome/browser/ui/toolbar/app_menu_model_unittest.cc
@@ -18,6 +18,7 @@
 #include "chrome/browser/ui/global_error/global_error_service_factory.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/toolbar/app_menu_icon_controller.h"
+#include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/upgrade_detector/upgrade_detector.h"
 #include "chrome/test/base/browser_with_test_window_test.h"
 #include "chrome/test/base/menu_model_test.h"
@@ -115,8 +116,30 @@
                                   ui::Accelerator* accelerator) const override {
     return false;
   }
+
+ protected:
+  base::test::ScopedFeatureList feature_list_;
 };
 
+class ExtensionsMenuModelTest : public AppMenuModelTest,
+                                public testing::WithParamInterface<bool> {
+ public:
+  ExtensionsMenuModelTest() {
+    feature_list_.InitWithFeatureState(features::kExtensionsMenuInAppMenu,
+                                       GetParam());
+  }
+
+  ExtensionsMenuModelTest(const ExtensionsMenuModelTest&) = delete;
+  ExtensionsMenuModelTest& operator=(const ExtensionsMenuModelTest&) = delete;
+
+  ~ExtensionsMenuModelTest() override = default;
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    ExtensionsMenuModelTest,
+    /* features::kNewExtensionsTopLevelMenu enabled */ testing::Bool());
+
 // Copies parts of MenuModelTest::Delegate and combines them with the
 // AppMenuModel since AppMenuModel is now a SimpleMenuModel::Delegate and
 // not derived from SimpleMenuModel.
@@ -258,6 +281,27 @@
   EXPECT_EQ(1, error1->execute_count());
 }
 
+// Tests that extensions sub menu (when enabled) generates the correct elements
+// or does not generate its elements when disabled.
+TEST_P(ExtensionsMenuModelTest, ExtensionsMenu) {
+  AppMenuModel model(this, browser());
+  model.Init();
+
+  if (GetParam()) {  // Menu enabled
+    ASSERT_TRUE(model.GetIndexOfCommandId(IDC_EXTENSIONS_SUBMENU));
+    ui::MenuModel* extensions_submenu = model.GetSubmenuModelAt(
+        model.GetIndexOfCommandId(IDC_EXTENSIONS_SUBMENU).value());
+    ASSERT_NE(extensions_submenu, nullptr);
+    ASSERT_EQ(2ul, extensions_submenu->GetItemCount());
+    EXPECT_EQ(IDC_EXTENSIONS_SUBMENU_MANAGE_EXTENSIONS,
+              extensions_submenu->GetCommandIdAt(0));
+    EXPECT_EQ(IDC_EXTENSIONS_SUBMENU_VISIT_CHROME_WEB_STORE,
+              extensions_submenu->GetCommandIdAt(1));
+  } else {
+    EXPECT_FALSE(model.GetIndexOfCommandId(IDC_EXTENSIONS_SUBMENU));
+  }
+}
+
 TEST_F(AppMenuModelTest, EnabledPerformanceItem) {
   base::test::ScopedFeatureList scoped_feature_list;
   scoped_feature_list.InitAndEnableFeature(
diff --git a/chrome/browser/ui/ui_features.cc b/chrome/browser/ui/ui_features.cc
index 0c0c115..9909e77 100644
--- a/chrome/browser/ui/ui_features.cc
+++ b/chrome/browser/ui/ui_features.cc
@@ -48,6 +48,12 @@
 #endif
 );
 
+// Create new Extensions app menu option (removing "More Tools -> Extensions")
+// with submenu to manage extensions and visit chrome web store.
+BASE_FEATURE(kExtensionsMenuInAppMenu,
+             "kExtensionsMenuInAppMenu",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 #if !defined(ANDROID)
 // Enables "Access Code Cast" UI.
 BASE_FEATURE(kAccessCodeCastUI,
diff --git a/chrome/browser/ui/ui_features.h b/chrome/browser/ui/ui_features.h
index 12bb173a..79b6bab 100644
--- a/chrome/browser/ui/ui_features.h
+++ b/chrome/browser/ui/ui_features.h
@@ -39,6 +39,8 @@
 
 BASE_DECLARE_FEATURE(kChromeWhatsNewUI);
 
+BASE_DECLARE_FEATURE(kExtensionsMenuInAppMenu);
+
 #if !defined(ANDROID)
 BASE_DECLARE_FEATURE(kAccessCodeCastUI);
 #endif
diff --git a/chrome/browser/ui/views/collected_cookies_views_browsertest.cc b/chrome/browser/ui/views/collected_cookies_views_browsertest.cc
index 0381a549..f435f5c 100644
--- a/chrome/browser/ui/views/collected_cookies_views_browsertest.cc
+++ b/chrome/browser/ui/views/collected_cookies_views_browsertest.cc
@@ -26,6 +26,7 @@
 #include "components/page_info/core/features.h"
 #include "content/public/common/content_paths.h"
 #include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "ui/base/interaction/element_identifier.h"
 #include "ui/events/base_event_utils.h"
@@ -244,10 +245,7 @@
   bool RunScriptAndGetBool(const std::string& script,
                            content::WebContents* web_contents) {
     EXPECT_TRUE(web_contents);
-    bool data;
-    EXPECT_TRUE(
-        content::ExecuteScriptAndExtractBool(web_contents, script, &data));
-    return data;
+    return content::EvalJs(web_contents, script).ExtractBool();
   }
 
   base::test::ScopedFeatureList feature_list_;
diff --git a/chrome/browser/ui/views/extensions/extensions_toolbar_container.h b/chrome/browser/ui/views/extensions/extensions_toolbar_container.h
index a131512..8546a66a 100644
--- a/chrome/browser/ui/views/extensions/extensions_toolbar_container.h
+++ b/chrome/browser/ui/views/extensions/extensions_toolbar_container.h
@@ -196,16 +196,16 @@
     std::string extension_id;
   };
 
-  // Check if the ExtensionsMenuView or ExtensionsTabbedMenuView is showing.
-  // TODO(crbug.com/1279986): This method will be removed once
-  // ExtensionsTabbedMenu is fully rolled out and we will call directly into the
-  // ExtensionsTabbedMenuCoordinator.
+  // Check if the extensions menu is showing.
+  // TODO(crbug.com/1279986): This method will be removed once extensions menu
+  // under kExtensionsMenuAccessControl feature is fully rolled out and we can
+  // call directly into the menu coordinator.
   bool IsExtensionsMenuShowing() const;
 
-  // // Hides the currently-showing ExtensionsMenuView or
-  // ExtensionsTabbedMenuView, if it exists. TODO(crbug.com/1279986): This
-  // method will be removed once ExtensionsTabbedMenu is fully rolled out and we
-  // will call directly into the ExtensionsTabbedMenuCoordinator.
+  // Hides the currently-showing extensions menu, if it exists.
+  // TODO(crbug.com/1279986): This method will be removed once extensions menu
+  // under kExtensionsMenuAccessControl feature is fully rolled out and we can
+  // call directly into the menu coordinator.
   void HideExtensionsMenu();
 
   // Determines whether an action must be visible (i.e. cannot be hidden for any
diff --git a/chrome/browser/ui/views/extensions/extensions_toolbar_unittest.h b/chrome/browser/ui/views/extensions/extensions_toolbar_unittest.h
index 3937072..7ded1105b 100644
--- a/chrome/browser/ui/views/extensions/extensions_toolbar_unittest.h
+++ b/chrome/browser/ui/views/extensions/extensions_toolbar_unittest.h
@@ -20,8 +20,7 @@
 
 // Base class for unit tests that use the toolbar area. This is used for unit
 // tests that are generally related to the ExtensionsToolbarContainer in the
-// ToolbarView area (such as ExtensionsToolbarControls and
-// ExtensionsTabbedMenuView).
+// ToolbarView area (e.g ExtensionsToolbarControls).
 // When possible, prefer creating a unit test with browser view instead of a
 // interactive ui or browser test since they are faster and less flaky.
 class ExtensionsToolbarUnitTest : public TestWithBrowserView {
diff --git a/chrome/browser/ui/views/page_info/page_info_bubble_view_unittest.cc b/chrome/browser/ui/views/page_info/page_info_bubble_view_unittest.cc
index c04428e..a6a042b 100644
--- a/chrome/browser/ui/views/page_info/page_info_bubble_view_unittest.cc
+++ b/chrome/browser/ui/views/page_info/page_info_bubble_view_unittest.cc
@@ -39,6 +39,7 @@
 #include "components/content_settings/core/common/pref_names.h"
 #include "components/history/core/browser/history_service.h"
 #include "components/page_info/core/features.h"
+#include "components/permissions/permission_recovery_success_rate_tracker.h"
 #include "components/permissions/permission_uma_util.h"
 #include "components/permissions/permission_util.h"
 #include "components/privacy_sandbox/privacy_sandbox_features.h"
@@ -430,6 +431,9 @@
             web_contents));
     api_ = std::make_unique<test::PageInfoBubbleViewTestApi>(
         parent_window_->GetNativeWindow(), web_contents);
+
+    permissions::PermissionRecoverySuccessRateTracker::CreateForWebContents(
+        web_contents);
   }
 
   void TearDown() override {
diff --git a/chrome/browser/ui/views/toolbar/app_menu.cc b/chrome/browser/ui/views/toolbar/app_menu.cc
index 8d7e86c..970f9ad 100644
--- a/chrome/browser/ui/views/toolbar/app_menu.cc
+++ b/chrome/browser/ui/views/toolbar/app_menu.cc
@@ -35,6 +35,7 @@
 #include "chrome/browser/ui/global_error/global_error_service_factory.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/toolbar/app_menu_model.h"
+#include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/ui/user_education/scoped_new_badge_tracker.h"
 #include "chrome/browser/ui/views/bookmarks/bookmark_menu_delegate.h"
 #include "chrome/browser/ui/views/frame/app_menu_button.h"
@@ -983,6 +984,11 @@
   if (command_id == IDC_MORE_TOOLS_MENU)
     return true;
 
+  if (base::FeatureList::IsEnabled(features::kExtensionsMenuInAppMenu) &&
+      command_id == IDC_EXTENSIONS_SUBMENU) {
+    return true;
+  }
+
   if (command_id == IDC_SHARING_HUB_MENU)
     return true;
 
diff --git a/chrome/browser/ui/views/toolbar/toolbar_view.cc b/chrome/browser/ui/views/toolbar/toolbar_view.cc
index 3e4c8f7..1e2a289 100644
--- a/chrome/browser/ui/views/toolbar/toolbar_view.cc
+++ b/chrome/browser/ui/views/toolbar/toolbar_view.cc
@@ -160,6 +160,9 @@
   return kViewCommandMap;
 }
 
+constexpr int kToolbarDividerWidth = 2;
+constexpr int kToolbarDividerHeight = 16;
+constexpr int kToolbarDividerCornerRadius = 1;
 }  // namespace
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -241,12 +244,17 @@
       base::BindRepeating(callback, browser_, IDC_HOME), prefs);
 
   std::unique_ptr<ExtensionsToolbarContainer> extensions_container;
+  std::unique_ptr<views::View> toolbar_divider;
 
   // Do not create the extensions or browser actions container if it is a guest
   // profile (only regular and incognito profiles host extensions).
   if (!browser_->profile()->IsGuestSession()) {
     extensions_container =
         std::make_unique<ExtensionsToolbarContainer>(browser_);
+
+    if (features::IsChromeRefresh2023()) {
+      toolbar_divider = std::make_unique<views::View>();
+    }
   }
   std::unique_ptr<media_router::CastToolbarButton> cast;
   if (media_router::MediaRouterEnabled(browser_->profile()))
@@ -288,6 +296,12 @@
   if (extensions_container)
     extensions_container_ = AddChildView(std::move(extensions_container));
 
+  if (toolbar_divider) {
+    toolbar_divider_ = AddChildView(std::move(toolbar_divider));
+    toolbar_divider_->SetPreferredSize(
+        gfx::Size(kToolbarDividerWidth, kToolbarDividerHeight));
+  }
+
   if (base::FeatureList::IsEnabled(features::kChromeLabs)) {
     chrome_labs_model_ = std::make_unique<ChromeLabsBubbleViewModel>();
     UpdateChromeLabsNewBadgePrefs(browser_->profile(),
@@ -728,6 +742,12 @@
                                        extensions_flex_rule);
   }
 
+  if (toolbar_divider_) {
+    SkColor color = GetColorProvider()->GetColor(ui::kColorSysOutline);
+    toolbar_divider_->SetBackground(
+        views::CreateRoundedRectBackground(color, kToolbarDividerCornerRadius));
+  }
+
   LayoutCommon();
 }
 
diff --git a/chrome/browser/ui/views/toolbar/toolbar_view.h b/chrome/browser/ui/views/toolbar/toolbar_view.h
index 129df5e..62db2b41 100644
--- a/chrome/browser/ui/views/toolbar/toolbar_view.h
+++ b/chrome/browser/ui/views/toolbar/toolbar_view.h
@@ -266,6 +266,7 @@
   raw_ptr<CustomTabBarView> custom_tab_bar_ = nullptr;
   raw_ptr<LocationBarView> location_bar_ = nullptr;
   raw_ptr<ExtensionsToolbarContainer> extensions_container_ = nullptr;
+  raw_ptr<views::View> toolbar_divider_ = nullptr;
   raw_ptr<ChromeLabsButton> chrome_labs_button_ = nullptr;
   raw_ptr<BatterySaverButton> battery_saver_button_ = nullptr;
   raw_ptr<media_router::CastToolbarButton> cast_ = nullptr;
diff --git a/chrome/browser/ui/views/user_education/tutorial_interactive_uitest.cc b/chrome/browser/ui/views/user_education/tutorial_interactive_uitest.cc
index f834009..60edf50b 100644
--- a/chrome/browser/ui/views/user_education/tutorial_interactive_uitest.cc
+++ b/chrome/browser/ui/views/user_education/tutorial_interactive_uitest.cc
@@ -97,10 +97,10 @@
   UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed);
   UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted);
 
-  const bool started = GetTutorialService()->StartTutorial(
-      kTestTutorialId, browser()->window()->GetElementContext(),
-      completed.Get(), aborted.Get());
-  EXPECT_TRUE(started);
+  GetTutorialService()->StartTutorial(kTestTutorialId,
+                                      browser()->window()->GetElementContext(),
+                                      completed.Get(), aborted.Get());
+  EXPECT_TRUE(GetTutorialService()->IsRunningTutorial());
 
   ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent(
       GetElement(kTabStripElementId), kCustomEventType1);
diff --git a/chrome/browser/ui/views/user_education/views_tutorial_unittest.cc b/chrome/browser/ui/views/user_education/views_tutorial_unittest.cc
index 462b564..ccce1d5a 100644
--- a/chrome/browser/ui/views/user_education/views_tutorial_unittest.cc
+++ b/chrome/browser/ui/views/user_education/views_tutorial_unittest.cc
@@ -149,10 +149,11 @@
                           kIndicatorElementId, "", kArrow);
   tutorial_registry_.AddTutorial(kTutorialId, std::move(desc));
 
-  ASSERT_TRUE(tutorial_service_.StartTutorial(
+  tutorial_service_.StartTutorial(
       kTutorialId,
       views::ElementTrackerViews::GetContextForWidget(widget_.get()),
-      completed.Get(), aborted.Get()));
+      completed.Get(), aborted.Get());
+  ASSERT_TRUE(tutorial_service_.IsRunningTutorial());
 
   hide_button_on_press_ = true;
   views::test::InteractionTestUtilSimulatorViews::PressButton(
@@ -198,10 +199,11 @@
                           kIndicatorElementId, "", kArrow);
   tutorial_registry_.AddTutorial(kTutorialId, std::move(desc));
 
-  ASSERT_TRUE(tutorial_service_.StartTutorial(
+  tutorial_service_.StartTutorial(
       kTutorialId,
       views::ElementTrackerViews::GetContextForWidget(widget_.get()),
-      completed.Get(), aborted.Get()));
+      completed.Get(), aborted.Get());
+  ASSERT_TRUE(tutorial_service_.IsRunningTutorial());
 
   views::test::InteractionTestUtilSimulatorViews::PressButton(
       button_.get(), ui::test::InteractionTestUtil::InputType::kKeyboard);
diff --git a/chrome/browser/ui/webui/browser_command/browser_command_handler.cc b/chrome/browser/ui/webui/browser_command/browser_command_handler.cc
index e82a915..17c328ff 100644
--- a/chrome/browser/ui/webui/browser_command/browser_command_handler.cc
+++ b/chrome/browser/ui/webui/browser_command/browser_command_handler.cc
@@ -219,8 +219,9 @@
       BrowserHasTabGroups() ? kTabGroupWithExistingGroupTutorialId
                             : kTabGroupTutorialId;
 
-  bool started_tutorial = tutorial_service->StartTutorial(tutorial_id, context);
-  tutorial_service->LogStartedFromWhatsNewPage(tutorial_id, started_tutorial);
+  tutorial_service->StartTutorial(tutorial_id, context);
+  tutorial_service->LogStartedFromWhatsNewPage(
+      tutorial_id, tutorial_service->IsRunningTutorial());
 }
 
 void BrowserCommandHandler::OpenFeedbackForm() {
diff --git a/chrome/browser/ui/webui/browser_command/browser_command_handler_unittest.cc b/chrome/browser/ui/webui/browser_command/browser_command_handler_unittest.cc
index ad0ac965..9a24b97 100644
--- a/chrome/browser/ui/webui/browser_command/browser_command_handler_unittest.cc
+++ b/chrome/browser/ui/webui/browser_command/browser_command_handler_unittest.cc
@@ -130,13 +130,18 @@
     return std::u16string();
   }
 
-  bool StartTutorial(
+  void StartTutorial(
       user_education::TutorialIdentifier id,
       ui::ElementContext context,
       base::OnceClosure completed_callback = base::DoNothing(),
       base::OnceClosure aborted_callback = base::DoNothing()) override {
-    return true;
+    running_ = true;
   }
+
+  bool IsRunningTutorial() const override { return running_; }
+
+ private:
+  bool running_ = false;
 };
 
 class MockTutorialService : public TestTutorialService {
@@ -148,12 +153,13 @@
   ~MockTutorialService() override = default;
 
   MOCK_METHOD4(StartTutorial,
-               bool(user_education::TutorialIdentifier,
+               void(user_education::TutorialIdentifier,
                     ui::ElementContext,
                     base::OnceClosure,
                     base::OnceClosure));
   MOCK_METHOD2(LogStartedFromWhatsNewPage,
                void(user_education::TutorialIdentifier, bool));
+  MOCK_CONST_METHOD0(IsRunningTutorial, bool());
 };
 
 class MockCommandHandler : public TestCommandHandler {
@@ -465,7 +471,8 @@
     ClickInfoPtr info = ClickInfo::New();
     EXPECT_CALL(service, StartTutorial(kTabGroupTutorialId, kTestContext1,
                                        testing::_, testing::_))
-        .WillOnce(testing::Return(true));
+        .Times(1);
+    EXPECT_CALL(service, IsRunningTutorial).WillOnce(testing::Return(true));
     EXPECT_CALL(service, LogStartedFromWhatsNewPage(kTabGroupTutorialId, true));
     EXPECT_TRUE(
         ExecuteCommand(Command::kStartTabGroupTutorial, std::move(info)));
@@ -478,7 +485,8 @@
     ClickInfoPtr info = ClickInfo::New();
     EXPECT_CALL(service, StartTutorial(kTabGroupWithExistingGroupTutorialId,
                                        kTestContext1, testing::_, testing::_))
-        .WillOnce(testing::Return(true));
+        .Times(1);
+    EXPECT_CALL(service, IsRunningTutorial).WillOnce(testing::Return(true));
     EXPECT_CALL(service, LogStartedFromWhatsNewPage(
                              kTabGroupWithExistingGroupTutorialId, true));
     EXPECT_TRUE(
diff --git a/chrome/browser/ui/webui/internals/user_education/user_education_internals_page_handler_impl.cc b/chrome/browser/ui/webui/internals/user_education/user_education_internals_page_handler_impl.cc
index e3af8d3..ce78bb5 100644
--- a/chrome/browser/ui/webui/internals/user_education/user_education_internals_page_handler_impl.cc
+++ b/chrome/browser/ui/webui/internals/user_education/user_education_internals_page_handler_impl.cc
@@ -153,8 +153,9 @@
   const ui::ElementContext context =
       chrome::FindBrowserWithProfile(profile_)->window()->GetElementContext();
   std::string result;
-  if (!tutorial_service_->StartTutorial(tutorial_id, context)) {
-    result = "Cannot start tutorial.";
+  tutorial_service_->StartTutorial(tutorial_id, context);
+  if (!tutorial_service_->IsRunningTutorial()) {
+    result = "Failed to start tutorial " + tutorial_id;
   }
   std::move(callback).Run(result);
 }
diff --git a/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.cc b/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.cc
index ee9eb71e..f9f0cf1 100644
--- a/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.cc
+++ b/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.cc
@@ -41,6 +41,24 @@
       device_id, std::move(settings));
 }
 
+void InputDeviceSettingsProvider::SetPointingStickSettings(
+    uint32_t device_id,
+    ::ash::mojom::PointingStickSettingsPtr settings) {
+  DCHECK(features::IsInputDeviceSettingsSplitEnabled());
+  DCHECK(InputDeviceSettingsController::Get());
+  InputDeviceSettingsController::Get()->SetPointingStickSettings(
+      device_id, std::move(settings));
+}
+
+void InputDeviceSettingsProvider::SetMouseSettings(
+    uint32_t device_id,
+    ::ash::mojom::MouseSettingsPtr settings) {
+  DCHECK(features::IsInputDeviceSettingsSplitEnabled());
+  DCHECK(InputDeviceSettingsController::Get());
+  InputDeviceSettingsController::Get()->SetMouseSettings(device_id,
+                                                         std::move(settings));
+}
+
 void InputDeviceSettingsProvider::GetConnectedKeyboards(
     GetConnectedKeyboardsCallback callback) {
   DCHECK(features::IsInputDeviceSettingsSplitEnabled());
diff --git a/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.h b/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.h
index f435ec94..8531f9b 100644
--- a/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.h
+++ b/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.h
@@ -41,6 +41,11 @@
       mojo::PendingRemote<mojom::MouseSettingsObserver> observer) override;
   void SetKeyboardSettings(uint32_t device_id,
                            ::ash::mojom::KeyboardSettingsPtr settings) override;
+  void SetPointingStickSettings(
+      uint32_t device_id,
+      ::ash::mojom::PointingStickSettingsPtr settings) override;
+  void SetMouseSettings(uint32_t device_id,
+                        ::ash::mojom::MouseSettingsPtr settings) override;
 
   // InputDeviceSettingsController::Observer:
   void OnKeyboardConnected(const ::ash::mojom::Keyboard& keyboard) override;
diff --git a/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.mojom b/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.mojom
index 52c7b427..ff429f92 100644
--- a/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.mojom
+++ b/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.mojom
@@ -61,4 +61,9 @@
   ObserveMouseSettings(pending_remote<MouseSettingsObserver> observer);
   // Sets the keyboard settings based on its id.
   SetKeyboardSettings(uint32 device_id, ash.mojom.KeyboardSettings settings);
+  // Sets the point stick settings based on its id.
+  SetPointingStickSettings(
+      uint32 device_id, ash.mojom.PointingStickSettings settings);
+  // Sets the mouse settings based on its id.
+  SetMouseSettings(uint32 device_id, ash.mojom.MouseSettings settings);
 };
diff --git a/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider_unittest.cc b/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider_unittest.cc
index 1cbc38ff..55da01d 100644
--- a/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider_unittest.cc
+++ b/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider_unittest.cc
@@ -214,10 +214,14 @@
       DeviceId id,
       ::ash::mojom::TouchpadSettingsPtr settings) override {}
   void SetMouseSettings(DeviceId id,
-                        ::ash::mojom::MouseSettingsPtr settings) override {}
+                        ::ash::mojom::MouseSettingsPtr settings) override {
+    ++num_times_set_mouse_settings_called_;
+  }
   void SetPointingStickSettings(
       DeviceId id,
-      ::ash::mojom::PointingStickSettingsPtr settings) override {}
+      ::ash::mojom::PointingStickSettingsPtr settings) override {
+    ++num_times_set_pointing_stick_settings_called_;
+  }
 
   void AddKeyboard(::ash::mojom::KeyboardPtr keyboard) {
     keyboards_.push_back(std::move(keyboard));
@@ -285,6 +289,12 @@
   int num_times_set_keyboard_settings_called() {
     return num_times_set_keyboard_settings_called_;
   }
+  int num_times_set_pointing_stick_settings_called() {
+    return num_times_set_pointing_stick_settings_called_;
+  }
+  int num_times_set_mouse_settings_called() {
+    return num_times_set_mouse_settings_called_;
+  }
 
  private:
   std::vector<::ash::mojom::KeyboardPtr> keyboards_;
@@ -293,6 +303,8 @@
   std::vector<::ash::mojom::PointingStickPtr> pointing_sticks_;
   raw_ptr<InputDeviceSettingsController::Observer> observer_ = nullptr;
   int num_times_set_keyboard_settings_called_ = 0;
+  int num_times_set_pointing_stick_settings_called_ = 0;
+  int num_times_set_mouse_settings_called_ = 0;
 };
 
 }  // namespace
@@ -352,6 +364,36 @@
   EXPECT_EQ(2, controller_->num_times_set_keyboard_settings_called());
 }
 
+TEST_F(InputDeviceSettingsProviderTest, TestSetPointingStickSettings) {
+  controller_->AddPointingStick(kPointingStick1.Clone());
+  provider_->SetPointingStickSettings(kPointingStick1.id,
+                                      kPointingStick1.settings->Clone());
+
+  base::RunLoop().RunUntilIdle();
+  EXPECT_EQ(1, controller_->num_times_set_pointing_stick_settings_called());
+
+  controller_->AddPointingStick(kPointingStick2.Clone());
+  provider_->SetPointingStickSettings(kPointingStick2.id,
+                                      kPointingStick1.settings->Clone());
+
+  base::RunLoop().RunUntilIdle();
+  EXPECT_EQ(2, controller_->num_times_set_pointing_stick_settings_called());
+}
+
+TEST_F(InputDeviceSettingsProviderTest, TestSetMouseSettings) {
+  controller_->AddMouse(kMouse1.Clone());
+  provider_->SetMouseSettings(kMouse1.id, kMouse1.settings->Clone());
+
+  base::RunLoop().RunUntilIdle();
+  EXPECT_EQ(1, controller_->num_times_set_mouse_settings_called());
+
+  controller_->AddMouse(kMouse2.Clone());
+  provider_->SetMouseSettings(kMouse2.id, kMouse1.settings->Clone());
+
+  base::RunLoop().RunUntilIdle();
+  EXPECT_EQ(2, controller_->num_times_set_mouse_settings_called());
+}
+
 TEST_F(InputDeviceSettingsProviderTest, TestKeyboardSettingsObeserver) {
   std::vector<::ash::mojom::KeyboardPtr> expected_keyboards;
   expected_keyboards.push_back(kKeyboard1.Clone());
diff --git a/chrome/browser/ui/webui/side_panel/read_anything/read_anything_ui.cc b/chrome/browser/ui/webui/side_panel/read_anything/read_anything_ui.cc
index b8e6995e..6ffc440 100644
--- a/chrome/browser/ui/webui/side_panel/read_anything/read_anything_ui.cc
+++ b/chrome/browser/ui/webui/side_panel/read_anything/read_anything_ui.cc
@@ -33,6 +33,7 @@
       {"readAnythingTabTitle", IDS_READING_MODE_TITLE},
       {"emptyStateHeader", IDS_READING_MODE_EMPTY_STATE_HEADER},
       {"emptyStateSubheader", IDS_READING_MODE_EMPTY_STATE_SUBHEADER},
+      {"readAnythingLoadingMessage", IDS_READ_ANYTHING_LOADING},
   };
   for (const auto& str : kLocalizedStrings)
     webui::AddLocalizedString(source, str.name, str.id);
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 74321fa1..2fe8b92a 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1678391859-7e800eff6c1857fe3d22260194bc4394b5b36fc1.profdata
+chrome-mac-arm-main-1678406398-665c7ee9471649e8f179036c5c41bb0c29fb3c46.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 71e5324..faaea23b 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1678384789-1eea5759f041a9f55459e1641218ffd1db75330b.profdata
+chrome-win32-main-1678395580-5ce0b858a568fd95838528e0ff77940b3a4d1ef6.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 8a23851..6fe1bcf43 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1678373835-19315deb83b25609d86dc667bf2ac3470c3afb02.profdata
+chrome-win64-main-1678384789-e53b532a015daff2f9f2708d571a3e6ba2f04dbb.profdata
diff --git a/chrome/common/BUILD.gn b/chrome/common/BUILD.gn
index b55dbfe4..a30b315 100644
--- a/chrome/common/BUILD.gn
+++ b/chrome/common/BUILD.gn
@@ -710,10 +710,7 @@
   # linkage and will correctly emplace over the correct symbols in
   # delayimp.lib at link time.
   source_set("delay_load_support") {
-    sources = [
-      "win/delay_load_failure_hook.cc",
-      "win/delay_load_failure_hook.h",
-    ]
+    sources = [ "win/delay_load_failure_hook.cc" ]
 
     deps = [ "//base" ]
   }
diff --git a/chrome/common/extensions/permissions/chrome_api_permissions.cc b/chrome/common/extensions/permissions/chrome_api_permissions.cc
index 4b62591..fff26f3 100644
--- a/chrome/common/extensions/permissions/chrome_api_permissions.cc
+++ b/chrome/common/extensions/permissions/chrome_api_permissions.cc
@@ -151,8 +151,6 @@
      APIPermissionInfo::kFlagCannotBeOptional},
     {APIPermissionID::kEnterpriseReportingPrivate,
      "enterprise.reportingPrivate", APIPermissionInfo::kFlagCannotBeOptional},
-    {APIPermissionID::kFileBrowserHandlerInternal, "fileBrowserHandlerInternal",
-     APIPermissionInfo::kFlagCannotBeOptional},
     {APIPermissionID::kFileManagerPrivate, "fileManagerPrivate",
      APIPermissionInfo::kFlagCannotBeOptional},
     {APIPermissionID::kIdentityPrivate, "identityPrivate",
diff --git a/chrome/common/extensions/permissions/chrome_api_permissions_unittest.cc b/chrome/common/extensions/permissions/chrome_api_permissions_unittest.cc
index 851d40a..f0f6d610 100644
--- a/chrome/common/extensions/permissions/chrome_api_permissions_unittest.cc
+++ b/chrome/common/extensions/permissions/chrome_api_permissions_unittest.cc
@@ -26,13 +26,8 @@
   ASSERT_EQ(1u, all_api_permissions.count(mojom::APIPermissionID::kStorage));
 
   std::string kKnownBad[] = {
-      "bookmarkManagerPrivate",
-      "fileBrowserHandlerInternal",
-      "homepage",
-      "searchProvider",
-      "startupPages",
-      "tabCaptureForTab",
-      "newTabPageOverride",
+      "homepage",         "searchProvider",     "startupPages",
+      "tabCaptureForTab", "newTabPageOverride",
   };
 
   const FeatureProvider* permission_features =
diff --git a/chrome/common/extensions/permissions/permission_set_unittest.cc b/chrome/common/extensions/permissions/permission_set_unittest.cc
index 94fa3a6..266e61d 100644
--- a/chrome/common/extensions/permissions/permission_set_unittest.cc
+++ b/chrome/common/extensions/permissions/permission_set_unittest.cc
@@ -835,7 +835,6 @@
   skip.insert(APIPermissionID::kEchoPrivate);
   skip.insert(APIPermissionID::kEnterprisePlatformKeysPrivate);
   skip.insert(APIPermissionID::kFeedbackPrivate);
-  skip.insert(APIPermissionID::kFileBrowserHandlerInternal);
   skip.insert(APIPermissionID::kFileManagerPrivate);
   skip.insert(APIPermissionID::kFirstRunPrivate);
   skip.insert(APIPermissionID::kSharedStoragePrivate);
@@ -1757,16 +1756,6 @@
   EXPECT_FALSE(perm_set->IsEmpty());
 }
 
-TEST(PermissionsTest, ImpliedPermissions) {
-  APIPermissionSet apis;
-  apis.insert(APIPermissionID::kFileBrowserHandler);
-  EXPECT_EQ(1U, apis.size());
-
-  PermissionSet perm_set(std::move(apis), ManifestPermissionSet(),
-                         URLPatternSet(), URLPatternSet());
-  EXPECT_EQ(2U, perm_set.apis().size());
-}
-
 TEST(PermissionsTest, SyncFileSystemPermission) {
   scoped_refptr<Extension> extension = LoadManifest(
       "permissions", "sync_file_system.json");
diff --git a/chrome/common/win/delay_load_failure_hook.cc b/chrome/common/win/delay_load_failure_hook.cc
index b8ec34d7..91e28a9 100644
--- a/chrome/common/win/delay_load_failure_hook.cc
+++ b/chrome/common/win/delay_load_failure_hook.cc
@@ -15,15 +15,10 @@
 
 namespace {
 
-bool g_hooks_enabled = true;
-
 // Delay load failure hook that generates a crash report. By default a failure
 // to delay load will trigger an exception handled by the delay load runtime and
 // this won't generate a crash report.
 FARPROC WINAPI DelayLoadFailureHook(unsigned reason, DelayLoadInfo* dll_info) {
-  if (!g_hooks_enabled)
-    return 0;
-
   char dll_name[MAX_PATH];
   base::strlcpy(dll_name, dll_info->szDll, std::size(dll_name));
   // It's not an error if "bthprops.cpl" fails to be loaded, there's a custom
@@ -42,10 +37,6 @@
 
 }  // namespace
 
-void DisableDelayLoadFailureHooksForCurrentModule() {
-  g_hooks_enabled = false;
-}
-
 }  // namespace chrome
 
 // Set the delay load failure hook to the function above.
diff --git a/chrome/common/win/delay_load_failure_hook.h b/chrome/common/win/delay_load_failure_hook.h
deleted file mode 100644
index f12bf11..0000000
--- a/chrome/common/win/delay_load_failure_hook.h
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2022 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROME_COMMON_WIN_DELAY_LOAD_FAILURE_HOOK_H_
-#define CHROME_COMMON_WIN_DELAY_LOAD_FAILURE_HOOK_H_
-
-namespace chrome {
-
-// This should be called early in process startup (before any delay load
-// failures) to disable the delay load hooks for the current module.
-void DisableDelayLoadFailureHooksForCurrentModule();
-
-}  // namespace chrome
-
-#endif  // CHROME_COMMON_WIN_DELAY_LOAD_FAILURE_HOOK_H_
diff --git a/chrome/common/win/delay_load_failure_hook_unittest.cc b/chrome/common/win/delay_load_failure_hook_unittest.cc
index 8165ad8a..8801398f 100644
--- a/chrome/common/win/delay_load_failure_hook_unittest.cc
+++ b/chrome/common/win/delay_load_failure_hook_unittest.cc
@@ -6,10 +6,26 @@
 
 #include <delayimp.h>
 
+#include "base/test/gtest_util.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 TEST(ChromeDelayLoadHookTest, HooksAreSetAtLinkTime) {
   // This test verifies that delay load hooks are correctly in place for the
   // current module.
-  EXPECT_NE(__pfnDliFailureHook2, nullptr);
+  ASSERT_NE(__pfnDliFailureHook2, nullptr);
+
+  // Subtle: In tests, unit_tests.exe is linked with
+  // chrome/common/win/delay_load_failure_hook.cc not
+  // chrome/app/delay_load_failure_hook_win.cc. So, __pfnDliFailureHook2 will
+  // always call DelayLoadFailureHook and not DelayLoadFailureHookEXE despite
+  // existing in unit_tests.exe.
+  //
+  // In production chrome.exe, __pfnDliFailureHook2 is instead backed by
+  // DelayLoadFailureHookEXE in chrome/app/delay_load_failure_hook_win.cc, while
+  // in chrome.dll, __pfnDliFailureHook2 is backed by DelayLoadFailureHook from
+  // chrome/common/win/delay_load_failure_hook.cc.
+  //
+  // This test verifies DelayLoadFailureHook crashes.
+  DelayLoadInfo dli = {.szDll = "test.dll"};
+  EXPECT_CHECK_DEATH({ __pfnDliFailureHook2(dliFailLoadLib, &dli); });
 }
diff --git a/chrome/renderer/accessibility/read_anything_app_controller.cc b/chrome/renderer/accessibility/read_anything_app_controller.cc
index 0326a58a..19a30ace 100644
--- a/chrome/renderer/accessibility/read_anything_app_controller.cc
+++ b/chrome/renderer/accessibility/read_anything_app_controller.cc
@@ -415,6 +415,10 @@
   ui::AXTreeSerializer<const ui::AXNode*> serializer(tree_source.get());
   ui::AXTreeUpdate snapshot;
   CHECK(serializer.SerializeChanges(tree->root(), &snapshot));
+  // TODO(b/1266555): Use v8::Function rather than javascript. If possible,
+  // replace this function call with firing an event.
+  std::string script = "chrome.readAnything.showLoading();";
+  render_frame_->ExecuteJavaScript(base::ASCIIToUTF16(script));
   model_.SetDistillationInProgress(true);
   distiller_->Distill(*tree, snapshot, model_.active_ukm_source_id());
 }
diff --git a/chrome/services/speech/speech_recognition_recognizer_impl.cc b/chrome/services/speech/speech_recognition_recognizer_impl.cc
index b705029..734ea2e 100644
--- a/chrome/services/speech/speech_recognition_recognizer_impl.cc
+++ b/chrome/services/speech/speech_recognition_recognizer_impl.cc
@@ -13,6 +13,7 @@
 #include "base/files/file_util.h"
 #include "base/functional/bind.h"
 #include "base/metrics/histogram_functions.h"
+#include "base/strings/strcat.h"
 #include "base/strings/string_util.h"
 #include "base/task/bind_post_task.h"
 #include "base/task/sequenced_task_runner.h"
@@ -121,6 +122,10 @@
 }  // namespace
 
 SpeechRecognitionRecognizerImpl::~SpeechRecognitionRecognizerImpl() {
+  base::UmaHistogramBoolean(
+      base::StrCat({"Accessibility.LiveCaption.", primary_language_name_,
+                    ".SessionContainsRecognizedSpeech"}),
+      session_contains_speech_);
   RecordDuration();
   soda_client_.reset();
 }
@@ -145,6 +150,10 @@
 
 void SpeechRecognitionRecognizerImpl::OnRecognitionEvent(
     media::SpeechRecognitionResult event) {
+  if (!event.transcription.empty()) {
+    session_contains_speech_ = true;
+  }
+
   if (!client_remote_.is_bound())
     return;
 
diff --git a/chrome/services/speech/speech_recognition_recognizer_impl.h b/chrome/services/speech/speech_recognition_recognizer_impl.h
index af9fc99..d2d33ec6 100644
--- a/chrome/services/speech/speech_recognition_recognizer_impl.h
+++ b/chrome/services/speech/speech_recognition_recognizer_impl.h
@@ -160,6 +160,10 @@
   // Whether the client is still requesting speech recognition.
   bool is_client_requesting_speech_recognition_ = true;
 
+  // Whether the speech recognition session contains any recognized speech. Used
+  // for logging purposes only.
+  bool session_contains_speech_ = false;
+
   scoped_refptr<base::SequencedTaskRunner> task_runner_;
 
   base::WeakPtrFactory<SpeechRecognitionRecognizerImpl> weak_factory_{this};
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 07e2ef6..0a0f2dc 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -2134,7 +2134,6 @@
       "../browser/sessions/session_restore_observer_browsertest.cc",
       "../browser/sessions/session_service_log_browsertest.cc",
       "../browser/sessions/tab_restore_browsertest.cc",
-      "../browser/shared_highlighting/shared_highlighting_browsertest.cc",
       "../browser/signin/e2e_tests/account_capabilities_observer.cc",
       "../browser/signin/e2e_tests/account_capabilities_observer.h",
       "../browser/signin/e2e_tests/accounts_removed_waiter.cc",
@@ -5822,12 +5821,6 @@
         [ "../browser/policy/browser_dm_token_storage_linux_unittest.cc" ]
   }
 
-  if (is_chromeos) {
-    sources += [
-      "../browser/crash_upload_list/crash_upload_list_chromeos_unittest.cc",
-    ]
-  }
-
   if (is_chromeos_ash) {
     sources += [
       "../browser/metrics/chrome_metrics_service_client_ash_unittest.cc",
@@ -5954,6 +5947,7 @@
     "//chrome/browser/breadcrumbs",
     "//chrome/browser/breadcrumbs:unit_tests",
     "//chrome/browser/browsing_data:constants",
+    "//chrome/browser/crash_upload_list:unit_tests",
     "//chrome/browser/devtools",
     "//chrome/browser/dips:unit_tests",
     "//chrome/browser/enterprise/identifiers",
diff --git a/chrome/test/data/extensions/api_test/history/incognito/incognito.js b/chrome/test/data/extensions/api_test/history/incognito/incognito.js
index 2752330a..7253b6ee 100644
--- a/chrome/test/data/extensions/api_test/history/incognito/incognito.js
+++ b/chrome/test/data/extensions/api_test/history/incognito/incognito.js
@@ -7,14 +7,14 @@
 
 function addItem() {
   chrome.history.addUrl({url: 'http://www.a.com/'}, function() {
-    window.domAutomationController.send('success');
+    chrome.test.sendScriptResult('success');
   });
 }
 
 function countItemsInHistory() {
   var query = {'text': ''};
   chrome.history.search(query, function(results) {
-    window.domAutomationController.send(results.length.toString());
+    chrome.test.sendScriptResult(results.length.toString());
   });
 }
 
diff --git a/chrome/test/data/extensions/api_test/history/incognito/manifest.json b/chrome/test/data/extensions/api_test/history/incognito/manifest.json
index 44b23b7..e0065826a 100644
--- a/chrome/test/data/extensions/api_test/history/incognito/manifest.json
+++ b/chrome/test/data/extensions/api_test/history/incognito/manifest.json
@@ -6,6 +6,7 @@
   "permissions": ["history"],
   "incognito": "split",
   "background": {
-    "scripts": ["incognito.js"]
+    "scripts": ["incognito.js"],
+    "persistent": true
   }
 }
diff --git a/chrome/test/data/extensions/api_test/history/regular/a.html b/chrome/test/data/extensions/api_test/history/regular/a.html
deleted file mode 100644
index a8791cba..0000000
--- a/chrome/test/data/extensions/api_test/history/regular/a.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!--
-Copyright 2011 The Chromium Authors
-Use of this source code is governed by a BSD-style license that can be
-found in the LICENSE file.
--->
-<html>
-<head>
-  <title>www.a.com</title>
-</head>
-<body>
-  1234567890
-</body>
-</html>
diff --git a/chrome/test/data/extensions/api_test/history/regular/b.html b/chrome/test/data/extensions/api_test/history/regular/b.html
deleted file mode 100644
index 5afddb55..0000000
--- a/chrome/test/data/extensions/api_test/history/regular/b.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!--
-Copyright 2011 The Chromium Authors
-Use of this source code is governed by a BSD-style license that can be
-found in the LICENSE file.
--->
-<html>
-<head>
-  <title>www.b.com</title>
-</head>
-<body>
-  ABCDEFGHIJKLMNOPQRSTUVWXYZ
-</body>
-</html>
diff --git a/chrome/test/data/extensions/api_test/history/regular/common.js b/chrome/test/data/extensions/api_test/history/regular/common.js
index 0768f11..6efaa93 100644
--- a/chrome/test/data/extensions/api_test/history/regular/common.js
+++ b/chrome/test/data/extensions/api_test/history/regular/common.js
@@ -10,12 +10,6 @@
 var GOOGLE_URL = 'http://www.google.com/';
 var PICASA_URL = 'http://www.picasa.com/';
 
-// PORT will be changed to the port of the test server.
-var A_RELATIVE_URL =
-    'http://www.a.com:PORT/extensions/api_test/history/a.html';
-var B_RELATIVE_URL =
-    'http://www.b.com:PORT/extensions/api_test/history/b.html';
-
 /**
  * Object used for listening to the chrome.history.onVisited events.  The
  * global object 'itemVisitedCallback' stores the last item received.
@@ -108,15 +102,7 @@
  * @param {Array<function>} testFns The tests to run.
  */
 function runHistoryTestFns(testFns) {
-  chrome.test.getConfig(function(config) {
-    var fixPort = function(url) {
-      return url.replace(/PORT/, config.testServer.port);
-    };
-    A_RELATIVE_URL = fixPort(A_RELATIVE_URL);
-    B_RELATIVE_URL = fixPort(B_RELATIVE_URL);
-
-    chrome.test.runTests(testFns);
-  });
+  chrome.test.runTests(testFns);
 }
 
 /**
diff --git a/chrome/test/data/extensions/api_test/history/regular/delete.html b/chrome/test/data/extensions/api_test/history/regular/delete.html
deleted file mode 100644
index b0983ab..0000000
--- a/chrome/test/data/extensions/api_test/history/regular/delete.html
+++ /dev/null
@@ -1,7 +0,0 @@
-<!--
-Copyright 2011 The Chromium Authors
-Use of this source code is governed by a BSD-style license that can be
-found in the LICENSE file.
--->
-<script src="common.js"></script>
-<script src="delete.js"></script>
diff --git a/chrome/test/data/extensions/api_test/history/regular/delete.js b/chrome/test/data/extensions/api_test/history/regular/delete/delete.js
similarity index 96%
rename from chrome/test/data/extensions/api_test/history/regular/delete.js
rename to chrome/test/data/extensions/api_test/history/regular/delete/delete.js
index 2270a173..c2f89b4 100644
--- a/chrome/test/data/extensions/api_test/history/regular/delete.js
+++ b/chrome/test/data/extensions/api_test/history/regular/delete/delete.js
@@ -5,8 +5,11 @@
 // History api test for Chrome.
 // browser_tests.exe --gtest_filter=HistoryApiTest.Delete
 
-// runHistoryTestFns is defined in ./common.js .
-runHistoryTestFns([
+const scriptUrl = '_test_resources/api_test/history/regular/common.js';
+let loadScript = chrome.test.loadScript(scriptUrl);
+
+loadScript.then(async function() {
+chrome.test.runTests([
   // All the tests require a blank state to start from.  If this fails,
   // expect all other history tests (HistoryExtensionApiTest.*) to fail.
   function clearHistory() {
@@ -172,4 +175,4 @@
       });
     });
   }
-]);
+])});
diff --git a/chrome/test/data/extensions/api_test/history/regular/delete/manifest.json b/chrome/test/data/extensions/api_test/history/regular/delete/manifest.json
new file mode 100644
index 0000000..5a7217f
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/history/regular/delete/manifest.json
@@ -0,0 +1,11 @@
+{
+  "name": "chrome.history",
+  "version": "0.1",
+  "manifest_version": 2,
+  "description": "end-to-end browser test for chrome.history API",
+  "permissions": ["history"],
+  "background": {
+    "scripts": ["delete.js"],
+    "persistent": true
+  }
+}
diff --git a/chrome/test/data/extensions/api_test/history/regular/delete_prohibited.html b/chrome/test/data/extensions/api_test/history/regular/delete_prohibited.html
deleted file mode 100644
index cf92004a..0000000
--- a/chrome/test/data/extensions/api_test/history/regular/delete_prohibited.html
+++ /dev/null
@@ -1,7 +0,0 @@
-<!--
-Copyright 2013 The Chromium Authors
-Use of this source code is governed by a BSD-style license that can be
-found in the LICENSE file.
--->
-<script src="common.js"></script>
-<script src="delete_prohibited.js"></script>
diff --git a/chrome/test/data/extensions/api_test/history/regular/delete_prohibited.js b/chrome/test/data/extensions/api_test/history/regular/delete_prohibited/delete_prohibited.js
similarity index 93%
rename from chrome/test/data/extensions/api_test/history/regular/delete_prohibited.js
rename to chrome/test/data/extensions/api_test/history/regular/delete_prohibited/delete_prohibited.js
index e1b0c82..9a268f0 100644
--- a/chrome/test/data/extensions/api_test/history/regular/delete_prohibited.js
+++ b/chrome/test/data/extensions/api_test/history/regular/delete_prohibited/delete_prohibited.js
@@ -61,8 +61,11 @@
   }));
 }
 
-// runHistoryTestFns is defined in ./common.js .
-runHistoryTestFns([
+const scriptUrl = '_test_resources/api_test/history/regular/common.js';
+let loadScript = chrome.test.loadScript(scriptUrl);
+
+loadScript.then(async function() {
+chrome.test.runTests([
   function deleteUrl() {
     verifyNoDeletion(function(callback) {
       chrome.history.deleteUrl({ 'url': GOOGLE_URL }, callback);
@@ -80,4 +83,4 @@
   function deleteAll() {
     verifyNoDeletion(chrome.history.deleteAll);
   }
-]);
+])});
diff --git a/chrome/test/data/extensions/api_test/history/regular/delete_prohibited/manifest.json b/chrome/test/data/extensions/api_test/history/regular/delete_prohibited/manifest.json
new file mode 100644
index 0000000..64b163b
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/history/regular/delete_prohibited/manifest.json
@@ -0,0 +1,11 @@
+{
+  "name": "chrome.history",
+  "version": "0.1",
+  "manifest_version": 2,
+  "description": "end-to-end browser test for chrome.history API",
+  "permissions": ["history"],
+  "background": {
+    "scripts": ["delete_prohibited.js"],
+    "persistent": true
+  }
+}
diff --git a/chrome/test/data/extensions/api_test/history/regular/get_visits.html b/chrome/test/data/extensions/api_test/history/regular/get_visits.html
deleted file mode 100644
index 78950377..0000000
--- a/chrome/test/data/extensions/api_test/history/regular/get_visits.html
+++ /dev/null
@@ -1,7 +0,0 @@
-<!--
-Copyright 2011 The Chromium Authors
-Use of this source code is governed by a BSD-style license that can be
-found in the LICENSE file.
--->
-<script src="common.js"></script>
-<script src="get_visits.js"></script>
diff --git a/chrome/test/data/extensions/api_test/history/regular/get_visits.js b/chrome/test/data/extensions/api_test/history/regular/get_visits/get_visits.js
similarity index 84%
rename from chrome/test/data/extensions/api_test/history/regular/get_visits.js
rename to chrome/test/data/extensions/api_test/history/regular/get_visits/get_visits.js
index a33c344..fcbda3df 100644
--- a/chrome/test/data/extensions/api_test/history/regular/get_visits.js
+++ b/chrome/test/data/extensions/api_test/history/regular/get_visits/get_visits.js
@@ -5,8 +5,11 @@
 // History api test for Chrome.
 // browser_tests.exe --gtest_filter=HistoryExtensionApiTest.GetVisits
 
-// runHistoryTestFns is defined in ./common.js .
-runHistoryTestFns([
+const scriptUrl = '_test_resources/api_test/history/regular/common.js';
+let loadScript = chrome.test.loadScript(scriptUrl);
+
+loadScript.then(async function() {
+chrome.test.runTests([
   function getVisits() {
     // getVisits callback.
     function getVisitsTestVerification() {
@@ -34,4 +37,4 @@
       populateHistory([GOOGLE_URL], function() { });
     });
   }
-]);
+])});
diff --git a/chrome/test/data/extensions/api_test/history/regular/get_visits/manifest.json b/chrome/test/data/extensions/api_test/history/regular/get_visits/manifest.json
new file mode 100644
index 0000000..98229a35
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/history/regular/get_visits/manifest.json
@@ -0,0 +1,11 @@
+{
+  "name": "chrome.history",
+  "version": "0.1",
+  "manifest_version": 2,
+  "description": "end-to-end browser test for chrome.history API",
+  "permissions": ["history"],
+  "background": {
+    "scripts": ["get_visits.js"],
+    "persistent": true
+  }
+}
diff --git a/chrome/test/data/extensions/api_test/history/regular/manifest.json b/chrome/test/data/extensions/api_test/history/regular/manifest.json
deleted file mode 100644
index 78829e79..0000000
--- a/chrome/test/data/extensions/api_test/history/regular/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "name": "chrome.history",
-  "version": "0.1",
-  "manifest_version": 2,
-  "description": "end-to-end browser test for chrome.history API",
-  "permissions": ["history", "tabs"]
-}
diff --git a/chrome/test/data/extensions/api_test/history/regular/misc_search.html b/chrome/test/data/extensions/api_test/history/regular/misc_search.html
deleted file mode 100644
index 631169b..0000000
--- a/chrome/test/data/extensions/api_test/history/regular/misc_search.html
+++ /dev/null
@@ -1,7 +0,0 @@
-<!--
-Copyright 2011 The Chromium Authors
-Use of this source code is governed by a BSD-style license that can be
-found in the LICENSE file.
--->
-<script src="common.js"></script>
-<script src="misc_search.js"></script>
diff --git a/chrome/test/data/extensions/api_test/history/regular/misc_search/manifest.json b/chrome/test/data/extensions/api_test/history/regular/misc_search/manifest.json
new file mode 100644
index 0000000..bb05630
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/history/regular/misc_search/manifest.json
@@ -0,0 +1,11 @@
+{
+  "name": "chrome.history",
+  "version": "0.1",
+  "manifest_version": 2,
+  "description": "end-to-end browser test for chrome.history API",
+  "permissions": ["history"],
+  "background": {
+    "scripts": ["misc_search.js"],
+    "persistent": true
+  }
+}
diff --git a/chrome/test/data/extensions/api_test/history/regular/misc_search.js b/chrome/test/data/extensions/api_test/history/regular/misc_search/misc_search.js
similarity index 89%
rename from chrome/test/data/extensions/api_test/history/regular/misc_search.js
rename to chrome/test/data/extensions/api_test/history/regular/misc_search/misc_search.js
index b0714bc..00d78cb 100644
--- a/chrome/test/data/extensions/api_test/history/regular/misc_search.js
+++ b/chrome/test/data/extensions/api_test/history/regular/misc_search/misc_search.js
@@ -5,8 +5,12 @@
 // History api test for Chrome.
 // browser_tests.exe --gtest_filter=HistoryExtensionApiTest.MiscSearch
 
-// runHistoryTestFns is defined in ./common.js .
-runHistoryTestFns([
+
+const scriptUrl = '_test_resources/api_test/history/regular/common.js';
+let loadScript = chrome.test.loadScript(scriptUrl);
+
+loadScript.then(async function() {
+chrome.test.runTests([
   function basicSearch() {
     // basicSearch callback.
     function basicSearchTestVerification() {
@@ -56,4 +60,4 @@
       populateHistory(urls, function() { });
     });
   },
-]);
+])});
diff --git a/chrome/test/data/extensions/api_test/history/regular/search_after_add.html b/chrome/test/data/extensions/api_test/history/regular/search_after_add.html
deleted file mode 100644
index 3dc215d2..0000000
--- a/chrome/test/data/extensions/api_test/history/regular/search_after_add.html
+++ /dev/null
@@ -1,7 +0,0 @@
-<!--
-Copyright 2011 The Chromium Authors
-Use of this source code is governed by a BSD-style license that can be
-found in the LICENSE file.
--->
-<script src="common.js"></script>
-<script src="search_after_add.js"></script>
diff --git a/chrome/test/data/extensions/api_test/history/regular/search_after_add/manifest.json b/chrome/test/data/extensions/api_test/history/regular/search_after_add/manifest.json
new file mode 100644
index 0000000..b23e61c
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/history/regular/search_after_add/manifest.json
@@ -0,0 +1,11 @@
+{
+  "name": "chrome.history",
+  "version": "0.1",
+  "manifest_version": 2,
+  "description": "end-to-end browser test for chrome.history API",
+  "permissions": ["history"],
+  "background": {
+    "scripts": ["search_after_add.js"],
+    "persistent": true
+  }
+}
diff --git a/chrome/test/data/extensions/api_test/history/regular/search_after_add.js b/chrome/test/data/extensions/api_test/history/regular/search_after_add/search_after_add.js
similarity index 77%
rename from chrome/test/data/extensions/api_test/history/regular/search_after_add.js
rename to chrome/test/data/extensions/api_test/history/regular/search_after_add/search_after_add.js
index 7c9b0ac..1d696cd 100644
--- a/chrome/test/data/extensions/api_test/history/regular/search_after_add.js
+++ b/chrome/test/data/extensions/api_test/history/regular/search_after_add/search_after_add.js
@@ -5,8 +5,11 @@
 // History api test for Chrome.
 // browser_tests.exe --gtest_filter=HistoryExtensionApiTest.SearchAfterAdd
 
-// runHistoryTestFns is defined in ./common.js .
-runHistoryTestFns([
+const scriptUrl = '_test_resources/api_test/history/regular/common.js';
+let loadScript = chrome.test.loadScript(scriptUrl);
+
+loadScript.then(async function() {
+chrome.test.runTests([
   function searchAfterAdd() {
     chrome.history.deleteAll(function() {
       var VALID_URL = 'http://www.google.com/';
@@ -19,4 +22,4 @@
       });
     });
   }
-]);
+])});
diff --git a/chrome/test/data/extensions/api_test/history/regular/timed_search.html b/chrome/test/data/extensions/api_test/history/regular/timed_search.html
deleted file mode 100644
index 1b8eec3..0000000
--- a/chrome/test/data/extensions/api_test/history/regular/timed_search.html
+++ /dev/null
@@ -1,7 +0,0 @@
-<!--
-Copyright 2011 The Chromium Authors
-Use of this source code is governed by a BSD-style license that can be
-found in the LICENSE file.
--->
-<script src="common.js"></script>
-<script src="timed_search.js"></script>
diff --git a/chrome/test/data/extensions/api_test/history/regular/timed_search/manifest.json b/chrome/test/data/extensions/api_test/history/regular/timed_search/manifest.json
new file mode 100644
index 0000000..892da398
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/history/regular/timed_search/manifest.json
@@ -0,0 +1,11 @@
+{
+  "name": "chrome.history",
+  "version": "0.1",
+  "manifest_version": 2,
+  "description": "end-to-end browser test for chrome.history API",
+  "permissions": ["history"],
+  "background": {
+    "scripts": ["timed_search.js"],
+    "persistent": true
+  }
+}
diff --git a/chrome/test/data/extensions/api_test/history/regular/timed_search.js b/chrome/test/data/extensions/api_test/history/regular/timed_search/timed_search.js
similarity index 93%
rename from chrome/test/data/extensions/api_test/history/regular/timed_search.js
rename to chrome/test/data/extensions/api_test/history/regular/timed_search/timed_search.js
index aab07e7..dffb1f36 100644
--- a/chrome/test/data/extensions/api_test/history/regular/timed_search.js
+++ b/chrome/test/data/extensions/api_test/history/regular/timed_search/timed_search.js
@@ -5,8 +5,11 @@
 // History api test for Chrome.
 // browser_tests.exe --gtest_filter=HistoryExtensionApiTest.TimedSearch
 
-// runHistoryTestFns is defined in ./common.js .
-runHistoryTestFns([
+const scriptUrl = '_test_resources/api_test/history/regular/common.js';
+let loadScript = chrome.test.loadScript(scriptUrl);
+
+loadScript.then(async function() {
+chrome.test.runTests([
   // Give time epochs x,y,z and history events A,B which occur in the sequence
   // x A y B z, test that searching in [x,y] finds only A.
   function timeScopedSearchStartRange() {
@@ -78,4 +81,4 @@
       });
     });
   }
-]);
+])});
diff --git a/chrome/test/data/webui/BUILD.gn b/chrome/test/data/webui/BUILD.gn
index 41e19df2..58e95d1 100644
--- a/chrome/test/data/webui/BUILD.gn
+++ b/chrome/test/data/webui/BUILD.gn
@@ -236,6 +236,7 @@
         "chromeos/firmware_update/firmware_update_browsertest.js",
         "chromeos/office_fallback/office_fallback_browsertest.js",
         "chromeos/os_feedback_ui/os_feedback_browsertest.js",
+        "chromeos/personalization_app/personalization_app_browsertest.js",
         "chromeos/scanning/scanning_app_browsertest.js",
         "chromeos/shimless_rma/shimless_rma_browsertest.js",
         "nearby_share/nearby_browsertest.js",
@@ -368,6 +369,7 @@
     "color_provider_css_colors_test_chromeos.ts",
     "color_provider_css_colors_test.ts",
     "fake_chrome_event.ts",
+    "invalidations/invalidations_test.ts",
     "metrics_test_support.ts",
     "mock_controller.ts",
     "mocked_metrics_reporter.ts",
@@ -385,7 +387,6 @@
 
     # TODO(dpapad): Migrate the files below to TypeScript and remove allowJs
     # from tsconfig_base.json.
-    "invalidations/invalidations_test.js",
     "mocha_adapter.js",
     "mojo_webui_test_support.js",
     "usb_internals_test.js",
diff --git a/chrome/test/data/webui/chai_assert.ts b/chrome/test/data/webui/chai_assert.ts
index 7fbac26..4113306 100644
--- a/chrome/test/data/webui/chai_assert.ts
+++ b/chrome/test/data/webui/chai_assert.ts
@@ -135,3 +135,7 @@
 export function assertArrayEquals(expected: any[], actual: any[]) {
   assertDeepEquals(expected, actual);
 }
+
+export function assertStringContains(expected: string, contains: string) {
+  chai.expect(expected).to.have.string(contains);
+}
\ No newline at end of file
diff --git a/chrome/test/data/webui/chromeos/personalization_app/BUILD.gn b/chrome/test/data/webui/chromeos/personalization_app/BUILD.gn
index 40db8d8..acfd9d9a 100644
--- a/chrome/test/data/webui/chromeos/personalization_app/BUILD.gn
+++ b/chrome/test/data/webui/chromeos/personalization_app/BUILD.gn
@@ -59,6 +59,7 @@
     "ambient_observer_test.ts",
     "wallpaper_preview_element_test.ts",
     "wallpaper_selected_element_test.ts",
+    "zone_customization_element_test.ts",
   ]
   deps = [
     "..:build_ts",
diff --git a/chrome/test/data/webui/chromeos/personalization_app/keyboard_backlight_element_test.ts b/chrome/test/data/webui/chromeos/personalization_app/keyboard_backlight_element_test.ts
index ae8e4c0d..281b378c 100644
--- a/chrome/test/data/webui/chromeos/personalization_app/keyboard_backlight_element_test.ts
+++ b/chrome/test/data/webui/chromeos/personalization_app/keyboard_backlight_element_test.ts
@@ -5,7 +5,7 @@
 import 'chrome://personalization/strings.m.js';
 import 'chrome://webui-test/mojo_webui_test_support.js';
 
-import {KeyboardBacklight, KeyboardBacklightActionName, KeyboardBacklightObserver, SetBacklightColorAction, SetShouldShowNudgeAction, SetWallpaperColorAction} from 'chrome://personalization/js/personalization_app.js';
+import {KeyboardBacklight, KeyboardBacklightActionName, KeyboardBacklightObserver, SetCurrentBacklightStateAction, SetShouldShowNudgeAction, SetWallpaperColorAction} from 'chrome://personalization/js/personalization_app.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
 import {assertDeepEquals, assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
 import {waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
@@ -60,47 +60,52 @@
     const colorContainers =
         selectorContainer.querySelectorAll('.color-container');
     assertEquals(9, colorContainers!.length);
-
     personalizationStore.setReducersEnabled(true);
     personalizationStore.expectAction(
-        KeyboardBacklightActionName.SET_BACKLIGHT_COLOR);
+        KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE);
     (colorContainers[1] as HTMLElement).click();
     await keyboardBacklightProvider.whenCalled('setBacklightColor');
 
-    const action = await personalizationStore.waitForAction(
-                       KeyboardBacklightActionName.SET_BACKLIGHT_COLOR) as
-        SetBacklightColorAction;
-    assertTrue(!!action.backlightColor);
-    assertTrue(!!personalizationStore.data.keyboardBacklight.backlightColor);
+    const action =
+        await personalizationStore.waitForAction(
+            KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE) as
+        SetCurrentBacklightStateAction;
+    assertTrue(!!action.currentBacklightState);
+    assertTrue(
+        !!personalizationStore.data.keyboardBacklight.currentBacklightState);
   });
 
   test('sets backlight color in store on first load', async () => {
     personalizationStore.expectAction(
-        KeyboardBacklightActionName.SET_BACKLIGHT_COLOR);
+        KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE);
     keyboardBacklightElement = initElement(KeyboardBacklight);
     await keyboardBacklightProvider.whenCalled('setKeyboardBacklightObserver');
-    keyboardBacklightProvider.fireOnBacklightColorChanged(
-        keyboardBacklightProvider.backlightColor);
-    const action = await personalizationStore.waitForAction(
-                       KeyboardBacklightActionName.SET_BACKLIGHT_COLOR) as
-        SetBacklightColorAction;
-    assertEquals(
-        keyboardBacklightProvider.backlightColor, action.backlightColor);
+    keyboardBacklightProvider.fireOnBacklightStateChanged(
+        keyboardBacklightProvider.currentBacklightState);
+    const action =
+        await personalizationStore.waitForAction(
+            KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE) as
+        SetCurrentBacklightStateAction;
+    assertDeepEquals(
+        keyboardBacklightProvider.currentBacklightState,
+        action.currentBacklightState);
   });
 
   test('sets backlight color data in store on changed', async () => {
     await keyboardBacklightProvider.whenCalled('setKeyboardBacklightObserver');
 
     personalizationStore.expectAction(
-        KeyboardBacklightActionName.SET_BACKLIGHT_COLOR);
+        KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE);
     keyboardBacklightProvider.keyboardBacklightObserverRemote!
-        .onBacklightColorChanged(keyboardBacklightProvider.backlightColor);
+        .onBacklightStateChanged(
+            keyboardBacklightProvider.currentBacklightState);
 
-    const {backlightColor} =
+    const {currentBacklightState} =
         await personalizationStore.waitForAction(
-            KeyboardBacklightActionName.SET_BACKLIGHT_COLOR) as
-        SetBacklightColorAction;
-    assertEquals(keyboardBacklightProvider.backlightColor, backlightColor);
+            KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE) as
+        SetCurrentBacklightStateAction;
+    assertDeepEquals(
+        keyboardBacklightProvider.currentBacklightState, currentBacklightState);
   });
 
   test('sets wallpaper color in store on first load', async () => {
@@ -166,7 +171,8 @@
   test(
       'shows customization button if multi-zone rgb keyboard is supported',
       async () => {
-        loadTimeData.overrideValues({isMultiZoneRgbKeyboardSupported: true});
+        loadTimeData.overrideValues(
+            {keyboardBacklightZoneCount: keyboardBacklightProvider.zoneCount});
         keyboardBacklightElement = initElement(KeyboardBacklight);
         const customizationButton =
             keyboardBacklightElement.shadowRoot!.getElementById(
@@ -175,7 +181,8 @@
       });
 
   test('clicking on customization button opens a dialog', async () => {
-    loadTimeData.overrideValues({isMultiZoneRgbKeyboardSupported: true});
+    loadTimeData.overrideValues(
+        {keyboardBacklightZoneCount: keyboardBacklightProvider.zoneCount});
     keyboardBacklightElement = initElement(KeyboardBacklight);
     const customizationButton =
         keyboardBacklightElement.shadowRoot!.getElementById(
@@ -195,7 +202,8 @@
   });
 
   test('shows wallpaper color at the end with multi-zone enabled', async () => {
-    loadTimeData.overrideValues({isMultiZoneRgbKeyboardSupported: true});
+    loadTimeData.overrideValues(
+        {keyboardBacklightZoneCount: keyboardBacklightProvider.zoneCount});
 
     keyboardBacklightElement = initElement(KeyboardBacklight);
 
@@ -215,7 +223,7 @@
   test(
       'shows wallpaper color button at the beginning with multi-zone disabled',
       async () => {
-        loadTimeData.overrideValues({isMultiZoneRgbKeyboardSupported: false});
+        loadTimeData.overrideValues({keyboardBacklightZoneCount: 0});
 
         keyboardBacklightElement = initElement(KeyboardBacklight);
 
diff --git a/chrome/test/data/webui/chromeos/personalization_app/personalization_app_component_browsertest.js b/chrome/test/data/webui/chromeos/personalization_app/personalization_app_component_browsertest.js
index 1aec5e859..d23808ed 100644
--- a/chrome/test/data/webui/chromeos/personalization_app/personalization_app_component_browsertest.js
+++ b/chrome/test/data/webui/chromeos/personalization_app/personalization_app_component_browsertest.js
@@ -79,6 +79,7 @@
    'GooglePhotosSharedAlbumDialogTest',
    'google_photos_shared_album_dialog_element_test.js'
  ],
+ ['ZoneCustomizationTest', 'zone_customization_element_test.js'],
 ].forEach(test => registerTest(...test));
 
 function registerTest(testName, module, caseName) {
diff --git a/chrome/test/data/webui/chromeos/personalization_app/test_keyboard_backlight_interface_provider.ts b/chrome/test/data/webui/chromeos/personalization_app/test_keyboard_backlight_interface_provider.ts
index c8cd229..aa0d1f6 100644
--- a/chrome/test/data/webui/chromeos/personalization_app/test_keyboard_backlight_interface_provider.ts
+++ b/chrome/test/data/webui/chromeos/personalization_app/test_keyboard_backlight_interface_provider.ts
@@ -2,13 +2,21 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {BacklightColor, KeyboardBacklightObserverInterface, KeyboardBacklightObserverRemote, KeyboardBacklightProviderInterface} from 'chrome://personalization/js/personalization_app.js';
+import {BacklightColor, CurrentBacklightState, KeyboardBacklightObserverInterface, KeyboardBacklightObserverRemote, KeyboardBacklightProviderInterface} from 'chrome://personalization/js/personalization_app.js';
 import {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js';
 import {TestBrowserProxy} from 'chrome://webui-test/test_browser_proxy.js';
 
 export class TestKeyboardBacklightProvider extends TestBrowserProxy implements
     KeyboardBacklightProviderInterface {
-  public backlightColor: BacklightColor = BacklightColor.kBlue;
+  public zoneCount: number = 5;
+  public zoneColors: BacklightColor[] = [
+    BacklightColor.kBlue,
+    BacklightColor.kRed,
+    BacklightColor.kWallpaper,
+    BacklightColor.kYellow,
+  ];
+  public currentBacklightState:
+      CurrentBacklightState = {color: BacklightColor.kBlue};
 
   constructor() {
     super([
@@ -23,6 +31,14 @@
   keyboardBacklightObserverRemote: KeyboardBacklightObserverInterface|null =
       null;
 
+  setZoneCount(zoneCount: number) {
+    this.zoneCount = zoneCount;
+  }
+
+  setCurrentBacklightState(backlightState: CurrentBacklightState) {
+    this.currentBacklightState = backlightState;
+  }
+
   setBacklightColor(backlightColor: BacklightColor) {
     this.methodCalled('setBacklightColor', backlightColor);
   }
@@ -45,9 +61,9 @@
     this.keyboardBacklightObserverRemote = remote;
   }
 
-  fireOnBacklightColorChanged(backlightColor: BacklightColor) {
-    this.keyboardBacklightObserverRemote!.onBacklightColorChanged(
-        backlightColor);
+  fireOnBacklightStateChanged(currentBacklightState: CurrentBacklightState) {
+    this.keyboardBacklightObserverRemote!.onBacklightStateChanged(
+        currentBacklightState);
   }
 
   fireOnWallpaperColorChanged(wallpaperColor: SkColor) {
diff --git a/chrome/test/data/webui/chromeos/personalization_app/zone_customization_element_test.ts b/chrome/test/data/webui/chromeos/personalization_app/zone_customization_element_test.ts
new file mode 100644
index 0000000..be0b30e0
--- /dev/null
+++ b/chrome/test/data/webui/chromeos/personalization_app/zone_customization_element_test.ts
@@ -0,0 +1,116 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'chrome://personalization/strings.m.js';
+import 'chrome://webui-test/mojo_webui_test_support.js';
+
+import {CurrentBacklightState, KeyboardBacklightActionName, KeyboardBacklightObserver, SetCurrentBacklightStateAction, staticColorIds, ZoneCustomizationElement} from 'chrome://personalization/js/personalization_app.js';
+import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
+import {assertDeepEquals, assertEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
+import {waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
+
+import {baseSetup, initElement, teardownElement} from './personalization_app_test_utils.js';
+import {TestKeyboardBacklightProvider} from './test_keyboard_backlight_interface_provider.js';
+import {TestPersonalizationStore} from './test_personalization_store.js';
+
+suite('ZoneCustomizationElementTest', function() {
+  let zoneCustomizationElement: ZoneCustomizationElement|null;
+  let keyboardBacklightProvider: TestKeyboardBacklightProvider;
+  let personalizationStore: TestPersonalizationStore;
+
+  setup(() => {
+    const mocks = baseSetup();
+    keyboardBacklightProvider = mocks.keyboardBacklightProvider;
+    personalizationStore = mocks.personalizationStore;
+    KeyboardBacklightObserver.initKeyboardBacklightObserverIfNeeded();
+  });
+
+  teardown(async () => {
+    await teardownElement(zoneCustomizationElement);
+    zoneCustomizationElement = null;
+    KeyboardBacklightObserver.shutdown();
+  });
+
+  async function initZoneCustomizationElement() {
+    loadTimeData.overrideValues(
+        {keyboardBacklightZoneCount: keyboardBacklightProvider.zoneCount});
+    personalizationStore.data.keyboardBacklight.currentBacklightState =
+        keyboardBacklightProvider.currentBacklightState;
+    personalizationStore.notifyObservers();
+    zoneCustomizationElement = initElement(ZoneCustomizationElement);
+    await waitAfterNextRender(zoneCustomizationElement);
+  }
+
+  test(
+      'displays content with current backlight state as a static color',
+      async () => {
+        await initZoneCustomizationElement();
+        const zoneSelector =
+            zoneCustomizationElement!.shadowRoot!.getElementById(
+                'zoneSelector');
+        assertTrue(!!zoneSelector, 'zone selector should display');
+        const zoneButtons =
+            zoneCustomizationElement!.shadowRoot!.querySelectorAll(
+                '.zone-button');
+        assertEquals(
+            5, zoneButtons!.length,
+            '5 zones should display in customization dialog');
+        const dialogCloseButton =
+            zoneCustomizationElement!.shadowRoot!.getElementById(
+                'dialogCloseButton');
+        assertTrue(!!dialogCloseButton, 'close dialog button should display');
+      });
+
+  test(
+      'updates zone content with current backlight state as zone colors',
+      async () => {
+        keyboardBacklightProvider.setZoneCount(4);
+        keyboardBacklightProvider.setCurrentBacklightState(
+            {zoneColors: keyboardBacklightProvider.zoneColors});
+        await initZoneCustomizationElement();
+        const zoneSelector =
+            zoneCustomizationElement!.shadowRoot!.getElementById(
+                'zoneSelector');
+        assertTrue(!!zoneSelector, 'zone selector should display');
+        const zoneButtons =
+            zoneCustomizationElement!.shadowRoot!.querySelectorAll(
+                '.zone-button');
+        assertEquals(
+            4, zoneButtons!.length,
+            '4 zones should display in customization dialog');
+        const colorIcons =
+            zoneCustomizationElement!.shadowRoot!.querySelectorAll(
+                'color-icon');
+        assertEquals(
+            4, colorIcons!.length,
+            '4 color icons should display in customization dialog');
+        // Color of the color-icon displayed in each zone should match with the
+        // corresponding one in zone colors.
+        for (let i = 0; i < 4; i++) {
+          const zoneColor = keyboardBacklightProvider.zoneColors[i];
+          const expectedColorId = staticColorIds[zoneColor!];
+          const colorId =
+              (colorIcons[i] as HTMLElement).getAttribute('color-id');
+          assertEquals(
+              expectedColorId, colorId,
+              `colorId for zone ${i + 1} should be ${expectedColorId}`);
+        }
+      });
+
+  test('sets zone colors data in store on first load', async () => {
+    const currentBacklightState: CurrentBacklightState = {
+      zoneColors: keyboardBacklightProvider.zoneColors,
+    };
+    personalizationStore.expectAction(
+        KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE);
+    await keyboardBacklightProvider.whenCalled('setKeyboardBacklightObserver');
+    keyboardBacklightProvider.fireOnBacklightStateChanged(
+        currentBacklightState);
+    const action =
+        await personalizationStore.waitForAction(
+            KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE) as
+        SetCurrentBacklightStateAction;
+    assertDeepEquals(currentBacklightState, action.currentBacklightState);
+  });
+});
diff --git a/chrome/test/data/webui/extensions/url_util_test.ts b/chrome/test/data/webui/extensions/url_util_test.ts
index eab6d2db..9591a45 100644
--- a/chrome/test/data/webui/extensions/url_util_test.ts
+++ b/chrome/test/data/webui/extensions/url_util_test.ts
@@ -9,7 +9,7 @@
 
 suite('UrlUtilTest', function() {
   function getExpectedImageSet(url: string): string {
-    return '-webkit-image-set(' +
+    return 'image-set(' +
         'url("chrome://favicon2/?size=20&scaleFactor=1x&pageUrl=' +
         encodeURIComponent(url) + '&allowGoogleServerFallback=0") 1x, ' +
         'url("chrome://favicon2/?size=20&scaleFactor=2x&pageUrl=' +
diff --git a/chrome/test/data/webui/invalidations/invalidations_test.js b/chrome/test/data/webui/invalidations/invalidations_test.ts
similarity index 68%
rename from chrome/test/data/webui/invalidations/invalidations_test.js
rename to chrome/test/data/webui/invalidations/invalidations_test.ts
index 0a72a72..a38a0b1 100644
--- a/chrome/test/data/webui/invalidations/invalidations_test.js
+++ b/chrome/test/data/webui/invalidations/invalidations_test.ts
@@ -3,23 +3,27 @@
 // found in the LICENSE file.
 
 import {webUIListenerCallback} from 'chrome://resources/js/cr.js';
-import {$} from 'chrome://resources/js/util_ts.js';
-import {assertEquals, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
+import {getRequiredElement} from 'chrome://resources/js/util_ts.js';
 
-window.invalidations_test = {};
-invalidations_test = window.invalidations_test;
-invalidations_test.TestNames = {
-  RegisterNewInvalidation: 'register new invalidation',
-  ChangeInvalidationsState: 'change invalidations state',
-  RegisterNewIds: 'register new ids',
-  UpdateRegisteredHandlers: 'update registered handlers',
-  UpdateInternalDisplay: 'update internal display',
+import {assertEquals, assertNotEquals, assertTrue} from '../chai_assert.js';
+
+const invalidations_test = {
+  TestNames: {
+    RegisterNewInvalidation: 'register new invalidation',
+    ChangeInvalidationsState: 'change invalidations state',
+    RegisterNewIds: 'register new ids',
+    UpdateRegisteredHandlers: 'update registered handlers',
+    UpdateInternalDisplay: 'update internal display',
+  },
 };
 
+Object.assign(window, {invalidations_test});
+
 suite('invalidations_test', function() {
   // Test that registering an invalidations appears properly on the textarea.
   test(invalidations_test.TestNames.RegisterNewInvalidation, function() {
-    const invalidationsLog = $('invalidations-log');
+    const invalidationsLog =
+        getRequiredElement<HTMLTextAreaElement>('invalidations-log');
     const invalidation = [
       {isUnknownVersion: 'true', objectId: {name: 'EXTENSIONS', source: 1004}},
     ];
@@ -35,19 +39,21 @@
   // Test that changing the Invalidations Service state appears both in the
   // span and in the textarea.
   test(invalidations_test.TestNames.ChangeInvalidationsState, function() {
-    const invalidationsState = $('invalidations-state');
-    const invalidationsLog = $('invalidations-log');
+    const invalidationsState =
+        getRequiredElement<HTMLElement>('invalidations-state');
+    const invalidationsLog =
+        getRequiredElement<HTMLTextAreaElement>('invalidations-log');
     const newState = 'INVALIDATIONS_ENABLED';
     const newNewState = 'TRANSIENT_INVALIDATION_ERROR';
 
     webUIListenerCallback('state-updated', newState);
     const isContainedState =
-        invalidationsState.textContent.indexOf('INVALIDATIONS_ENABLED') !== -1;
+        invalidationsState.textContent!.indexOf('INVALIDATIONS_ENABLED') !== -1;
     assertTrue(isContainedState, 'could not change the invalidations text');
 
     invalidationsLog.value = '';
     webUIListenerCallback('state-updated', newNewState);
-    const isContainedState2 = invalidationsState.textContent.indexOf(
+    const isContainedState2 = invalidationsState.textContent!.indexOf(
                                   'TRANSIENT_INVALIDATION_ERROR') !== -1;
     assertTrue(isContainedState2, 'could not change the invalidations text');
     const isContainedLog = invalidationsLog.value.indexOf(
@@ -58,10 +64,17 @@
 
   // Test that objects ids appear on the table.
   test(invalidations_test.TestNames.RegisterNewIds, function() {
-    let newDataType = [
+    interface DataType {
+      name: string;
+      source: number;
+      totalCount?: number;
+    }
+
+    let newDataType: DataType[] = [
       {name: 'EXTENSIONS', source: 1004, totalCount: 0},
       {name: 'FAVICON_IMAGE', source: 1004, totalCount: 0},
     ];
+
     const registrarName = 'Fake';
     const pattern1 =
         [registrarName, '1004', 'EXTENSIONS', '0', '0', '', '', ''];
@@ -74,23 +87,24 @@
     webUIListenerCallback('ids-updated', registrarName, newDataType);
 
     // Test that the two patterns are contained in the table.
-    const oidTable = $('objectsid-table-container');
+    const oidTable =
+        getRequiredElement<HTMLTableElement>('objectsid-table-container');
     let foundPattern1 = false;
     let foundPattern2 = false;
-    for (let row = 0; row < oidTable.rows.length; row++) {
+    for (const row of oidTable.rows) {
       let pattern1Test = true;
       let pattern2Test = true;
-      for (let cell = 0; cell < oidTable.rows[row].cells.length; cell++) {
-        pattern1Test = pattern1Test &&
-            (pattern1[cell] === oidTable.rows[row].cells[cell].textContent);
-        pattern2Test = pattern2Test &&
-            (pattern2[cell] === oidTable.rows[row].cells[cell].textContent);
+      for (let cell = 0; cell < row.cells.length; cell++) {
+        pattern1Test =
+            pattern1Test && (pattern1[cell] === row.cells[cell]!.textContent);
+        pattern2Test =
+            pattern2Test && (pattern2[cell] === row.cells[cell]!.textContent);
       }
       if (pattern1Test) {
-        assertEquals('greyed', oidTable.rows[row].className);
+        assertEquals('greyed', row.className);
       }
       if (pattern2Test) {
-        assertEquals('content', oidTable.rows[row].className);
+        assertEquals('content', row.className);
       }
 
       foundPattern1 = foundPattern1 || pattern1Test;
@@ -106,7 +120,8 @@
   // Test that registering new handlers appear on the website.
   test(invalidations_test.TestNames.UpdateRegisteredHandlers, function() {
     function text() {
-      return $('registered-handlers').textContent;
+      return getRequiredElement<HTMLElement>('registered-handlers').textContent!
+          ;
     }
     webUIListenerCallback('handlers-updated', ['FakeApi', 'FakeClient']);
     assertNotEquals(text().indexOf('FakeApi'), -1);
@@ -121,6 +136,8 @@
   test(invalidations_test.TestNames.UpdateInternalDisplay, function() {
     const newDetailedStatus = {MessagesSent: 1};
     webUIListenerCallback('detailed-status-updated', newDetailedStatus);
-    assertEquals($('internal-display').value, '{\n  \"MessagesSent\": 1\n}');
+    assertEquals(
+        getRequiredElement<HTMLTextAreaElement>('internal-display').value,
+        '{\n  \"MessagesSent\": 1\n}');
   });
 });
diff --git a/chrome/test/data/webui/js/icon_test.ts b/chrome/test/data/webui/js/icon_test.ts
index 1ca9b76d..cea5ab3 100644
--- a/chrome/test/data/webui/js/icon_test.ts
+++ b/chrome/test/data/webui/js/icon_test.ts
@@ -11,12 +11,12 @@
     const url = 'http://foo.com';
 
     function getExpectedImageSet(size: number): string {
-      const expectedDesktop = '-webkit-image-set(' +
+      const expectedDesktop = 'image-set(' +
           `url("chrome://favicon2/?size=${size}&scaleFactor=1x&pageUrl=` +
           encodeURIComponent(url) + '&allowGoogleServerFallback=0") 1x, ' +
           `url("chrome://favicon2/?size=${size}&scaleFactor=2x&pageUrl=` +
           encodeURIComponent(url) + '&allowGoogleServerFallback=0") 2x)';
-      const expectedOther = '-webkit-image-set(' +
+      const expectedOther = 'image-set(' +
           `url("chrome://favicon2/?size=${size}&scaleFactor=1x&pageUrl=` +
           encodeURIComponent(url) + '&allowGoogleServerFallback=0") ' +
           window.devicePixelRatio + 'x)';
@@ -44,12 +44,12 @@
 
   test('GetFavicon', function() {
     const url = 'http://foo.com/foo.ico';
-    const expectedDesktop = '-webkit-image-set(' +
+    const expectedDesktop = 'image-set(' +
         'url("chrome://favicon2/?size=16&scaleFactor=1x&iconUrl=' +
         encodeURIComponent('http://foo.com/foo.ico') + '") 1x, ' +
         'url("chrome://favicon2/?size=16&scaleFactor=2x&iconUrl=' +
         encodeURIComponent('http://foo.com/foo.ico') + '") 2x)';
-    const expectedOther = '-webkit-image-set(' +
+    const expectedOther = 'image-set(' +
         'url("chrome://favicon2/?size=16&scaleFactor=1x&iconUrl=' +
         encodeURIComponent('http://foo.com/foo.ico') + '") ' +
         window.devicePixelRatio + 'x)';
diff --git a/chrome/test/data/webui/new_tab_page/modules/drive/module_test.ts b/chrome/test/data/webui/new_tab_page/modules/drive/module_test.ts
index be78cc8..30f7e524 100644
--- a/chrome/test/data/webui/new_tab_page/modules/drive/module_test.ts
+++ b/chrome/test/data/webui/new_tab_page/modules/drive/module_test.ts
@@ -113,10 +113,11 @@
     // Assert.
     const event: DismissModuleEvent = await whenFired;
     assertEquals('Files hidden', event.detail.message);
+    assertTrue(!!event.detail.restoreCallback);
     assertEquals(1, handler.getCallCount('dismissModule'));
 
     // Act.
-    event.detail.restoreCallback();
+    event.detail.restoreCallback!();
 
     // Assert.
     assertEquals(1, handler.getCallCount('restoreModule'));
diff --git a/chrome/test/data/webui/new_tab_page/modules/history_clusters/module_test.ts b/chrome/test/data/webui/new_tab_page/modules/history_clusters/module_test.ts
index 2f05e21..b13d5de6 100644
--- a/chrome/test/data/webui/new_tab_page/modules/history_clusters/module_test.ts
+++ b/chrome/test/data/webui/new_tab_page/modules/history_clusters/module_test.ts
@@ -6,12 +6,13 @@
 
 import {Cluster, SearchQuery, URLVisit} from 'chrome://new-tab-page/history_cluster_types.mojom-webui.js';
 import {PageHandlerRemote} from 'chrome://new-tab-page/history_clusters.mojom-webui.js';
-import {HistoryClusterLayoutType, historyClustersDescriptor, HistoryClustersModuleElement, HistoryClustersProxyImpl, LAYOUT_1_MIN_IMAGE_VISITS, LAYOUT_1_MIN_VISITS, LAYOUT_2_MIN_IMAGE_VISITS, LAYOUT_2_MIN_VISITS, LAYOUT_3_MIN_IMAGE_VISITS, LAYOUT_3_MIN_VISITS, MIN_RELATED_SEARCHES} from 'chrome://new-tab-page/lazy_load.js';
+import {DismissModuleEvent, HistoryClusterLayoutType, historyClustersDescriptor, HistoryClustersModuleElement, HistoryClustersProxyImpl, LAYOUT_1_MIN_IMAGE_VISITS, LAYOUT_1_MIN_VISITS, LAYOUT_2_MIN_IMAGE_VISITS, LAYOUT_2_MIN_VISITS, LAYOUT_3_MIN_IMAGE_VISITS, LAYOUT_3_MIN_VISITS, MIN_RELATED_SEARCHES} from 'chrome://new-tab-page/lazy_load.js';
 import {$$} from 'chrome://new-tab-page/new_tab_page.js';
 import {assertEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
 import {fakeMetricsPrivate, MetricsTracker} from 'chrome://webui-test/metrics_test_support.js';
 import {waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
 import {TestMock} from 'chrome://webui-test/test_mock.js';
+import {eventToPromise} from 'chrome://webui-test/test_util.js';
 
 import {installMock} from '../../test_support.js';
 
@@ -362,4 +363,32 @@
     assertTrue(!!headerElement);
     assertModuleHeaderTitle(headerElement, 'Resume your journey for SRP');
   });
+
+  test('Backend is notified when module is dismissed', async () => {
+    const sampleClusterLabel = '"Sample Journey"';
+    const sampleCluster = createSampleCluster({label: sampleClusterLabel});
+    handler.setResultFor(
+        'getCluster', Promise.resolve({cluster: sampleCluster}));
+
+    const moduleElement = await historyClustersDescriptor.initialize(0) as
+        HistoryClustersModuleElement;
+
+    document.body.append(moduleElement);
+    assertTrue(!!moduleElement);
+    await handler.whenCalled('getCluster');
+    await waitAfterNextRender(moduleElement);
+
+    const waitForDismissEvent = eventToPromise('dismiss-module', moduleElement);
+    const dismissButton =
+        moduleElement.shadowRoot!.querySelector('ntp-module-header')!
+            .shadowRoot!.querySelector<HTMLElement>('#dismissButton')!;
+    dismissButton.click();
+    const dismissEvent: DismissModuleEvent = await waitForDismissEvent;
+    assertEquals(`${sampleCluster.label!} hidden`, dismissEvent.detail.message);
+    const visits = await handler.whenCalled('dismissCluster');
+    assertEquals(3, visits.length);
+    visits.forEach((visit: URLVisit, index: number) => {
+      assertEquals(index, Number(visit.visitId));
+    });
+  });
 });
diff --git a/chrome/test/data/webui/new_tab_page/modules/modules_test.ts b/chrome/test/data/webui/new_tab_page/modules/modules_test.ts
index c0a9664..c42aa04 100644
--- a/chrome/test/data/webui/new_tab_page/modules/modules_test.ts
+++ b/chrome/test/data/webui/new_tab_page/modules/modules_test.ts
@@ -10,6 +10,7 @@
 import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
 import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
 import {fakeMetricsPrivate, MetricsTracker} from 'chrome://webui-test/metrics_test_support.js';
+import {waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
 import {TestMock} from 'chrome://webui-test/test_mock.js';
 
 import {assertNotStyle, assertStyle, capture, createElement, initNullModule, installMock, render} from '../test_support.js';
@@ -416,7 +417,11 @@
       assertFalse(restoreCalled);
 
       // Act.
-      modulesElement.$.undoRemoveModuleButton.click();
+      await waitAfterNextRender(modulesElement);
+      const undoRemoveModuleButton =
+          modulesElement.shadowRoot!.querySelector('#undoRemoveModuleButton') as
+          HTMLElement;
+      undoRemoveModuleButton.click();
 
       // Assert.
       assertDeepEquals(['foo', false], handler.getArgs('setModuleDisabled')[1]);
@@ -458,6 +463,53 @@
     });
   });
 
+  test('modules can be dismissed with no restore action', async () => {
+    const fooDescriptor = new ModuleDescriptor('foo', initNullModule);
+    moduleRegistry.setResultFor('getDescriptors', [fooDescriptor]);
+
+    // Act.
+    const modulesElement = await createModulesElement([
+      {
+        descriptor: fooDescriptor,
+        element: createElement(),
+      },
+    ]);
+    callbackRouterRemote.setDisabledModules(false, []);
+    await callbackRouterRemote.$.flushForTesting();
+
+    const moduleWrappers =
+        modulesElement.shadowRoot!.querySelectorAll('ntp-module-wrapper');
+    const moduleWrapperContainers =
+        modulesElement.shadowRoot!.querySelectorAll('.module-container');
+    assertEquals(1, moduleWrappers.length);
+    assertEquals(1, moduleWrapperContainers.length);
+    assertNotStyle(moduleWrappers[0]!, 'display', 'none');
+    assertNotStyle(moduleWrapperContainers[0]!, 'display', 'none');
+    assertFalse(modulesElement.$.removeModuleToast.open);
+
+    // Act.
+    moduleWrappers[0]!.dispatchEvent(new CustomEvent('dismiss-module', {
+      bubbles: true,
+      composed: true,
+      detail: {
+        message: 'Foo',
+      },
+    }));
+    await waitAfterNextRender(modulesElement);
+
+    // Assert.
+    assertNotStyle(moduleWrappers[0]!, 'display', 'none');
+    assertStyle(moduleWrapperContainers[0]!, 'display', 'none');
+    assertTrue(modulesElement.$.removeModuleToast.open);
+    assertEquals(
+        'Foo', modulesElement.$.removeModuleToastMessage.textContent!.trim());
+    assertEquals(1, handler.getCallCount('onDismissModule'));
+    assertEquals('foo', handler.getArgs('onDismissModule')[0]);
+    assertEquals(
+        null,
+        modulesElement.shadowRoot!.querySelector('#undoRemoveModuleButton'));
+  });
+
   test('modules can be dismissed and restored', async () => {
     // Arrange.
     let restoreCalled = false;
@@ -508,7 +560,11 @@
     assertFalse(restoreCalled);
 
     // Act.
-    modulesElement.$.undoRemoveModuleButton.click();
+    await waitAfterNextRender(modulesElement);
+    const undoRemoveModuleButton =
+        modulesElement.shadowRoot!.querySelector('#undoRemoveModuleButton') as
+        HTMLElement;
+    undoRemoveModuleButton.click();
 
     // Assert.
     assertNotStyle(moduleWrappers[0]!, 'display', 'none');
@@ -574,7 +630,11 @@
     assertFalse(restoreCalled);
 
     // Act.
-    modulesElement.$.undoRemoveModuleButton.click();
+    await waitAfterNextRender(modulesElement);
+    const undoRemoveModuleButton =
+        modulesElement.shadowRoot!.querySelector('#undoRemoveModuleButton') as
+        HTMLElement;
+    undoRemoveModuleButton.click();
 
     // Assert.
     assertDeepEquals(['foo', false], handler.getArgs('setModuleDisabled')[1]);
@@ -994,8 +1054,12 @@
       assertEquals(1, moduleWrappers.indexOf(tallModule));
       assertEquals(2, moduleWrappers.indexOf(shortModule1));
 
-      // Act.
-      modulesElement.$.undoRemoveModuleButton.click();
+      // // Act.
+      await waitAfterNextRender(modulesElement);
+      const undoRemoveModuleButton =
+          modulesElement.shadowRoot!.querySelector('#undoRemoveModuleButton') as
+          HTMLElement;
+      undoRemoveModuleButton.click();
 
       // Assert.
       assertDeepEquals(['bar', false], handler.getArgs('setModuleDisabled')[1]);
diff --git a/chrome/test/data/webui/new_tab_page/modules/photos/module_test.ts b/chrome/test/data/webui/new_tab_page/modules/photos/module_test.ts
index 8e83847..621db6d5 100644
--- a/chrome/test/data/webui/new_tab_page/modules/photos/module_test.ts
+++ b/chrome/test/data/webui/new_tab_page/modules/photos/module_test.ts
@@ -526,10 +526,11 @@
     assertEquals(
         loadTimeData.getString('modulesPhotosMemoriesHiddenToday'),
         event.detail.message);
+    assertTrue(!!event.detail.restoreCallback);
     assertEquals(1, handler.getCallCount('dismissModule'));
 
     // Act.
-    event.detail.restoreCallback();
+    event.detail.restoreCallback!();
 
     // Assert.
     assertEquals(1, handler.getCallCount('restoreModule'));
diff --git a/chrome/test/data/webui/new_tab_page/modules/recipes/module_test.ts b/chrome/test/data/webui/new_tab_page/modules/recipes/module_test.ts
index 60226bc..3cec54d6 100644
--- a/chrome/test/data/webui/new_tab_page/modules/recipes/module_test.ts
+++ b/chrome/test/data/webui/new_tab_page/modules/recipes/module_test.ts
@@ -207,18 +207,19 @@
         moduleElement.shadowRoot!.querySelector('ntp-module-header')!
             .shadowRoot!.querySelector<HTMLElement>('#dismissButton')!;
     dismissButton.click();
-    const dismissEvent: DismissModuleEvent = await waitForDismissEvent;
-    const toastMessage = dismissEvent.detail.message;
-    const restoreCallback = dismissEvent.detail.restoreCallback;
 
     // Assert.
+    const dismissEvent: DismissModuleEvent = await waitForDismissEvent;
+    const toastMessage = dismissEvent.detail.message;
     const moduleHeaderTitle =
         moduleElement.shadowRoot!.querySelector(
                                      'ntp-module-header')!.textContent!.trim();
     assertEquals(moduleHeaderTitle + ' hidden', toastMessage);
+    assertTrue(!!dismissEvent.detail.restoreCallback);
     assertEquals('Hello world', await handler.whenCalled('dismissTask'));
 
     // Act.
+    const restoreCallback = dismissEvent.detail.restoreCallback!;
     restoreCallback();
 
     // Assert.
diff --git a/chrome/test/data/webui/new_tab_page/realbox/realbox_test.ts b/chrome/test/data/webui/new_tab_page/realbox/realbox_test.ts
index 7a3ed05..1248971a 100644
--- a/chrome/test/data/webui/new_tab_page/realbox/realbox_test.ts
+++ b/chrome/test/data/webui/new_tab_page/realbox/realbox_test.ts
@@ -157,7 +157,14 @@
       iconElement: RealboxIconElement, destinationUrl: string) {
     assertStyle(
         iconElement.$.icon, 'background-image',
-        getFaviconForPageURL(destinationUrl, false, '', 32, true));
+
+        // Resolution units are converted from `x` (shorthand for `dppx`) to
+        // `dppx` (the canonical unit for the resolution type) because
+        // assertStyle is using computed values instead of specified ones, and
+        // the computed values have to return the canonical unit for the type.
+        getFaviconForPageURL(destinationUrl, false, '', 32, true)
+            .replace(' 1x', ' 1dppx')
+            .replace(' 2x', ' 2dppx'));
     assertStyle(iconElement.$.icon, '-webkit-mask-image', 'none');
   }
 
diff --git a/chrome/test/data/webui/settings/site_favicon_test.ts b/chrome/test/data/webui/settings/site_favicon_test.ts
index 18fdf9c7..b6d2cf8 100644
--- a/chrome/test/data/webui/settings/site_favicon_test.ts
+++ b/chrome/test/data/webui/settings/site_favicon_test.ts
@@ -22,7 +22,7 @@
   }
 
   function formExpected(url: string): string {
-    return '-webkit-image-set(' +
+    return 'image-set(' +
         'url("chrome://favicon2/?size=16&scaleFactor=1x&pageUrl=' +
         encodeURIComponent(url) + '&allowGoogleServerFallback=0") 1x, ' +
         'url("chrome://favicon2/?size=16&scaleFactor=2x&pageUrl=' +
diff --git a/chrome/test/data/webui/side_panel/read_anything/read_anything_app_test.ts b/chrome/test/data/webui/side_panel/read_anything/read_anything_app_test.ts
index 0c4926f8..95cf1ee0 100644
--- a/chrome/test/data/webui/side_panel/read_anything/read_anything_app_test.ts
+++ b/chrome/test/data/webui/side_panel/read_anything/read_anything_app_test.ts
@@ -6,7 +6,7 @@
 import 'chrome://webui-test/mojo_webui_test_support.js';
 
 import {ReadAnythingElement} from 'chrome://read-anything-side-panel.top-chrome/app.js';
-import {assertEquals} from 'chrome://webui-test/chai_assert.js';
+import {assertEquals, assertFalse, assertStringContains} from 'chrome://webui-test/chai_assert.js';
 import {eventToPromise} from 'chrome://webui-test/test_util.js';
 
 suite('ReadAnythingAppTest', () => {
@@ -17,27 +17,6 @@
   // rest of the Read Anything feature, which we are not testing here.
   chrome.readAnything.onConnected = () => {};
 
-  // This is called by readAnythingApp onselectionchange. It is usually
-  // implemented by ReadAnythingAppController which forwards these arguments to
-  // the browser process in the form of an AXEventNotificationDetail. Instead,
-  // we capture the arguments here and verify their values. Since
-  // onselectionchange is called asynchronously, the test must wait for this
-  // function to be called; therefore we fire a custom event
-  // on-selection-change-for-text here for the test to await.
-  chrome.readAnything.onSelectionChange =
-      (anchorNodeId: number, anchorOffset: number, focusNodeId: number,
-       focusOffset: number) => {
-        readAnythingApp.shadowRoot!.dispatchEvent(
-            new CustomEvent('on-selection-change-for-test', {
-              detail: {
-                anchorNodeId: anchorNodeId,
-                anchorOffset: anchorOffset,
-                focusNodeId: focusNodeId,
-                focusOffset: focusOffset,
-              },
-            }));
-      };
-
   setup(() => {
     document.body.innerHTML = window.trustedTypes!.emptyHTML;
     readAnythingApp = document.createElement('read-anything-app');
@@ -45,6 +24,29 @@
     chrome.readAnything.setThemeForTesting('default', 18.0, 0, 0, 1, 0);
   });
 
+  const setOnSelectionChangeForTest = () => {
+    // This is called by readAnythingApp onselectionchange. It is usually
+    // implemented by ReadAnythingAppController which forwards these arguments
+    // to the browser process in the form of an AXEventNotificationDetail.
+    // Instead, we capture the arguments here and verify their values. Since
+    // onselectionchange is called asynchronously, the test must wait for this
+    // function to be called; therefore we fire a custom event
+    // on-selection-change-for-text here for the test to await.
+    chrome.readAnything.onSelectionChange =
+        (anchorNodeId: number, anchorOffset: number, focusNodeId: number,
+         focusOffset: number) => {
+          readAnythingApp.shadowRoot!.dispatchEvent(
+              new CustomEvent('on-selection-change-for-test', {
+                detail: {
+                  anchorNodeId: anchorNodeId,
+                  anchorOffset: anchorOffset,
+                  focusNodeId: focusNodeId,
+                  focusOffset: focusOffset,
+                },
+              }));
+        };
+  };
+
   const assertFontName = (fontFamily: string) => {
     const container = readAnythingApp.shadowRoot!.getElementById('container');
     assertEquals(fontFamily, getComputedStyle(container!).fontFamily);
@@ -119,6 +121,78 @@
     assertEquals('1.6px', getComputedStyle(container!).letterSpacing);
   });
 
+  // The loading screen shows when we connect.
+  test('connectedCallback showLoadingScreen', () => {
+    const emptyState =
+        readAnythingApp.shadowRoot!.querySelector('sp-empty-state')!;
+
+    assertEquals(
+        readAnythingApp.shadowRoot!.getElementById(
+                                       'empty-state-container')!.hidden,
+        false);
+    assertEquals(emptyState.heading, 'Getting ready');
+    assertEquals(emptyState.body, '');
+    assertStringContains(emptyState.imagePath, 'throbber');
+    assertStringContains(emptyState.darkImagePath, 'throbber');
+  });
+
+  test(
+      'onselectionchange nothingSelectedOnLoadingScreenSelection', async () => {
+        const emptyState =
+            readAnythingApp.shadowRoot!.getElementById('empty-state-container');
+        let selectionChanged = false;
+        chrome.readAnything.onSelectionChange =
+            (_anchorNodeId: number, _anchorOffset: number, _focusNodeId: number,
+             _focusOffset: number) => {
+              selectionChanged = true;
+            };
+
+        const range = new Range();
+        range.setStartBefore(emptyState!);
+        range.setEndAfter(emptyState!);
+        const selection = readAnythingApp.shadowRoot!.getSelection();
+        selection!.removeAllRanges();
+        selection!.addRange(range);
+
+        setTimeout(() => {
+          assertFalse(selectionChanged);
+        }, 1000);
+      });
+
+  test('updateContent hidesLoadingScreen', () => {
+    // root htmlTag='#document' id=1
+    // ++paragraph htmlTag='p' id=2
+    // ++++staticText name='This is a paragraph' id=3
+    const axTree = {
+      rootId: 1,
+      nodes: [
+        {
+          id: 1,
+          role: 'rootWebArea',
+          htmlTag: '#document',
+          childIds: [2],
+        },
+        {
+          id: 2,
+          role: 'paragraph',
+          htmlTag: 'p',
+          childIds: [3],
+        },
+        {
+          id: 3,
+          role: 'staticText',
+          name: 'This is a paragraph',
+        },
+      ],
+    };
+    chrome.readAnything.setContentForTesting(axTree, [2]);
+
+    assertEquals(
+        readAnythingApp.shadowRoot!.getElementById(
+                                       'empty-state-container')!.hidden,
+        true);
+  });
+
   test('updateContent paragraph', () => {
     // root htmlTag='#document' id=1
     // ++paragraph htmlTag='p' id=2
@@ -631,6 +705,7 @@
         is_backward: false,
       },
     };
+    setOnSelectionChangeForTest();
     chrome.readAnything.setContentForTesting(axTree, []);
     // The expected string contains the complete text of each node in the
     // selection.
@@ -708,6 +783,7 @@
         is_backward: true,
       },
     };
+    setOnSelectionChangeForTest();
     chrome.readAnything.setContentForTesting(axTree, []);
     // The expected string contains the complete text of each node in the
     // selection.
@@ -751,6 +827,7 @@
         },
       ],
     };
+    setOnSelectionChangeForTest();
     chrome.readAnything.setContentForTesting(axTree, [1]);
     const expected = '<div>HelloWorldFriend</div>';
     assertContainerInnerHTML(expected);
diff --git a/chrome/updater/mac/keystone/agent_main.cc b/chrome/updater/mac/keystone/agent_main.cc
index 537cc5b..e246d74 100644
--- a/chrome/updater/mac/keystone/agent_main.cc
+++ b/chrome/updater/mac/keystone/agent_main.cc
@@ -2,17 +2,186 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <iostream>
+#include <map>
+#include <string>
+#include <utility>
+
+#include "base/at_exit.h"
 #include "base/command_line.h"
 #include "base/files/file_path.h"
+#include "base/functional/bind.h"
+#include "base/functional/callback.h"
+#include "base/functional/callback_helpers.h"
+#include "base/logging.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/message_loop/message_pump_type.h"
+#include "base/path_service.h"
 #include "base/process/launch.h"
+#include "base/ranges/algorithm.h"
+#include "base/strings/string_util.h"
+#include "base/task/single_thread_task_executor.h"
+#include "base/task/thread_pool/thread_pool_instance.h"
+#include "chrome/updater/app/app.h"
 #include "chrome/updater/constants.h"
+#include "chrome/updater/ipc/ipc_support.h"
+#include "chrome/updater/service_proxy_factory.h"
+#include "chrome/updater/update_service.h"
 #include "chrome/updater/updater_scope.h"
 #include "chrome/updater/util/util.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace updater {
 
-void Main() {
+namespace {
+
+constexpr char kCommandProductID[] = "productIDToUpdate";
+constexpr char kCommandPrintResults[] = "printResults";
+constexpr char kCommandUserInitiated[] = "userInitiated";
+
+// base::CommandLine can't be used because it is case-insensitive, and it does
+// not support long switches name prefixed with single '-'. This argument parser
+// converts an argv set into a map of switch name to switch value; for example
+//    `program_name -productIDToUpdate com.google.chrome -printResults YES`
+// is converted to:
+//    `{"productIDToUpdate": "com.google.chrome", "printResults": "YES"}`.
+std::map<std::string, std::string> ParseCommandLine(int argc,
+                                                    const char* argv[]) {
+  std::map<std::string, std::string> result;
+  std::string key;
+  for (int i = 1; i < argc; ++i) {
+    std::string arg(argv[i]);
+    if (base::StartsWith(arg, "-")) {
+      key = arg.substr(1);
+      result[key] = "";
+    } else {
+      if (!key.empty()) {
+        result[key] = arg;
+      }
+      key = "";
+    }
+  }
+  return result;
+}
+
+UpdaterScope Scope() {
+  base::FilePath executable_path;
+  if (base::PathService::Get(base::FILE_EXE, &executable_path) &&
+      base::StartsWith(executable_path.value(),
+                       GetKeystoneFolderPath(UpdaterScope::kSystem)->value())) {
+    return UpdaterScope::kSystem;
+  } else {
+    return UpdaterScope::kUser;
+  }
+}
+
+class KSAgentApp : public App {
+ public:
+  explicit KSAgentApp(const std::map<std::string, std::string>& switches)
+      : switches_(switches) {}
+
+ private:
+  ~KSAgentApp() override = default;
+  void FirstTaskRun() override;
+
+  void ChooseServiceForApp(
+      const std::string& app_id,
+      base::OnceCallback<void(UpdaterScope scope)> callback) const;
+
+  bool HasSwitch(const std::string& arg) const;
+  std::string SwitchValue(const std::string& arg) const;
+
+  void UpdateApp(const std::string& app_id);
+  void DoUpdate(const std::string& app_id, UpdaterScope scope);
+  void RecordUpdateResult(const UpdateService::UpdateState& update_state);
+  void PrintUpdateResultAndShutDown(UpdateService::Result result);
+
+  void Wake();
+
+  const std::map<std::string, std::string> switches_;
+  scoped_refptr<UpdateService> system_service_proxy_ =
+      CreateUpdateServiceProxy(UpdaterScope::kSystem);
+  scoped_refptr<UpdateService> user_service_proxy_ =
+      CreateUpdateServiceProxy(UpdaterScope::kUser);
+  int successful_install_count_ = 0;
+  ScopedIPCSupportWrapper ipc_support_;
+};
+
+void KSAgentApp::ChooseServiceForApp(
+    const std::string& app_id,
+    base::OnceCallback<void(UpdaterScope)> callback) const {
+  // Choose system scope if the app is a system app, otherwise choose
+  // user scope.
+  system_service_proxy_->GetAppStates(base::BindOnce(
+      [](const std::string& app_id,
+         base::OnceCallback<void(UpdaterScope)> callback,
+         const std::vector<updater::UpdateService::AppState>& states) {
+        std::move(callback).Run(
+            base::ranges::find_if(
+                states,
+                [&app_id](const updater::UpdateService::AppState& state) {
+                  return base::EqualsCaseInsensitiveASCII(state.app_id, app_id);
+                }) == std::end(states)
+                ? UpdaterScope::kUser
+                : UpdaterScope::kSystem);
+      },
+      app_id, std::move(callback)));
+}
+
+bool KSAgentApp::HasSwitch(const std::string& arg) const {
+  return base::Contains(switches_, arg);
+}
+
+std::string KSAgentApp::SwitchValue(const std::string& arg) const {
+  return HasSwitch(arg) ? switches_.at(arg) : std::string();
+}
+
+void KSAgentApp::RecordUpdateResult(
+    const UpdateService::UpdateState& update_state) {
+  if (update_state.state == UpdateService::UpdateState::State::kUpdated) {
+    // We don't need an accurate number of successful installations. A positive
+    // integer is enough to indicate that some updates are installed.
+    VLOG(0) << "An app update is installed successfully.";
+    successful_install_count_ += 1;
+  }
+}
+
+void KSAgentApp::PrintUpdateResultAndShutDown(UpdateService::Result result) {
+  // The output string format need to be strictly followed because this is the
+  // contract between the registration framework and the agent. The registration
+  // framework parses the agent outputs and broadcasts the parsed results via
+  // macOS notification center. Apps (like Chrome) can monitor the notification
+  // center and update the UI accordingly.
+  std::cout << "updateCheckSuccessful_="
+            << (result == UpdateService::Result::kSuccess ? "YES" : "NO")
+            << std::endl;
+  std::cout << "successfulInstallCount_=" << successful_install_count_
+            << std::endl;
+
+  Shutdown(result == UpdateService::Result::kSuccess ? 0 : 1);
+}
+
+void KSAgentApp::UpdateApp(const std::string& app_id) {
+  ChooseServiceForApp(app_id,
+                      base::BindOnce(&KSAgentApp::DoUpdate, this, app_id));
+}
+
+void KSAgentApp::DoUpdate(const std::string& app_id, UpdaterScope scope) {
+  VLOG(0) << "Updating " << app_id << " at "
+          << (scope == UpdaterScope::kSystem ? "system" : "user") << " scope.";
+  scoped_refptr<UpdateService> service_proxy = scope == UpdaterScope::kSystem
+                                                   ? system_service_proxy_
+                                                   : user_service_proxy_;
+  service_proxy->Update(
+      app_id, "", UpdateService::Priority::kForeground,
+      UpdateService::PolicySameVersionUpdate::kNotAllowed,
+      /*do_update_check_only=*/false,
+      base::BindRepeating(&KSAgentApp::RecordUpdateResult, this),
+      base::BindOnce(&KSAgentApp::PrintUpdateResultAndShutDown, this));
+}
+
+void KSAgentApp::Wake() {
+  VLOG(0) << "Launching wake processes.";
   for (UpdaterScope scope : {UpdaterScope::kSystem, UpdaterScope::kUser}) {
     absl::optional<base::FilePath> path = GetUpdaterExecutablePath(scope);
     if (!path) {
@@ -27,14 +196,40 @@
     command.AppendSwitchNative(kLoggingModuleSwitch, kLoggingModuleSwitchValue);
     base::LaunchProcess(command, {});
   }
+  Shutdown(0);
 }
 
+void KSAgentApp::FirstTaskRun() {
+  // The agent is a shim to trick the keystone registration framework.
+  if (!SwitchValue(kCommandProductID).empty() &&
+      SwitchValue(kCommandPrintResults) == "YES" &&
+      SwitchValue(kCommandUserInitiated) == "YES") {
+    // If the agent is run with explicit arguments to print the update result,
+    // make a call directly into the service to get update details.
+    UpdateApp(SwitchValue(kCommandProductID));
+  } else {
+    // Otherwise, just launch the --wake task. Not all callers correctly
+    // provide a scope, so it will wake both scopes (if present).
+    Wake();
+  }
+}
+
+int KSAgentAppMain(int argc, const char* argv[]) {
+  base::AtExitManager exit_manager;
+  base::CommandLine::Init(argc, argv);
+  updater::InitLogging(Scope());
+  InitializeThreadPool("keystone");
+  const base::ScopedClosureRunner shutdown_thread_pool(
+      base::BindOnce([]() { base::ThreadPoolInstance::Get()->Shutdown(); }));
+  base::SingleThreadTaskExecutor main_task_executor(base::MessagePumpType::UI);
+
+  return base::MakeRefCounted<KSAgentApp>(ParseCommandLine(argc, argv))->Run();
+}
+
+}  // namespace
+
 }  // namespace updater
 
-// The agent is a shim to trick the keystone registration framework. When run,
-// it should launch the --wake task. Not all callers correctly provide a scope,
-// so it will wake both scopes (if present).
-int main() {
-  updater::Main();
-  return 0;
+int main(int argc, const char* argv[]) {
+  return updater::KSAgentAppMain(argc, argv);
 }
diff --git a/chromeos/chromeos_strings.grd b/chromeos/chromeos_strings.grd
index 4fdc8b1..363cc9f4 100644
--- a/chromeos/chromeos_strings.grd
+++ b/chromeos/chromeos_strings.grd
@@ -2609,6 +2609,9 @@
         <message name="IDS_PERSONALIZATION_APP_KEYBOARD_BACKLIGHT_WALLPAPER_COLOR_DESCRIPTION" desc="The description for the button that allows users to select a keyboard backlight wallpaper-extracted color.">
           Match wallpaper
         </message>
+        <message name="IDS_PERSONALIZATION_APP_KEYBOARD_BACKLIGHT_ZONE_TITLE" desc="Title of the zone colors in the keyboard backlight customization dialog of personalization hub.">
+          Zone <ph name="ZONE_NUMBER">$1<ex>1</ex></ph>
+        </message>
 
         <!-- Personalization App Search Results -->
         <message name="IDS_PERSONALIZATION_APP_SEARCH_RESULT_TITLE" desc="Text for search result item which, when clicked, navigates the user to personalization app.">
diff --git a/chromeos/chromeos_strings_grd/IDS_PERSONALIZATION_APP_KEYBOARD_BACKLIGHT_ZONE_TITLE.png.sha1 b/chromeos/chromeos_strings_grd/IDS_PERSONALIZATION_APP_KEYBOARD_BACKLIGHT_ZONE_TITLE.png.sha1
new file mode 100644
index 0000000..df01c17d
--- /dev/null
+++ b/chromeos/chromeos_strings_grd/IDS_PERSONALIZATION_APP_KEYBOARD_BACKLIGHT_ZONE_TITLE.png.sha1
@@ -0,0 +1 @@
+cf0c4358e688f5ca594cd2883837591741e36982
\ No newline at end of file
diff --git a/components/autofill/core/browser/BUILD.gn b/components/autofill/core/browser/BUILD.gn
index c02d908..a76dc61 100644
--- a/components/autofill/core/browser/BUILD.gn
+++ b/components/autofill/core/browser/BUILD.gn
@@ -314,6 +314,7 @@
     "payments/card_unmask_challenge_option.h",
     "payments/card_unmask_delegate.cc",
     "payments/card_unmask_delegate.h",
+    "payments/client_behavior_constants.h",
     "payments/credit_card_access_manager.cc",
     "payments/credit_card_access_manager.h",
     "payments/credit_card_cvc_authenticator.cc",
diff --git a/components/autofill/core/browser/payments/client_behavior_constants.h b/components/autofill/core/browser/payments/client_behavior_constants.h
new file mode 100644
index 0000000..8eb40a2a
--- /dev/null
+++ b/components/autofill/core/browser/payments/client_behavior_constants.h
@@ -0,0 +1,27 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_AUTOFILL_CORE_BROWSER_PAYMENTS_CLIENT_BEHAVIOR_CONSTANTS_H_
+#define COMPONENTS_AUTOFILL_CORE_BROWSER_PAYMENTS_CLIENT_BEHAVIOR_CONSTANTS_H_
+
+namespace autofill {
+enum class ClientBehaviorConstants {
+  // ClientBehaviorConstant encompasses all the active client behaviors for the
+  // browser during the outgoing calls to the Payments server.
+  // Active client behaviors are the enums outlined below which tell a specific
+  // feature/experiment that is triggered/active on the browser.
+  // These enum flags are persisted to the Payments server logs. Entries should
+  // not be renumbered and numeric values should never be reused.
+  // Note that the number of the behavior, not the name, is sent in the JSON
+  // request to Payments.
+
+  // For more information on this signal, see AutofillEnableNewSaveCardBubbleUi
+  // flag. This enum is to be always included in the client_behavior_signals
+  // from M113 onwards as this retrieves the correct TOS footer for
+  // FasterAndProtected bubble.
+  kUsingFasterAndProtectedUi = 1,
+};
+}  // namespace autofill
+
+#endif  // COMPONENTS_AUTOFILL_CORE_BROWSER_PAYMENTS_CLIENT_BEHAVIOR_CONSTANTS_H_
diff --git a/components/autofill/core/browser/payments/credit_card_save_manager.cc b/components/autofill/core/browser/payments/credit_card_save_manager.cc
index a107b75b..73f5b229 100644
--- a/components/autofill/core/browser/payments/credit_card_save_manager.cc
+++ b/components/autofill/core/browser/payments/credit_card_save_manager.cc
@@ -25,6 +25,7 @@
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "build/build_config.h"
+#include "client_behavior_constants.h"
 #include "components/autofill/core/browser/autofill_client.h"
 #include "components/autofill/core/browser/autofill_experiments.h"
 #include "components/autofill/core/browser/autofill_type.h"
@@ -33,6 +34,7 @@
 #include "components/autofill/core/browser/form_structure.h"
 #include "components/autofill/core/browser/logging/log_manager.h"
 #include "components/autofill/core/browser/metrics/autofill_metrics.h"
+#include "components/autofill/core/browser/payments/client_behavior_constants.h"
 #include "components/autofill/core/browser/payments/payments_client.h"
 #include "components/autofill/core/browser/payments/payments_util.h"
 #include "components/autofill/core/browser/payments/virtual_card_enrollment_manager.h"
@@ -277,9 +279,17 @@
   show_save_prompt_ = !GetCreditCardSaveStrikeDatabase()->ShouldBlockFeature(
       base::UTF16ToUTF8(upload_request_.card.LastFourDigits()));
 
+#if !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_ANDROID)
+  if (base::FeatureList::IsEnabled(
+          features::kAutofillEnableNewSaveCardBubbleUi)) {
+    upload_request_.client_behavior_signals.push_back(
+        ClientBehaviorConstants::kUsingFasterAndProtectedUi);
+  }
+#endif  // !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_ANDROID)
+
   payments_client_->GetUploadDetails(
       country_only_profiles, upload_request_.detected_values,
-      upload_request_.active_experiments, app_locale_,
+      upload_request_.client_behavior_signals, app_locale_,
       base::BindOnce(&CreditCardSaveManager::OnDidGetUploadDetails,
                      weak_ptr_factory_.GetWeakPtr()),
       payments::kUploadCardBillableServiceNumber,
diff --git a/components/autofill/core/browser/payments/credit_card_save_manager_unittest.cc b/components/autofill/core/browser/payments/credit_card_save_manager_unittest.cc
index 8eaa8d2..ba144ff 100644
--- a/components/autofill/core/browser/payments/credit_card_save_manager_unittest.cc
+++ b/components/autofill/core/browser/payments/credit_card_save_manager_unittest.cc
@@ -30,6 +30,7 @@
 #include "components/autofill/core/browser/data_model/credit_card.h"
 #include "components/autofill/core/browser/metrics/autofill_metrics.h"
 #include "components/autofill/core/browser/metrics/payments/credit_card_save_metrics.h"
+#include "components/autofill/core/browser/payments/client_behavior_constants.h"
 #include "components/autofill/core/browser/payments/payments_customer_data.h"
 #include "components/autofill/core/browser/payments/payments_util.h"
 #include "components/autofill/core/browser/payments/test_credit_card_save_manager.h"
@@ -568,7 +569,7 @@
   FormSubmitted(credit_card_form);
   EXPECT_FALSE(autofill_client_.ConfirmSaveCardLocallyWasCalled());
   EXPECT_TRUE(credit_card_save_manager_->CreditCardWasUploaded());
-  EXPECT_TRUE(payments_client_->active_experiments_in_request().empty());
+  EXPECT_TRUE(payments_client_->client_behavior_signals_in_request().empty());
 
   // Verify that even though the full address profile was saved, only the
   // country was included in the upload details request to payments.
@@ -1689,6 +1690,70 @@
 }
 #endif
 
+#if !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_ANDROID)
+TEST_F(CreditCardSaveManagerTest,
+       AttemptToOfferCardUploadSave_AutofillEnableNewSaveCardBubbleUiEnabled) {
+  // Setting the flag to enable the new bubble for Save Card UI.
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndEnableFeature(
+      features::kAutofillEnableNewSaveCardBubbleUi);
+
+  // Set up our credit card form data.
+  FormData credit_card_form;
+  CreateTestCreditCardFormData(&credit_card_form, CreditCardFormOptions());
+  FormsSeen(std::vector<FormData>(1, credit_card_form));
+
+  // Edit the data, and submit.
+  credit_card_form.fields[0].value = u"Jane Doe";
+  credit_card_form.fields[1].value = u"4111111111111111";
+  credit_card_form.fields[2].value = ASCIIToUTF16(test::NextMonth());
+  credit_card_form.fields[3].value = ASCIIToUTF16(test::NextYear());
+  credit_card_form.fields[4].value = u"123";
+  FormSubmitted(credit_card_form);
+
+  EXPECT_FALSE(autofill_client_.ConfirmSaveCardLocallyWasCalled());
+  EXPECT_TRUE(credit_card_save_manager_->CreditCardWasUploaded());
+
+  // Confirm that client_behavior_signals vector does contain the
+  // FasterAndProtected signal.
+  std::vector<ClientBehaviorConstants> client_behavior_signals_in_request =
+      payments_client_->client_behavior_signals_in_request();
+  EXPECT_THAT(
+      client_behavior_signals_in_request,
+      testing::Contains(ClientBehaviorConstants::kUsingFasterAndProtectedUi));
+}
+
+TEST_F(CreditCardSaveManagerTest,
+       AttemptToOfferCardUploadSave_AutofillEnableNewSaveCardBubbleUiDisabled) {
+  // Setting the flag to disable the new bubble for Save Card UI.
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndDisableFeature(
+      features::kAutofillEnableNewSaveCardBubbleUi);
+
+  FormData credit_card_form;
+  CreateTestCreditCardFormData(&credit_card_form, CreditCardFormOptions());
+  FormsSeen(std::vector<FormData>(1, credit_card_form));
+
+  credit_card_form.fields[0].value = u"Jane Doe";
+  credit_card_form.fields[1].value = u"4111111111111111";
+  credit_card_form.fields[2].value = ASCIIToUTF16(test::NextMonth());
+  credit_card_form.fields[3].value = ASCIIToUTF16(test::NextYear());
+  credit_card_form.fields[4].value = u"123";
+  FormSubmitted(credit_card_form);
+
+  EXPECT_FALSE(autofill_client_.ConfirmSaveCardLocallyWasCalled());
+  EXPECT_TRUE(credit_card_save_manager_->CreditCardWasUploaded());
+
+  // Confirm that client_behavior_signals vector does not contain the
+  // FasterAndProtected signal.
+  std::vector<ClientBehaviorConstants> client_behavior_signals_in_request =
+      payments_client_->client_behavior_signals_in_request();
+  EXPECT_THAT(client_behavior_signals_in_request,
+              testing::Not(testing::Contains(
+                  ClientBehaviorConstants::kUsingFasterAndProtectedUi)));
+}
+#endif  // !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_ANDROID)
+
 // TODO(crbug.com/1113034): Create an equivalent test for iOS, or skip
 // permanently if the test doesn't apply to iOS flow.
 #if !BUILDFLAG(IS_IOS)
@@ -4217,7 +4282,7 @@
   FormSubmitted(credit_card_form);
   EXPECT_FALSE(autofill_client_.ConfirmSaveCardLocallyWasCalled());
   EXPECT_TRUE(credit_card_save_manager_->CreditCardWasUploaded());
-  EXPECT_TRUE(payments_client_->active_experiments_in_request().empty());
+  EXPECT_TRUE(payments_client_->client_behavior_signals_in_request().empty());
 }
 
 TEST_F(CreditCardSaveManagerTest,
diff --git a/components/autofill/core/browser/payments/local_card_migration_manager.cc b/components/autofill/core/browser/payments/local_card_migration_manager.cc
index adafc6e..6e5a237 100644
--- a/components/autofill/core/browser/payments/local_card_migration_manager.cc
+++ b/components/autofill/core/browser/payments/local_card_migration_manager.cc
@@ -20,6 +20,7 @@
 #include "components/autofill/core/browser/form_data_importer.h"
 #include "components/autofill/core/browser/metrics/autofill_metrics.h"
 #include "components/autofill/core/browser/metrics/payments/local_card_migration_metrics.h"
+#include "components/autofill/core/browser/payments/client_behavior_constants.h"
 #include "components/autofill/core/browser/payments/payments_client.h"
 #include "components/autofill/core/browser/payments/payments_util.h"
 #include "components/autofill/core/browser/personal_data_manager.h"
@@ -139,7 +140,8 @@
 
   payments_client_->GetUploadDetails(
       std::vector<AutofillProfile>(), GetDetectedValues(),
-      /*active_experiments=*/std::vector<const char*>(), app_locale_,
+      /*client_behavior_signals=*/std::vector<ClientBehaviorConstants>(),
+      app_locale_,
       base::BindOnce(&LocalCardMigrationManager::OnDidGetUploadDetails,
                      weak_ptr_factory_.GetWeakPtr(), is_from_settings_page),
       payments::kMigrateCardsBillableServiceNumber,
diff --git a/components/autofill/core/browser/payments/payments_client.cc b/components/autofill/core/browser/payments/payments_client.cc
index 5e6cada..7ebdc1a 100644
--- a/components/autofill/core/browser/payments/payments_client.cc
+++ b/components/autofill/core/browser/payments/payments_client.cc
@@ -22,6 +22,7 @@
 #include "components/autofill/core/browser/data_model/autofill_data_model.h"
 #include "components/autofill/core/browser/data_model/credit_card.h"
 #include "components/autofill/core/browser/payments/account_info_getter.h"
+#include "components/autofill/core/browser/payments/client_behavior_constants.h"
 #include "components/autofill/core/browser/payments/local_card_migration_manager.h"
 #include "components/autofill/core/browser/payments/payments_requests/get_details_for_enrollment_request.h"
 #include "components/autofill/core/browser/payments/payments_requests/get_unmask_details_request.h"
@@ -278,7 +279,7 @@
 void PaymentsClient::GetUploadDetails(
     const std::vector<AutofillProfile>& addresses,
     const int detected_values,
-    const std::vector<const char*>& active_experiments,
+    const std::vector<ClientBehaviorConstants>& client_behavior_signals,
     const std::string& app_locale,
     base::OnceCallback<void(AutofillClient::PaymentsRpcResult,
                             const std::u16string&,
@@ -288,7 +289,7 @@
     const int64_t billing_customer_number,
     UploadCardSource upload_card_source) {
   IssueRequest(std::make_unique<GetUploadDetailsRequest>(
-                   addresses, detected_values, active_experiments,
+                   addresses, detected_values, client_behavior_signals,
                    account_info_getter_->IsSyncFeatureEnabled(), app_locale,
                    std::move(callback), billable_service_number,
                    billing_customer_number, upload_card_source),
diff --git a/components/autofill/core/browser/payments/payments_client.h b/components/autofill/core/browser/payments/payments_client.h
index 09ce205..833a8b2 100644
--- a/components/autofill/core/browser/payments/payments_client.h
+++ b/components/autofill/core/browser/payments/payments_client.h
@@ -21,6 +21,7 @@
 #include "components/autofill/core/browser/payments/autofill_error_dialog_context.h"
 #include "components/autofill/core/browser/payments/card_unmask_challenge_option.h"
 #include "components/autofill/core/browser/payments/card_unmask_delegate.h"
+#include "components/autofill/core/browser/payments/client_behavior_constants.h"
 #include "components/autofill/core/browser/payments/virtual_card_enrollment_flow.h"
 #include "google_apis/gaia/google_service_auth_error.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
@@ -341,7 +342,7 @@
     std::u16string context_token;
     std::string risk_data;
     std::string app_locale;
-    std::vector<const char*> active_experiments;
+    std::vector<ClientBehaviorConstants> client_behavior_signals;
   };
 
   // An enum set in the GetUploadDetailsRequest indicating the source of the
@@ -452,14 +453,13 @@
   // decisions. |callback| is the callback function when get response from
   // server. |billable_service_number| is used to set the billable service
   // number in the GetUploadDetails request. If the conditions are met, the
-  // legal message will be returned via |callback|. |active_experiments| is used
-  // by Payments server to track requests that were triggered by enabled
-  // features. |upload_card_source| is used by Payments server metrics to track
-  // the source of the request.
+  // legal message will be returned via |callback|. |client_behavior_signals| is
+  // used by Payments server to track Chrome behaviors. |upload_card_source| is
+  // used by Payments server metrics to track the source of the request.
   virtual void GetUploadDetails(
       const std::vector<AutofillProfile>& addresses,
       const int detected_values,
-      const std::vector<const char*>& active_experiments,
+      const std::vector<ClientBehaviorConstants>& client_behavior_signals,
       const std::string& app_locale,
       base::OnceCallback<void(AutofillClient::PaymentsRpcResult,
                               const std::u16string&,
diff --git a/components/autofill/core/browser/payments/payments_client_unittest.cc b/components/autofill/core/browser/payments/payments_client_unittest.cc
index 50d81c6..8f56768 100644
--- a/components/autofill/core/browser/payments/payments_client_unittest.cc
+++ b/components/autofill/core/browser/payments/payments_client_unittest.cc
@@ -21,6 +21,7 @@
 #include "components/autofill/core/browser/autofill_test_utils.h"
 #include "components/autofill/core/browser/payments/autofill_error_dialog_context.h"
 #include "components/autofill/core/browser/payments/card_unmask_challenge_option.h"
+#include "components/autofill/core/browser/payments/client_behavior_constants.h"
 #include "components/autofill/core/browser/payments/credit_card_save_manager.h"
 #include "components/autofill/core/browser/payments/local_card_migration_manager.h"
 #include "components/autofill/core/browser/payments/payments_client.h"
@@ -38,8 +39,11 @@
 #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
 #include "services/network/test/test_url_loader_factory.h"
 #include "services/network/test/test_utils.h"
+#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+using ::testing::HasSubstr;
+
 namespace autofill::payments {
 namespace {
 
@@ -138,9 +142,16 @@
     return *this;
   }
 
+  GetUploadDetailsOptions& with_client_behavior_signals(
+      std::vector<ClientBehaviorConstants> v) {
+    client_behavior_signals = std::move(v);
+    return *this;
+  }
+
   PaymentsClient::UploadCardSource upload_card_source =
       PaymentsClient::UploadCardSource::UNKNOWN_UPLOAD_CARD_SOURCE;
   int64_t billing_customer_number = 111222333444L;
+  std::vector<ClientBehaviorConstants> client_behavior_signals;
 };
 
 struct UploadCardOptions {
@@ -159,9 +170,16 @@
     return *this;
   }
 
+  UploadCardOptions& with_client_behavior_signals(
+      std::vector<ClientBehaviorConstants> v) {
+    client_behavior_signals = std::move(v);
+    return *this;
+  }
+
   bool include_cvc = false;
   bool include_nickname = false;
   int64_t billing_customer_number = 111222333444L;
+  std::vector<ClientBehaviorConstants> client_behavior_signals;
 };
 
 }  // namespace
@@ -351,8 +369,8 @@
   void StartGettingUploadDetails(
       GetUploadDetailsOptions get_upload_details_options) {
     client_->GetUploadDetails(
-        BuildTestProfiles(), kAllDetectableValues, std::vector<const char*>(),
-        "language-LOCALE",
+        BuildTestProfiles(), kAllDetectableValues,
+        get_upload_details_options.client_behavior_signals, "language-LOCALE",
         base::BindOnce(&PaymentsClientTest::OnDidGetUploadDetails,
                        weak_ptr_factory_.GetWeakPtr()),
         /*billable_service_number=*/12345,
@@ -374,6 +392,9 @@
       upstream_nickname_ = u"grocery";
       request_details.card.SetNickname(upstream_nickname_);
     }
+    request_details.client_behavior_signals =
+        upload_card_options.client_behavior_signals;
+
     request_details.context_token = u"context token";
     request_details.risk_data = "some risk data";
     request_details.app_locale = "language-LOCALE";
@@ -1176,6 +1197,27 @@
 }
 
 TEST_F(PaymentsClientTest,
+       GetDetailsIncludesIncludesClientBehaviorSignalsInChromeUserContext) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      features::kAutofillEnableNewSaveCardBubbleUi);
+
+  StartGettingUploadDetails(
+      GetUploadDetailsOptions().with_client_behavior_signals(
+          std::vector<ClientBehaviorConstants>{
+              ClientBehaviorConstants::kUsingFasterAndProtectedUi}));
+
+  // Verify ChromeUserContext was set.
+  EXPECT_THAT(GetUploadData(), HasSubstr("chrome_user_context"));
+  // Verify Client_behavior_signals was set.
+  EXPECT_THAT(GetUploadData(), HasSubstr("client_behavior_signals"));
+  // Verify fake_client_behavior_signal was set.
+  // ClientBehaviorConstants::kUsingFasterAndProtectedUi has the numeric value
+  // set to 1.
+  EXPECT_THAT(GetUploadData(), HasSubstr("\"client_behavior_signals\":[1]"));
+}
+
+TEST_F(PaymentsClientTest,
        GetDetailsIncludesChromeUserContextIfWalletStorageFlagEnabled) {
   base::test::ScopedFeatureList feature_list;
   feature_list.InitAndEnableFeature(
@@ -1550,6 +1592,27 @@
               std::string::npos);
 }
 
+TEST_F(PaymentsClientTest, UploadRequestIncludesClientBehaviorSignals) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      features::kAutofillEnableNewSaveCardBubbleUi);
+
+  StartUploading(UploadCardOptions().with_client_behavior_signals(
+      std::vector<ClientBehaviorConstants>{
+          ClientBehaviorConstants::kUsingFasterAndProtectedUi}));
+  IssueOAuthToken();
+
+  // Verify ChromeUserContext was set.
+  EXPECT_THAT(GetUploadData(), HasSubstr("chrome_user_context"));
+  // Verify Client_behavior_signals was set.
+  EXPECT_THAT(GetUploadData(), HasSubstr("client_behavior_signals"));
+  // Verify fake_client_behavior_signal was set.
+  // ClientBehaviorConstants::kUsingFasterAndProtectedUi has the numeric value
+  // set to 1.
+  EXPECT_THAT(GetUploadData(),
+              HasSubstr("%22client_behavior_signals%22:%5B1%5D"));
+}
+
 TEST_F(PaymentsClientTest,
        UploadRequestIncludesEncryptedPanUsingAlternateType) {
   base::test::ScopedFeatureList feature_list;
diff --git a/components/autofill/core/browser/payments/payments_requests/get_upload_details_request.cc b/components/autofill/core/browser/payments/payments_requests/get_upload_details_request.cc
index ca3af53..934cd62 100644
--- a/components/autofill/core/browser/payments/payments_requests/get_upload_details_request.cc
+++ b/components/autofill/core/browser/payments/payments_requests/get_upload_details_request.cc
@@ -11,6 +11,7 @@
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/values.h"
+#include "components/autofill/core/browser/payments/client_behavior_constants.h"
 
 namespace autofill::payments {
 
@@ -22,7 +23,7 @@
 GetUploadDetailsRequest::GetUploadDetailsRequest(
     const std::vector<AutofillProfile>& addresses,
     const int detected_values,
-    const std::vector<const char*>& active_experiments,
+    const std::vector<ClientBehaviorConstants>& client_behavior_signals,
     const bool full_sync_enabled,
     const std::string& app_locale,
     base::OnceCallback<void(AutofillClient::PaymentsRpcResult,
@@ -34,7 +35,7 @@
     PaymentsClient::UploadCardSource upload_card_source)
     : addresses_(addresses),
       detected_values_(detected_values),
-      active_experiments_(active_experiments),
+      client_behavior_signals_(client_behavior_signals),
       full_sync_enabled_(full_sync_enabled),
       app_locale_(app_locale),
       callback_(std::move(callback)),
@@ -62,10 +63,9 @@
                 BuildCustomerContextDictionary(billing_customer_number_));
   }
   request_dict.Set("context", std::move(context));
-
-  base::Value::Dict chrome_user_context;
-  chrome_user_context.Set("full_sync_enabled", full_sync_enabled_);
-  request_dict.Set("chrome_user_context", std::move(chrome_user_context));
+  request_dict.Set(
+      "chrome_user_context",
+      BuildChromeUserContext(client_behavior_signals_, full_sync_enabled_));
 
   base::Value::List addresses;
   for (const AutofillProfile& profile : addresses_) {
@@ -85,8 +85,6 @@
   // Payments will decide if the provided data is enough to offer upload save.
   request_dict.Set("detected_values", detected_values_);
 
-  SetActiveExperiments(active_experiments_, request_dict);
-
   switch (upload_card_source_) {
     case PaymentsClient::UploadCardSource::UNKNOWN_UPLOAD_CARD_SOURCE:
       request_dict.Set("upload_card_source", "UNKNOWN_UPLOAD_CARD_SOURCE");
diff --git a/components/autofill/core/browser/payments/payments_requests/get_upload_details_request.h b/components/autofill/core/browser/payments/payments_requests/get_upload_details_request.h
index 8f0cdde5..1023845 100644
--- a/components/autofill/core/browser/payments/payments_requests/get_upload_details_request.h
+++ b/components/autofill/core/browser/payments/payments_requests/get_upload_details_request.h
@@ -10,6 +10,7 @@
 #include "base/functional/callback.h"
 #include "base/values.h"
 #include "components/autofill/core/browser/autofill_client.h"
+#include "components/autofill/core/browser/payments/client_behavior_constants.h"
 #include "components/autofill/core/browser/payments/payments_client.h"
 #include "components/autofill/core/browser/payments/payments_requests/payments_request.h"
 
@@ -20,7 +21,7 @@
   GetUploadDetailsRequest(
       const std::vector<AutofillProfile>& addresses,
       const int detected_values,
-      const std::vector<const char*>& active_experiments,
+      const std::vector<ClientBehaviorConstants>& client_behavior_signals,
       const bool full_sync_enabled,
       const std::string& app_locale,
       base::OnceCallback<void(AutofillClient::PaymentsRpcResult,
@@ -52,7 +53,7 @@
 
   const std::vector<AutofillProfile> addresses_;
   const int detected_values_;
-  const std::vector<const char*> active_experiments_;
+  const std::vector<ClientBehaviorConstants> client_behavior_signals_;
   const bool full_sync_enabled_;
   std::string app_locale_;
   base::OnceCallback<void(AutofillClient::PaymentsRpcResult,
diff --git a/components/autofill/core/browser/payments/payments_requests/payments_request.cc b/components/autofill/core/browser/payments/payments_requests/payments_request.cc
index 3924679..04df0b5 100644
--- a/components/autofill/core/browser/payments/payments_requests/payments_request.cc
+++ b/components/autofill/core/browser/payments/payments_requests/payments_request.cc
@@ -7,8 +7,10 @@
 #include <utility>
 
 #include "base/strings/string_number_conversions.h"
+#include "base/types/cxx23_to_underlying.h"
 #include "base/values.h"
 #include "build/build_config.h"
+#include "components/autofill/core/browser/payments/client_behavior_constants.h"
 #include "components/autofill/core/browser/payments/payments_client.h"
 
 namespace autofill::payments {
@@ -53,18 +55,20 @@
   return customer_context;
 }
 
-void PaymentsRequest::SetActiveExperiments(
-    const std::vector<const char*>& active_experiments,
-    base::Value::Dict& request_dict) {
-  if (active_experiments.empty())
-    return;
-
-  base::Value::List active_chrome_experiments;
-  for (const char* experiment : active_experiments)
-    active_chrome_experiments.Append(experiment);
-
-  request_dict.Set("active_chrome_experiments",
-                   std::move(active_chrome_experiments));
+base::Value::Dict PaymentsRequest::BuildChromeUserContext(
+    const std::vector<ClientBehaviorConstants>& client_behavior_signals,
+    bool full_sync_enabled) {
+  base::Value::Dict chrome_user_context;
+  chrome_user_context.Set("full_sync_enabled", full_sync_enabled);
+  if (!client_behavior_signals.empty()) {
+    base::Value::List active_client_signals;
+    for (ClientBehaviorConstants signal : client_behavior_signals) {
+      active_client_signals.Append(base::to_underlying(signal));
+    }
+    chrome_user_context.Set("client_behavior_signals",
+                            std::move(active_client_signals));
+  }
+  return chrome_user_context;
 }
 
 base::Value::Dict PaymentsRequest::BuildAddressDictionary(
diff --git a/components/autofill/core/browser/payments/payments_requests/payments_request.h b/components/autofill/core/browser/payments/payments_requests/payments_request.h
index 457d75a..8d0f48fb 100644
--- a/components/autofill/core/browser/payments/payments_requests/payments_request.h
+++ b/components/autofill/core/browser/payments/payments_requests/payments_request.h
@@ -11,6 +11,7 @@
 #include "components/autofill/core/browser/autofill_client.h"
 #include "components/autofill/core/browser/data_model/autofill_profile.h"
 #include "components/autofill/core/browser/data_model/credit_card.h"
+#include "components/autofill/core/browser/payments/client_behavior_constants.h"
 #include "components/autofill/core/common/autofill_payments_features.h"
 
 namespace autofill::payments {
@@ -53,11 +54,11 @@
   base::Value::Dict BuildCustomerContextDictionary(
       int64_t external_customer_id);
 
-  // Shared helper function that populates the list of active experiments that
-  // affect either the data sent in payments RPCs or whether the RPCs are sent
-  // or not.
-  void SetActiveExperiments(const std::vector<const char*>& active_experiments,
-                            base::Value::Dict& request_dict);
+  // Shared helper function that builds the Chrome user context which is then
+  // set in the payment requests.
+  base::Value::Dict BuildChromeUserContext(
+      const std::vector<ClientBehaviorConstants>& client_behavior_signals,
+      bool full_sync_enabled);
 
   // Shared helper functoin that returns a dictionary with the structure
   // expected by Payments RPCs, containing each of the fields in |profile|,
diff --git a/components/autofill/core/browser/payments/payments_requests/upload_card_request.cc b/components/autofill/core/browser/payments/payments_requests/upload_card_request.cc
index 25987cc..1042348 100644
--- a/components/autofill/core/browser/payments/payments_requests/upload_card_request.cc
+++ b/components/autofill/core/browser/payments/payments_requests/upload_card_request.cc
@@ -76,10 +76,10 @@
   }
   request_dict.Set("context", std::move(context));
 
-  base::Value::Dict chrome_user_context;
-  chrome_user_context.Set("full_sync_enabled", full_sync_enabled_);
-  request_dict.Set("chrome_user_context", std::move(chrome_user_context));
-
+  request_dict.Set(
+      "chrome_user_context",
+      BuildChromeUserContext(request_details_.client_behavior_signals,
+                             full_sync_enabled_));
   SetStringIfNotEmpty(request_details_.card, CREDIT_CARD_NAME_FULL, app_locale,
                       "cardholder_name", request_dict);
 
@@ -105,8 +105,6 @@
     request_dict.Set("nickname", request_details_.card.nickname());
   }
 
-  SetActiveExperiments(request_details_.active_experiments, request_dict);
-
   const std::u16string pan = request_details_.card.GetInfo(
       AutofillType(CREDIT_CARD_NUMBER), app_locale);
   std::string json_request;
diff --git a/components/autofill/core/browser/payments/test_payments_client.cc b/components/autofill/core/browser/payments/test_payments_client.cc
index 42d57219..3d597b69 100644
--- a/components/autofill/core/browser/payments/test_payments_client.cc
+++ b/components/autofill/core/browser/payments/test_payments_client.cc
@@ -56,7 +56,7 @@
 void TestPaymentsClient::GetUploadDetails(
     const std::vector<AutofillProfile>& addresses,
     const int detected_values,
-    const std::vector<const char*>& active_experiments,
+    const std::vector<ClientBehaviorConstants>& client_behavior_signals,
     const std::string& app_locale,
     base::OnceCallback<void(AutofillClient::PaymentsRpcResult,
                             const std::u16string&,
@@ -67,7 +67,7 @@
     PaymentsClient::UploadCardSource upload_card_source) {
   upload_details_addresses_ = addresses;
   detected_values_ = detected_values;
-  active_experiments_ = active_experiments;
+  client_behavior_signals_ = client_behavior_signals;
   billable_service_number_ = billable_service_number;
   billing_customer_number_ = billing_customer_number;
   upload_card_source_ = upload_card_source;
@@ -85,7 +85,7 @@
                             const PaymentsClient::UploadCardResponseDetails&)>
         callback) {
   upload_card_addresses_ = request_details.profiles;
-  active_experiments_ = request_details.active_experiments;
+  client_behavior_signals_ = request_details.client_behavior_signals;
   std::move(callback).Run(AutofillClient::PaymentsRpcResult::kSuccess,
                           upload_card_response_details_);
 }
diff --git a/components/autofill/core/browser/payments/test_payments_client.h b/components/autofill/core/browser/payments/test_payments_client.h
index 363bd7b..bfd749a9 100644
--- a/components/autofill/core/browser/payments/test_payments_client.h
+++ b/components/autofill/core/browser/payments/test_payments_client.h
@@ -47,7 +47,7 @@
   void GetUploadDetails(
       const std::vector<AutofillProfile>& addresses,
       const int detected_values,
-      const std::vector<const char*>& active_experiments,
+      const std::vector<ClientBehaviorConstants>& client_behavior_signals,
       const std::string& app_locale,
       base::OnceCallback<void(AutofillClient::PaymentsRpcResult,
                               const std::u16string&,
@@ -139,8 +139,9 @@
   const std::vector<AutofillProfile>& addresses_in_upload_card() const {
     return upload_card_addresses_;
   }
-  const std::vector<const char*>& active_experiments_in_request() const {
-    return active_experiments_;
+  const std::vector<ClientBehaviorConstants>&
+  client_behavior_signals_in_request() const {
+    return client_behavior_signals_;
   }
   int billable_service_number_in_request() const {
     return billable_service_number_;
@@ -177,7 +178,7 @@
   std::vector<AutofillProfile> upload_card_addresses_;
   int detected_values_;
   std::string pan_first_six_;
-  std::vector<const char*> active_experiments_;
+  std::vector<ClientBehaviorConstants> client_behavior_signals_;
   int billable_service_number_;
   int64_t billing_customer_number_;
   PaymentsClient::UploadCardSource upload_card_source_;
diff --git a/components/autofill/core/browser/personal_data_manager.cc b/components/autofill/core/browser/personal_data_manager.cc
index 0a34ec4..739fb5ac 100644
--- a/components/autofill/core/browser/personal_data_manager.cc
+++ b/components/autofill/core/browser/personal_data_manager.cc
@@ -2138,51 +2138,22 @@
   return AddIBAN(imported_iban);
 }
 
-void PersonalDataManager::LogStoredProfileMetrics() const {
-  if (!has_logged_stored_profile_metrics_) {
-    autofill_metrics::LogStoredProfileMetrics(GetProfiles());
-    // Only log this info once per Chrome user profile load.
-    has_logged_stored_profile_metrics_ = true;
+void PersonalDataManager::LogStoredDataMetrics() const {
+  if (has_logged_stored_data_metrics_) {
+    return;
   }
-}
+  // Only log this info once per Chrome user profile load.
+  has_logged_stored_data_metrics_ = true;
 
-void PersonalDataManager::LogStoredCreditCardMetrics() const {
-  if (!has_logged_stored_credit_card_metrics_) {
-    AutofillMetrics::LogStoredCreditCardMetrics(
-        local_credit_cards_, server_credit_cards_,
-        GetServerCardWithArtImageCount(), kDisusedDataModelTimeDelta);
-
-    // Only log this info once per Chrome user profile load.
-    has_logged_stored_credit_card_metrics_ = true;
-  }
-}
-
-void PersonalDataManager::LogStoredIbanMetrics() const {
-  if (!has_logged_stored_iban_metrics_) {
-    autofill_metrics::LogStoredIbanMetrics(local_ibans_,
-                                           kDisusedDataModelTimeDelta);
-
-    // Only log this info once per Chrome user profile load.
-    has_logged_stored_iban_metrics_ = true;
-  }
-}
-
-void PersonalDataManager::LogStoredOfferMetrics() const {
-  if (!has_logged_stored_offer_metrics_) {
-    autofill_metrics::LogStoredOfferMetrics(autofill_offer_data_);
-    // Only log this info once per Chrome user profile load.
-    has_logged_stored_offer_metrics_ = true;
-  }
-}
-
-void PersonalDataManager::LogStoredVirtualCardUsageMetrics() const {
-  if (!has_logged_stored_virtual_card_usage_metrics_) {
-    autofill_metrics::LogStoredVirtualCardUsageCount(
-        autofill_virtual_card_usage_data_.size());
-
-    // Only log this info once per chrome user profile load.
-    has_logged_stored_virtual_card_usage_metrics_ = true;
-  }
+  autofill_metrics::LogStoredProfileMetrics(GetProfiles());
+  AutofillMetrics::LogStoredCreditCardMetrics(
+      local_credit_cards_, server_credit_cards_,
+      GetServerCardWithArtImageCount(), kDisusedDataModelTimeDelta);
+  autofill_metrics::LogStoredIbanMetrics(local_ibans_,
+                                         kDisusedDataModelTimeDelta);
+  autofill_metrics::LogStoredOfferMetrics(autofill_offer_data_);
+  autofill_metrics::LogStoredVirtualCardUsageCount(
+      autofill_virtual_card_usage_data_.size());
 }
 
 std::string PersonalDataManager::MostCommonCountryCodeFromProfiles() const {
diff --git a/components/autofill/core/browser/personal_data_manager.h b/components/autofill/core/browser/personal_data_manager.h
index 21050ca8..380ed37 100644
--- a/components/autofill/core/browser/personal_data_manager.h
+++ b/components/autofill/core/browser/personal_data_manager.h
@@ -710,24 +710,8 @@
   void CancelPendingServerQuery(WebDataServiceBase::Handle* handle);
 
   // The first time this is called, logs a UMA metrics about the user's autofill
-  // addresses. On subsequent calls, does nothing.
-  void LogStoredProfileMetrics() const;
-
-  // The first time this is called, logs an UMA metric about the user's autofill
-  // credit cards. On subsequent calls, does nothing.
-  void LogStoredCreditCardMetrics() const;
-
-  // The first time this is called, logs an UMA metric about the user's autofill
-  // IBANs. On subsequent calls, does nothing.
-  void LogStoredIbanMetrics() const;
-
-  // The first time this is called, logs UMA metrics about the users's autofill
-  // offer data. On subsequent calls, does nothing.
-  void LogStoredOfferMetrics() const;
-
-  // The first time this is called, logs UMA metrics about the users's autofill
-  // virtual card usage data. On subsequent calls, does nothing.
-  void LogStoredVirtualCardUsageMetrics() const;
+  // addresses, credit card, offer and IBAN. On subsequent calls, does nothing.
+  void LogStoredDataMetrics() const;
 
   // Whether the server cards are enabled and should be suggested to the user.
   virtual bool ShouldSuggestServerCards() const;
@@ -978,21 +962,9 @@
   // Default value is false.
   bool is_off_the_record_ = false;
 
-  // Whether we have already logged the stored profile metrics this session.
-  mutable bool has_logged_stored_profile_metrics_ = false;
-
-  // Whether we have already logged the stored credit card metrics this session.
-  mutable bool has_logged_stored_credit_card_metrics_ = false;
-
-  // Whether we have already logged the stored IBAN metrics this session.
-  mutable bool has_logged_stored_iban_metrics_ = false;
-
-  // Whether we have already logged the stored offer metrics this session.
-  mutable bool has_logged_stored_offer_metrics_ = false;
-
-  // Whether we have already logged the stored virtual card usage metrics this
-  // session.
-  mutable bool has_logged_stored_virtual_card_usage_metrics_ = false;
+  // Whether we have already logged the stored profile, credit card, IBAN, offer
+  // and virtual card usage metrics this session.
+  mutable bool has_logged_stored_data_metrics_ = false;
 
   // An observer to listen for changes to prefs::kAutofillCreditCardEnabled.
   std::unique_ptr<BooleanPrefMember> credit_card_enabled_pref_;
diff --git a/components/autofill/core/browser/personal_data_manager_cleaner.cc b/components/autofill/core/browser/personal_data_manager_cleaner.cc
index 3ad8f99..74616f3 100644
--- a/components/autofill/core/browser/personal_data_manager_cleaner.cc
+++ b/components/autofill/core/browser/personal_data_manager_cleaner.cc
@@ -63,12 +63,8 @@
   if (!personal_data_manager_->IsSyncEnabledFor(syncer::AUTOFILL_WALLET_DATA))
     ApplyCardFixesAndCleanups();
 
-  // Log address, credit card, offer, and usage data startup metrics.
-  personal_data_manager_->LogStoredProfileMetrics();
-  personal_data_manager_->LogStoredCreditCardMetrics();
-  personal_data_manager_->LogStoredIbanMetrics();
-  personal_data_manager_->LogStoredOfferMetrics();
-  personal_data_manager_->LogStoredVirtualCardUsageMetrics();
+  // Log address, credit card, offer, IBAN, and usage data startup metrics.
+  personal_data_manager_->LogStoredDataMetrics();
 
   personal_data_manager_->NotifyPersonalDataObserver();
 }
diff --git a/components/breadcrumbs/core/breadcrumb_persistent_storage_manager.cc b/components/breadcrumbs/core/breadcrumb_persistent_storage_manager.cc
index 071ad992..5e591c20 100644
--- a/components/breadcrumbs/core/breadcrumb_persistent_storage_manager.cc
+++ b/components/breadcrumbs/core/breadcrumb_persistent_storage_manager.cc
@@ -115,7 +115,8 @@
 
 BreadcrumbPersistentStorageManager::BreadcrumbPersistentStorageManager(
     const base::FilePath& directory,
-    base::RepeatingCallback<bool()> is_metrics_enabled_callback)
+    base::RepeatingCallback<bool()> is_metrics_enabled_callback,
+    base::OnceClosure initialization_done_callback)
     :  // Ensure first event will not be delayed by initializing with a time in
        // the past.
       last_written_time_(base::TimeTicks::Now() - kMinDelayBetweenWrites),
@@ -128,13 +129,15 @@
   task_runner_->PostTaskAndReplyWithResult(
       FROM_HERE, base::BindOnce(&DoGetStoredEvents, breadcrumbs_file_path_),
       base::BindOnce(&BreadcrumbPersistentStorageManager::Initialize,
-                     weak_ptr_factory_.GetWeakPtr()));
+                     weak_ptr_factory_.GetWeakPtr(),
+                     std::move(initialization_done_callback)));
 }
 
 BreadcrumbPersistentStorageManager::~BreadcrumbPersistentStorageManager() =
     default;
 
 void BreadcrumbPersistentStorageManager::Initialize(
+    base::OnceClosure initialization_done_callback,
     const std::string& previous_session_events) {
   breadcrumbs::BreadcrumbManager::GetInstance().SetPreviousSessionEvents(
       base::SplitString(previous_session_events, kEventSeparator,
@@ -143,7 +146,7 @@
 
   // Write any startup events that have accumulated while waiting for the file
   // position to be set.
-  WriteEvents();
+  WriteEvents(std::move(initialization_done_callback));
 }
 
 void BreadcrumbPersistentStorageManager::Write(const std::string& events,
@@ -184,7 +187,8 @@
   return should_create_files;
 }
 
-void BreadcrumbPersistentStorageManager::WriteEvents() {
+void BreadcrumbPersistentStorageManager::WriteEvents(
+    base::OnceClosure done_callback) {
   // No events can be written to the file until the size of existing breadcrumbs
   // is known.
   if (!file_position_) {
@@ -201,7 +205,8 @@
     write_timer_.Start(
         FROM_HERE, kMinDelayBetweenWrites - time_delta_since_last_write,
         base::BindOnce(&BreadcrumbPersistentStorageManager::WriteEvents,
-                       weak_ptr_factory_.GetWeakPtr()));
+                       weak_ptr_factory_.GetWeakPtr(),
+                       std::move(done_callback)));
     return;
   }
 
@@ -211,13 +216,14 @@
       // Use >= here instead of > to allow space for \0 to terminate file.
       >= kPersistedFilesizeInBytes) {
     Write(GetEvents(), /*append=*/false);
-    return;
-  }
-
-  // Otherwise, simply append the pending breadcrumbs.
-  if (!pending_breadcrumbs_.empty()) {
+  } else if (!pending_breadcrumbs_.empty()) {
+    // Otherwise, simply append the pending breadcrumbs.
     Write(pending_breadcrumbs_, /*append=*/true);
   }
+
+  // Add `done_callback` to the task runner's task queue, so it runs after any
+  // posted `DoWriteEventsToFile()` task has been run.
+  task_runner_->PostTask(FROM_HERE, std::move(done_callback));
 }
 
 }  // namespace breadcrumbs
diff --git a/components/breadcrumbs/core/breadcrumb_persistent_storage_manager.h b/components/breadcrumbs/core/breadcrumb_persistent_storage_manager.h
index 77c892d..71ab605c 100644
--- a/components/breadcrumbs/core/breadcrumb_persistent_storage_manager.h
+++ b/components/breadcrumbs/core/breadcrumb_persistent_storage_manager.h
@@ -10,6 +10,7 @@
 
 #include "base/files/file_path.h"
 #include "base/functional/callback.h"
+#include "base/functional/callback_forward.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
@@ -39,7 +40,8 @@
   // prepended to the event log.
   explicit BreadcrumbPersistentStorageManager(
       const base::FilePath& directory,
-      base::RepeatingCallback<bool()> is_metrics_enabled_callback);
+      base::RepeatingCallback<bool()> is_metrics_enabled_callback,
+      base::OnceClosure initialization_done_callback = base::DoNothing());
   ~BreadcrumbPersistentStorageManager() override;
   BreadcrumbPersistentStorageManager(
       const BreadcrumbPersistentStorageManager&) = delete;
@@ -50,7 +52,8 @@
   // Sets `file_position_` based on the given `previous_session_events`, and
   // passes them to the BreadcrumbManager. If any events have already been
   // logged it then writes them to the file.
-  void Initialize(const std::string& previous_session_events);
+  void Initialize(base::OnceClosure initialization_done_callback,
+                  const std::string& previous_session_events);
 
   // Returns whether metrics consent has been provided and the persistent
   // storage manager can therefore create its breadcrumbs files. Deletes any
@@ -60,7 +63,7 @@
   // Writes |pending_breadcrumbs_| to |breadcrumbs_file_| if it fits, otherwise
   // rewrites the file. NOTE: Writing may be delayed if the file has recently
   // been written into.
-  void WriteEvents();
+  void WriteEvents(base::OnceClosure done_callback = base::DoNothing());
 
   // Writes the given `events` to `breadcrumbs_file_`. If `append` is false,
   // overwrites the file.
diff --git a/components/breadcrumbs/core/breadcrumb_persistent_storage_manager_unittest.cc b/components/breadcrumbs/core/breadcrumb_persistent_storage_manager_unittest.cc
index a39b9f6..bf1af09 100644
--- a/components/breadcrumbs/core/breadcrumb_persistent_storage_manager_unittest.cc
+++ b/components/breadcrumbs/core/breadcrumb_persistent_storage_manager_unittest.cc
@@ -97,12 +97,18 @@
                              base::SPLIT_WANT_NONEMPTY);
   }
 
+  // Creates the persistent storage manager. Waits for it to read any existing
+  // events from the breadcrumbs file and rewrite it if it's oversized.
   void CreateBreadcrumbPersistentStorageManager() {
+    base::RunLoop run_loop;
     persistent_storage_ = std::make_unique<BreadcrumbPersistentStorageManager>(
         scoped_temp_dir_.GetPath(),
-        /*is_metrics_enabled_callback=*/base::BindRepeating(
+        /*is_metrics_enabled_callback=*/
+        base::BindRepeating(
             &BreadcrumbPersistentStorageManagerTest::is_metrics_enabled,
-            base::Unretained(this)));
+            base::Unretained(this)),
+        /*initialization_done_callback=*/run_loop.QuitClosure());
+    run_loop.Run();
   }
 
   bool is_metrics_enabled() { return is_metrics_enabled_; }
@@ -185,9 +191,8 @@
 
 // Ensures that events are read correctly if the persisted file becomes
 // corrupted by losing the EOF token or if kPersistedFilesizeInBytes is reduced.
-// TODO(crbug.com/1404642): This test is flaky.
 TEST_F(BreadcrumbPersistentStorageManagerTest,
-       DISABLED_GetStoredEventsAfterFilesizeReduction) {
+       GetStoredEventsAfterFilesizeReduction) {
   const base::FilePath breadcrumbs_file_path =
       GetBreadcrumbPersistentStorageFilePath(scoped_temp_dir_.GetPath());
   base::File file(breadcrumbs_file_path,
@@ -207,7 +212,10 @@
       /*offset=*/0, base::as_bytes(base::make_span(past_breadcrumbs))));
   ASSERT_TRUE(file.Flush());
   file.Close();
+  ASSERT_EQ(written_events, GetPersistedEvents().size());
 
+  // Create the persistent storage manager. It should resize the breadcrumbs
+  // file to the appropriate length.
   CreateBreadcrumbPersistentStorageManager();
 
   const auto events = GetPersistedEvents();
diff --git a/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/gesture/SwipeGestureListener.java b/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/gesture/SwipeGestureListener.java
index 842e12a..beb55b3 100644
--- a/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/gesture/SwipeGestureListener.java
+++ b/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/gesture/SwipeGestureListener.java
@@ -46,7 +46,8 @@
  * used to detect simple gestures defined in {@link GestureDetector}.
  */
 public class SwipeGestureListener extends SimpleOnGestureListener {
-    @IntDef({ScrollDirection.LEFT, ScrollDirection.RIGHT, ScrollDirection.UP, ScrollDirection.DOWN})
+    @IntDef({ScrollDirection.UNKNOWN, ScrollDirection.LEFT, ScrollDirection.RIGHT,
+            ScrollDirection.UP, ScrollDirection.DOWN})
     @Retention(RetentionPolicy.SOURCE)
     public @interface ScrollDirection {
         int UNKNOWN = 0;
diff --git a/components/media_router/browser/media_router_debugger_unittest.cc b/components/media_router/browser/media_router_debugger_unittest.cc
index 995dfbe..71ce07557 100644
--- a/components/media_router/browser/media_router_debugger_unittest.cc
+++ b/components/media_router/browser/media_router_debugger_unittest.cc
@@ -31,11 +31,6 @@
   // Reports should still be disabled since we the feature flag has not been
   // set.
   debugger_->EnableRtcpReports();
-  EXPECT_FALSE(debugger_->IsRtcpReportsEnabled());
-
-  // All conditions should be met and the function should return true now.
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitWithFeatures({media::kEnableRtcpReporting}, {});
 
   EXPECT_TRUE(debugger_->IsRtcpReportsEnabled());
 }
diff --git a/components/omnibox/browser/autocomplete_controller.cc b/components/omnibox/browser/autocomplete_controller.cc
index 292d114..fbcb09ee 100644
--- a/components/omnibox/browser/autocomplete_controller.cc
+++ b/components/omnibox/browser/autocomplete_controller.cc
@@ -6,6 +6,7 @@
 
 #include <inttypes.h>
 
+#include <algorithm>
 #include <cstddef>
 #include <memory>
 #include <numeric>
@@ -332,6 +333,7 @@
       search_provider_(nullptr),
       zero_suggest_provider_(nullptr),
       on_device_head_provider_(nullptr),
+      history_fuzzy_provider_(nullptr),
       notify_changed_debouncer_(
           OmniboxFieldTrial::
               kAutocompleteStabilityUpdateResultDebounceFromLastRun.Get(),
@@ -475,12 +477,34 @@
   // distinguished in the ms-scale buckets, is large enough to move the
   // arithmetic mean.
   base::TimeTicks start_time = base::TimeTicks::Now();
+
+  // Keep a max-heap of negative relevances to quickly estimate a relevance
+  // cutoff that can be used to improve counterfactual triggering.
+  // Prevent memory churn by starting with full size heap, ready for
+  // first change to be pushed without reallocation.
+  std::vector<int> relevances(result_.GetDynamicMaxMatches() + 1, 0);
+  relevances.pop_back();
+
   for (const auto& provider : providers_) {
-    if (!ShouldRunProvider(provider.get()))
+    if (!ShouldRunProvider(provider.get())) {
       continue;
+    }
 
     base::TimeTicks provider_start_time = base::TimeTicks::Now();
+    if (history_fuzzy_provider_) {
+      history_fuzzy_provider_->SetCounterfactualRelevanceHint(
+          -relevances.front());
+    }
     provider->Start(input_, minimal_changes);
+
+    for (const AutocompleteMatch& match : provider->matches()) {
+      relevances.push_back(-match.relevance);
+      std::push_heap(relevances.begin(), relevances.end());
+      std::pop_heap(relevances.begin(), relevances.end());
+      relevances.pop_back();
+      DCHECK(std::is_heap(relevances.begin(), relevances.end()));
+    }
+
     // `UmaHistogramTimes()` uses 1ms - 10s buckets, whereas this uses 1ms - 5s
     // buckets.
     // TODO(crbug.com/1340291|manukh): This isn't handled by `metrics_` yet. It
@@ -886,7 +910,8 @@
     providers_.push_back(voice_suggest_provider_.get());
   }
   if (provider_types & AutocompleteProvider::TYPE_HISTORY_FUZZY) {
-    providers_.push_back(new HistoryFuzzyProvider(provider_client_.get()));
+    history_fuzzy_provider_ = new HistoryFuzzyProvider(provider_client_.get());
+    providers_.push_back(history_fuzzy_provider_.get());
   }
   if (provider_types & AutocompleteProvider::TYPE_OPEN_TAB) {
     open_tab_provider_ = new OpenTabProvider(provider_client_.get());
diff --git a/components/omnibox/browser/autocomplete_controller.h b/components/omnibox/browser/autocomplete_controller.h
index a349166..07cd1871 100644
--- a/components/omnibox/browser/autocomplete_controller.h
+++ b/components/omnibox/browser/autocomplete_controller.h
@@ -34,6 +34,7 @@
 
 class ClipboardProvider;
 class DocumentProvider;
+class HistoryFuzzyProvider;
 class HistoryURLProvider;
 class HistoryQuickProvider;
 class KeywordProvider;
@@ -374,6 +375,8 @@
 
   raw_ptr<VoiceSuggestProvider> voice_suggest_provider_;
 
+  raw_ptr<HistoryFuzzyProvider> history_fuzzy_provider_;
+
   raw_ptr<OpenTabProvider> open_tab_provider_;
 
   // A vector of scoring signals annotators for URL suggestions.
diff --git a/components/omnibox/browser/autocomplete_grouper_sections.cc b/components/omnibox/browser/autocomplete_grouper_sections.cc
index 7b5531e..71ef821 100644
--- a/components/omnibox/browser/autocomplete_grouper_sections.cc
+++ b/components/omnibox/browser/autocomplete_grouper_sections.cc
@@ -95,16 +95,11 @@
   // Sort matches in the order of their potential containing groups. E.g., if
   // `groups_ = {group 1, group 2}, this sorts all matches that can be added to
   // group 1 before those that can only be added to group 2.
-  base::ranges::stable_sort(
-      matches,
-      [&](const auto& group_index1, const auto& group_index2) {
-        return group_index1 - group_index2;
-      },
-      [&](const auto& match) {
-        // Don't have to handle `FindGroup()` returning `groups_.end()` since
-        // those matches won't be added to the section anyways.
-        return std::distance(groups_.begin(), FindGroup(match));
-      });
+  base::ranges::stable_sort(matches, std::less<int>{}, [&](const auto& match) {
+    // Don't have to handle `FindGroup()` returning `groups_.end()` since
+    // those matches won't be added to the section anyways.
+    return std::distance(groups_.begin(), FindGroup(match));
+  });
 }
 
 AndroidZpsSection::AndroidZpsSection(omnibox::GroupConfigMap& group_configs)
@@ -112,7 +107,7 @@
                  {{1, omnibox::GROUP_MOBILE_SEARCH_READY_OMNIBOX},
                   {1, omnibox::GROUP_MOBILE_CLIPBOARD},
                   {1, omnibox::GROUP_MOBILE_MOST_VISITED},
-                  {15, omnibox::GROUP_PREVIOUS_SEARCH_RELATED},
+                  {15, omnibox::GROUP_VISITED_DOC_RELATED},
                   {15, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST}},
                  group_configs) {}
 
diff --git a/components/omnibox/browser/autocomplete_grouper_sections_unittest.cc b/components/omnibox/browser/autocomplete_grouper_sections_unittest.cc
index c4540d3..72fb140 100644
--- a/components/omnibox/browser/autocomplete_grouper_sections_unittest.cc
+++ b/components/omnibox/browser/autocomplete_grouper_sections_unittest.cc
@@ -52,6 +52,39 @@
   test({CreateMatch(1, omnibox::GROUP_SEARCH)}, {});
 }
 
+TEST(AutocompleteGrouperGroupsTest, SortingUsesProperComparator) {
+  class TestZpsSection : public ZpsSection {
+   public:
+    // Up to 2 items of the following types.
+    explicit TestZpsSection(omnibox::GroupConfigMap& group_configs)
+        : ZpsSection(2,
+                     {{1, omnibox::GROUP_MOBILE_SEARCH_READY_OMNIBOX},
+                      {1, omnibox::GROUP_MOBILE_CLIPBOARD},
+                      {1, omnibox::GROUP_MOBILE_MOST_VISITED},
+                      {1, omnibox::GROUP_VISITED_DOC_RELATED},
+                      {1, omnibox::GROUP_RELATED_QUERIES}},
+                     group_configs) {}
+  };
+
+  auto test = [](ACMatches matches, std::vector<int> expected_relevances) {
+    PSections sections;
+    omnibox::GroupConfigMap group_configs;
+    sections.push_back(std::make_unique<TestZpsSection>(group_configs));
+    auto out_matches = Section::GroupMatches(std::move(sections), matches);
+    VerifyMatches(out_matches, expected_relevances);
+  };
+
+  {
+    SCOPED_TRACE("Confirm appropriate comparator is used to sort suggestions");
+    test({CreateMatch(1, omnibox::GROUP_MOBILE_SEARCH_READY_OMNIBOX),
+          CreateMatch(2, omnibox::GROUP_MOBILE_CLIPBOARD),
+          CreateMatch(3, omnibox::GROUP_MOBILE_MOST_VISITED),
+          CreateMatch(4, omnibox::GROUP_VISITED_DOC_RELATED),
+          CreateMatch(5, omnibox::GROUP_RELATED_QUERIES)},
+         {1, 2});
+  }
+}
+
 // Tests the groups, limits, and rules for the ZPS section.
 TEST(AutocompleteGrouperSectionsTest, ZpsSection) {
   auto test = [](ACMatches matches, std::vector<int> expected_relevances) {
@@ -443,3 +476,200 @@
         {100, 99, 98, 97, 96, 95, 94, 93, 92, 91});
   }
 }
+
+// Tests the groups, limits, and rules for the Android ZPS section.
+TEST(AutocompleteGrouperSectionsTest, AndroidZpsSection) {
+  auto test = [](ACMatches matches, std::vector<int> expected_relevances) {
+    PSections sections;
+    omnibox::GroupConfigMap group_configs;
+    sections.push_back(std::make_unique<AndroidZpsSection>(group_configs));
+    auto out_matches = Section::GroupMatches(std::move(sections), matches);
+    VerifyMatches(out_matches, expected_relevances);
+  };
+
+  {
+    SCOPED_TRACE("Given no matches, should return no matches.");
+    test({}, {});
+  }
+  {
+    SCOPED_TRACE("Android/ZPS with extra searches.");
+    // Verify that the Clipboard suggestion is retained on top.
+    test(
+        {
+            CreateMatch(100, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST, true),
+            CreateMatch(99, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(98, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(97, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(96, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(95, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(94, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(93, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(92, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(91, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(90, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(89, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(88, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(87, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(86, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(85, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(84, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+        },
+        {100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86});
+  }
+  {
+    SCOPED_TRACE("Android/ZPS with Clipboard entries.");
+    // Verify that the Clipboard suggestion is retained on top.
+    test(
+        {
+            CreateMatch(100, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST, true),
+            CreateMatch(99, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(98, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(97, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(96, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(95, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(94, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(93, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(92, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(91, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(90, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(89, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(88, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(87, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(86, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(85, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(84, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(3, omnibox::GROUP_MOBILE_CLIPBOARD),
+            // Bogus, repetitive, only one allowed.
+            CreateMatch(2, omnibox::GROUP_MOBILE_CLIPBOARD),
+            CreateMatch(1, omnibox::GROUP_MOBILE_CLIPBOARD),
+        },
+        {3, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87});
+  }
+  {
+    SCOPED_TRACE("Android/ZPS with Search Ready Omnibox.");
+    // Verify that the Clipboard suggestion is retained on top.
+    test(
+        {
+            CreateMatch(100, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST, true),
+            CreateMatch(99, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(98, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(97, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(96, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(95, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(94, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(93, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(92, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(91, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(90, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(89, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(88, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(87, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(86, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(85, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(84, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(2, omnibox::GROUP_MOBILE_SEARCH_READY_OMNIBOX),
+            // Bogus, repetitive, only one allowed.
+            CreateMatch(1, omnibox::GROUP_MOBILE_SEARCH_READY_OMNIBOX),
+            CreateMatch(0, omnibox::GROUP_MOBILE_SEARCH_READY_OMNIBOX),
+        },
+        {2, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87});
+  }
+  {
+    SCOPED_TRACE("Android/ZPS on Web with recent searches only.");
+    // Verify that the Clipboard suggestion is retained on top.
+    test(
+        {
+            CreateMatch(100, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST, true),
+            CreateMatch(99, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(98, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(97, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(96, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(95, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(94, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(93, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(92, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(91, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(90, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(89, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(88, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(87, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(86, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(85, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(84, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(2, omnibox::GROUP_MOBILE_SEARCH_READY_OMNIBOX),
+        },
+        {2, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87});
+  }
+  {
+    SCOPED_TRACE("Android/ZPS with MV Tiles.");
+    // Verify that the Clipboard suggestion is retained on top.
+    test(
+        {
+            CreateMatch(100, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST, true),
+            CreateMatch(99, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(98, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(97, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(96, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(95, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(94, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(93, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(92, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(91, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(90, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(89, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(88, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(87, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(86, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(85, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(84, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(4, omnibox::GROUP_MOBILE_MOST_VISITED),
+            // Bogus, repetitive, currently only one allowed.
+            // This will be permitted when group rendering shifts to horizontal.
+            CreateMatch(3, omnibox::GROUP_MOBILE_MOST_VISITED),
+            CreateMatch(2, omnibox::GROUP_MOBILE_MOST_VISITED),
+        },
+        {4, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87});
+  }
+  {
+    SCOPED_TRACE("Android/ZPS with multiple auxiliary suggestions.");
+    // Verify that the Clipboard suggestion is retained on top.
+    test(
+        {
+            CreateMatch(100, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST, true),
+            CreateMatch(99, omnibox::GROUP_VISITED_DOC_RELATED),
+            CreateMatch(98, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(97, omnibox::GROUP_VISITED_DOC_RELATED),
+            CreateMatch(96, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(95, omnibox::GROUP_VISITED_DOC_RELATED),
+            CreateMatch(94, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(93, omnibox::GROUP_VISITED_DOC_RELATED),
+            CreateMatch(92, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(91, omnibox::GROUP_VISITED_DOC_RELATED),
+            CreateMatch(90, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(89, omnibox::GROUP_VISITED_DOC_RELATED),
+            CreateMatch(88, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(87, omnibox::GROUP_VISITED_DOC_RELATED),
+            CreateMatch(86, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            CreateMatch(85, omnibox::GROUP_VISITED_DOC_RELATED),
+            CreateMatch(84, omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST),
+            // SRO should always be shown first, despite low relevance.
+            // Only one item permitted.
+            CreateMatch(2, omnibox::GROUP_MOBILE_SEARCH_READY_OMNIBOX),
+            CreateMatch(1, omnibox::GROUP_MOBILE_SEARCH_READY_OMNIBOX),
+            // Clipboard should always be shown after SRO, if both are present.
+            // Only one item permitted.
+            CreateMatch(20, omnibox::GROUP_MOBILE_CLIPBOARD),
+            CreateMatch(19, omnibox::GROUP_MOBILE_CLIPBOARD),
+            // MV Tiles should always be on the third position if both SRO and
+            // Clipboard are present.
+            // Currently only one item is permitted.
+            CreateMatch(40, omnibox::GROUP_MOBILE_MOST_VISITED),
+            CreateMatch(39, omnibox::GROUP_MOBILE_MOST_VISITED),
+        },
+        // Observe that PERSONALIZED_ZERO_SUGGEST and VISITED_DOC suggestions are
+        // grouped together.
+        // VISITED_DOC_RELATED are prioritized over the PERSONALIZED_ZERO_SUGGEST
+        // because these are more context relevant.
+        {2, 20, 40, 99, 97, 95, 93, 91, 89, 87, 85, 100, 98, 96, 94});
+  }
+}
diff --git a/components/omnibox/browser/autocomplete_result.cc b/components/omnibox/browser/autocomplete_result.cc
index a54a3af..e668673c 100644
--- a/components/omnibox/browser/autocomplete_result.cc
+++ b/components/omnibox/browser/autocomplete_result.cc
@@ -53,19 +53,9 @@
 
 namespace {
 
-constexpr bool is_android =
-#if BUILDFLAG(IS_ANDROID)
-    true;
-#else
-    false;
-#endif
-
-constexpr bool is_ios =
-#if BUILDFLAG(IS_IOS)
-    true;
-#else
-    false;
-#endif
+constexpr bool is_android = !!BUILDFLAG(IS_ANDROID);
+constexpr bool is_ios = !!BUILDFLAG(IS_IOS);
+constexpr bool is_desktop = !(is_android || is_ios);
 
 // Rotates |it| to be in the front of |matches|.
 // |it| must be a valid iterator of |matches| or equal to |matches->end()|.
@@ -348,9 +338,13 @@
 
   // If `kGroupingFramework` is enabled and the current input & platform are
   // supported, delegate to the framework.
+  //
+  // - Include both Desktop ZPS and prefixed suggestions.
+  // - Include Android ZPS only (no prefixed suggestions),
+  // - IOS is currently not included.
   const bool is_zero_suggest = input.IsZeroSuggest();
   if (base::FeatureList::IsEnabled(omnibox::kGroupingFramework) &&
-      !is_android && !is_ios) {
+      (is_desktop || (is_android && is_zero_suggest))) {
     // Grouping requires all matches have a group ID. To keep providers 'dumb',
     // they only assign IDs when their ID isn't obvious from the match type.
     // Most matches will instead set IDs here to keep providers 'dumb' and the
@@ -370,14 +364,20 @@
 
     PSections sections;
     if (is_zero_suggest) {
+#if BUILDFLAG(IS_ANDROID)
+      sections.push_back(
+          std::make_unique<AndroidZpsSection>(suggestion_groups_map_));
+#else   // !BUILDFLAG(IS_ANDROID)
       sections.push_back(
           std::make_unique<DesktopZpsSection>(suggestion_groups_map_));
+
       if (page_classification == OmniboxEventProto::NTP_REALBOX &&
           base::FeatureList::IsEnabled(omnibox::kKeepSecondaryZeroSuggest)) {
         // Allow secondary zero-prefix suggestions in the NTP realbox, if any.
         sections.push_back(std::make_unique<DesktopSecondaryZpsSection>(
             suggestion_groups_map_));
       }
+#endif  // !BUILDFLAG(IS_ANDROID)
     } else {
       sections.push_back(
           std::make_unique<DesktopNonZpsSection>(suggestion_groups_map_));
diff --git a/components/omnibox/browser/history_fuzzy_provider.cc b/components/omnibox/browser/history_fuzzy_provider.cc
index fd18321..0992b99 100644
--- a/components/omnibox/browser/history_fuzzy_provider.cc
+++ b/components/omnibox/browser/history_fuzzy_provider.cc
@@ -478,6 +478,7 @@
   penalty_high_ = OmniboxFieldTrial::kFuzzyUrlSuggestionsPenaltyHigh.Get();
   penalty_taper_length_ =
       OmniboxFieldTrial::kFuzzyUrlSuggestionsPenaltyTaperLength.Get();
+  counterfactual_ = OmniboxFieldTrial::kFuzzyUrlSuggestionsCounterfactual.Get();
 
   if (ShouldBypassForLowEndDevice()) {
     // Note, this early return will prevent loading from database, which saves
@@ -500,6 +501,10 @@
   }
 }
 
+void HistoryFuzzyProvider::SetCounterfactualRelevanceHint(int relevance_hint) {
+  counterfactual_relevance_hint_ = relevance_hint;
+}
+
 void HistoryFuzzyProvider::Start(const AutocompleteInput& input,
                                  bool minimal_changes) {
   TRACE_EVENT0("omnibox", "HistoryFuzzyProvider::Start");
@@ -530,16 +535,25 @@
   }
 
   if (!matches_.empty()) {
-    // This will likely produce some false positives.
-    client()->GetOmniboxTriggeredFeatureService()->FeatureTriggered(
-        OmniboxTriggeredFeatureService::Feature::kFuzzyUrlSuggestions);
+    // This will likely produce some false positives, but the likelihood
+    // is reduced by only triggering when one of the matches exceeds
+    // the relevance hint, an estimated cutoff value at which we expect fuzzy
+    // matches could persist after sorting and culling the full match set.
+    const bool met_threshold = std::any_of(
+        matches_.begin(), matches_.end(), [=](const AutocompleteMatch& match) {
+          return match.relevance > counterfactual_relevance_hint_;
+        });
+    if (met_threshold) {
+      client()->GetOmniboxTriggeredFeatureService()->FeatureTriggered(
+          OmniboxTriggeredFeatureService::Feature::kFuzzyUrlSuggestions);
+    }
 
     // When in the counterfactual group, we do all the work of finding fuzzy
     // matches, but do not provide the benefit. To reduce risk of unintended
     // consequences downstream (for example showing fewer suggestions than
     // normal), the matches are cleared here instead of at end of result
     // processing pipeline so they won't interact or dedupe with other matches.
-    if (OmniboxFieldTrial::kFuzzyUrlSuggestionsCounterfactual.Get()) {
+    if (counterfactual_) {
       matches_.clear();
     }
   }
diff --git a/components/omnibox/browser/history_fuzzy_provider.h b/components/omnibox/browser/history_fuzzy_provider.h
index d4f1909d..b10793b 100644
--- a/components/omnibox/browser/history_fuzzy_provider.h
+++ b/components/omnibox/browser/history_fuzzy_provider.h
@@ -194,6 +194,10 @@
   HistoryFuzzyProvider(const HistoryFuzzyProvider&) = delete;
   HistoryFuzzyProvider& operator=(const HistoryFuzzyProvider&) = delete;
 
+  // Set a relevance hint to improve counterfactual in late-running providers
+  // where most match relevances are already determined.
+  void SetCounterfactualRelevanceHint(int relevance_hint);
+
   // HistoryProvider:
   // AutocompleteProvider. `minimal_changes` is ignored since there is no async
   // completion performed.
@@ -260,6 +264,13 @@
   int penalty_high_;
   size_t penalty_taper_length_;
 
+  // Cache counterfactual feature param.
+  bool counterfactual_;
+
+  // A relevance value below which counterfactual matches are less likely
+  // to be kept, if they were to be included in the full output match set.
+  int counterfactual_relevance_hint_{0};
+
   // Weak pointer factory for callback binding safety.
   base::WeakPtrFactory<HistoryFuzzyProvider> weak_ptr_factory_{this};
 };
diff --git a/components/omnibox/browser/zero_suggest_verbatim_match_provider.cc b/components/omnibox/browser/zero_suggest_verbatim_match_provider.cc
index 8fe674a7..bebddc7 100644
--- a/components/omnibox/browser/zero_suggest_verbatim_match_provider.cc
+++ b/components/omnibox/browser/zero_suggest_verbatim_match_provider.cc
@@ -92,5 +92,5 @@
     return;
 
   match.provider = this;
-  matches_.push_back(match);
+  matches_.push_back(std::move(match));
 }
diff --git a/components/page_info/page_info.cc b/components/page_info/page_info.cc
index 517e82a4..fd169957 100644
--- a/components/page_info/page_info.cc
+++ b/components/page_info/page_info.cc
@@ -48,6 +48,7 @@
 #endif
 #include "build/chromeos_buildflags.h"
 #include "components/page_info/core/features.h"
+#include "components/permissions/permission_recovery_success_rate_tracker.h"
 #include "components/permissions/permission_uma_util.h"
 #include "components/permissions/permission_util.h"
 #include "components/privacy_sandbox/privacy_sandbox_features.h"
@@ -728,14 +729,21 @@
         type, setting_old, setting, is_subscribed_to_permission_change_event);
   }
 
-  // Show the infobar only if permission's status is not handled by an
-  // origin.
+  // Show the infobar only if permission's status is not handled by an origin.
   // When the sound setting is changed, no reload is necessary.
   if (!is_subscribed_to_permission_change_event &&
       type != ContentSettingsType::SOUND) {
     show_info_bar_ = true;
   }
 
+  if (permissions::PermissionUtil::IsPermission(type)) {
+    auto* permission_tracker =
+        permissions::PermissionRecoverySuccessRateTracker::FromWebContents(
+            web_contents_.get());
+
+    permission_tracker->PermissionStatusChanged(type, setting, show_info_bar_);
+  }
+
   // Refresh the UI to reflect the new setting.
   PresentSitePermissions();
 }
diff --git a/components/permissions/BUILD.gn b/components/permissions/BUILD.gn
index 26e2d78..7876cd43 100644
--- a/components/permissions/BUILD.gn
+++ b/components/permissions/BUILD.gn
@@ -78,6 +78,8 @@
     "permission_manager.cc",
     "permission_manager.h",
     "permission_prompt.h",
+    "permission_recovery_success_rate_tracker.cc",
+    "permission_recovery_success_rate_tracker.h",
     "permission_request.cc",
     "permission_request.h",
     "permission_request_id.cc",
diff --git a/components/permissions/permission_recovery_success_rate_tracker.cc b/components/permissions/permission_recovery_success_rate_tracker.cc
new file mode 100644
index 0000000..e0dae2a
--- /dev/null
+++ b/components/permissions/permission_recovery_success_rate_tracker.cc
@@ -0,0 +1,78 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/permissions/permission_recovery_success_rate_tracker.h"
+
+#include "components/content_settings/core/common/content_settings_types.h"
+#include "components/permissions/permission_uma_util.h"
+#include "content/public/browser/web_contents.h"
+
+namespace permissions {
+
+PermissionRecoverySuccessRateTracker::PermissionRecoverySuccessRateTracker(
+    content::WebContents* web_contents)
+    : content::WebContentsObserver(web_contents),
+      content::WebContentsUserData<PermissionRecoverySuccessRateTracker>(
+          *web_contents) {
+  DCHECK(web_contents);
+}
+
+PermissionRecoverySuccessRateTracker::~PermissionRecoverySuccessRateTracker() {
+  DCHECK(reallowed_permissions_.empty());
+}
+
+void PermissionRecoverySuccessRateTracker::PermissionStatusChanged(
+    ContentSettingsType permission,
+    ContentSetting setting,
+    bool show_infobar) {
+  // If permission is not allowed, it is not actionable for origins.
+  if (setting != ContentSetting::CONTENT_SETTING_ALLOW) {
+    return;
+  }
+
+  reallowed_permissions_[permission] = show_infobar;
+}
+
+void PermissionRecoverySuccessRateTracker::ClearTrackingMap() {
+  for (const auto& [permission, show_infobar] : reallowed_permissions_) {
+    Track(permission, /*is_used=*/false, show_infobar);
+  }
+
+  reallowed_permissions_.clear();
+}
+
+void PermissionRecoverySuccessRateTracker::TrackUsage(
+    ContentSettingsType permission) {
+  if (reallowed_permissions_.find(permission) != reallowed_permissions_.end()) {
+    Track(permission, /*is_used=*/true, reallowed_permissions_[permission]);
+    reallowed_permissions_.erase(permission);
+  }
+}
+
+void PermissionRecoverySuccessRateTracker::Track(ContentSettingsType permission,
+                                                 bool is_used,
+                                                 bool show_infobar) {
+  PermissionUmaUtil::RecordPermissionRecoverySuccessRate(
+      permission, is_used, show_infobar, page_reload_);
+}
+
+void PermissionRecoverySuccessRateTracker::WebContentsDestroyed() {
+  ClearTrackingMap();
+}
+
+void PermissionRecoverySuccessRateTracker::PrimaryPageChanged(
+    content::Page& page) {
+  if (origin_ != page.GetMainDocument().GetLastCommittedOrigin()) {
+    origin_ = page.GetMainDocument().GetLastCommittedOrigin();
+    // Clear tracking map only for cross-origin navigation.
+    ClearTrackingMap();
+    page_reload_ = false;
+  } else {
+    page_reload_ = true;
+  }
+}
+
+WEB_CONTENTS_USER_DATA_KEY_IMPL(PermissionRecoverySuccessRateTracker);
+
+}  // namespace permissions
diff --git a/components/permissions/permission_recovery_success_rate_tracker.h b/components/permissions/permission_recovery_success_rate_tracker.h
new file mode 100644
index 0000000..d54a4c9
--- /dev/null
+++ b/components/permissions/permission_recovery_success_rate_tracker.h
@@ -0,0 +1,73 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_PERMISSIONS_PERMISSION_RECOVERY_SUCCESS_RATE_TRACKER_H_
+#define COMPONENTS_PERMISSIONS_PERMISSION_RECOVERY_SUCCESS_RATE_TRACKER_H_
+
+#include "components/content_settings/core/common/content_settings.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/browser/web_contents_user_data.h"
+
+enum class ContentSettingsType;
+
+namespace permissions {
+
+// This class track permissions that were reallowed via PageInfo. In addition,
+// it records if the "Reload this page" infobar was shown or if a page was
+// reloaded. The reload may be initiated via the infobar or any other UI
+// elements. The recording happens when a main frame is going to be destroyed.
+// That may happen while a tab is being closed or while cross-origin navigation.
+//
+// The recording happens instantly if permission is used by an origin as the
+// goal is to verify permission usage after it was reallowed.
+class PermissionRecoverySuccessRateTracker
+    : public content::WebContentsObserver,
+      public content::WebContentsUserData<
+          PermissionRecoverySuccessRateTracker> {
+ public:
+  explicit PermissionRecoverySuccessRateTracker(
+      content::WebContents* web_contents);
+
+  PermissionRecoverySuccessRateTracker(
+      const PermissionRecoverySuccessRateTracker&) = delete;
+  PermissionRecoverySuccessRateTracker& operator=(
+      const PermissionRecoverySuccessRateTracker&) = delete;
+
+  ~PermissionRecoverySuccessRateTracker() override;
+
+  // Adds `permission` into the `reallowed_permissions_` map. `permission` will
+  // be recorded before a main frame is destroyed or after `permission` is used
+  // by an origin.
+  void PermissionStatusChanged(ContentSettingsType permission,
+                               ContentSetting setting,
+                               bool show_infobar);
+
+  // `permission` has been used by an origin, record usage and remove from
+  // `reallowed_permissions_`.
+  void TrackUsage(ContentSettingsType permission);
+
+ private:
+  friend class content::WebContentsUserData<
+      PermissionRecoverySuccessRateTracker>;
+
+  void ClearTrackingMap();
+
+  void Track(ContentSettingsType permission, bool is_used, bool show_infobar);
+
+  // content::WebContentsObserver:
+  void WebContentsDestroyed() override;
+  void PrimaryPageChanged(content::Page& page) override;
+
+  absl::optional<url::Origin> origin_;
+
+  bool page_reload_ = false;
+
+  std::map<ContentSettingsType, bool> reallowed_permissions_;
+
+  WEB_CONTENTS_USER_DATA_KEY_DECL();
+};
+
+}  // namespace permissions
+
+#endif  // COMPONENTS_PERMISSIONS_PERMISSION_RECOVERY_SUCCESS_RATE_TRACKER_H_
diff --git a/components/permissions/permission_uma_util.cc b/components/permissions/permission_uma_util.cc
index 196da56..fe7021ca 100644
--- a/components/permissions/permission_uma_util.cc
+++ b/components/permissions/permission_uma_util.cc
@@ -498,6 +498,44 @@
                                 PermissionHeaderPolicyForUMA::NUM);
 }
 
+PermissionChangeInfo GetChangeInfo(bool is_used,
+                                   bool show_infobar,
+                                   bool page_reload) {
+  if (show_infobar) {
+    if (page_reload) {
+      if (is_used) {
+        return PermissionChangeInfo::kInfobarShownPageReloadPermissionUsed;
+      } else {
+        return PermissionChangeInfo::kInfobarShownPageReloadPermissionNotUsed;
+      }
+
+    } else {
+      if (is_used) {
+        return PermissionChangeInfo::kInfobarShownNoPageReloadPermissionUsed;
+      } else {
+        return PermissionChangeInfo::kInfobarShownNoPageReloadPermissionNotUsed;
+      }
+    }
+  } else {
+    if (page_reload) {
+      if (is_used) {
+        return PermissionChangeInfo::kInfobarNotShownPageReloadPermissionUsed;
+      } else {
+        return PermissionChangeInfo::
+            kInfobarNotShownPageReloadPermissionNotUsed;
+      }
+
+    } else {
+      if (is_used) {
+        return PermissionChangeInfo::kInfobarNotShownNoPageReloadPermissionUsed;
+      } else {
+        return PermissionChangeInfo::
+            kInfobarNotShownNoPageReloadPermissionNotUsed;
+      }
+    }
+  }
+}
+
 }  // anonymous namespace
 
 // PermissionUmaUtil ----------------------------------------------------------
@@ -630,6 +668,22 @@
                                 embargo_status, PermissionEmbargoStatus::NUM);
 }
 
+void PermissionUmaUtil::RecordPermissionRecoverySuccessRate(
+    ContentSettingsType permission,
+    bool is_used,
+    bool show_infobar,
+    bool page_reload) {
+  PermissionChangeInfo change_info =
+      GetChangeInfo(is_used, show_infobar, page_reload);
+
+  std::string permission_string = GetPermissionRequestString(
+      GetUmaValueForRequestType(ContentSettingsTypeToRequestType(permission)));
+
+  base::UmaHistogramEnumeration("Permissions.PageInfo.Changed." +
+                                    permission_string + ".Reallowed.Outcome",
+                                change_info);
+}
+
 void PermissionUmaUtil::RecordPermissionPromptAttempt(
     const std::vector<PermissionRequest*>& requests,
     bool IsLocationBarEditingOrEmpty) {
diff --git a/components/permissions/permission_uma_util.h b/components/permissions/permission_uma_util.h
index c227842..0ed52cb 100644
--- a/components/permissions/permission_uma_util.h
+++ b/components/permissions/permission_uma_util.h
@@ -373,6 +373,35 @@
   NUM
 };
 
+// This enum backs up the
+// 'Permissions.PageInfo.Changed.{PermissionType}.Reallowed.Outcome' histograms
+// enum. It is used for collecting permission usage rates after permission
+// status was reallowed via PageInfo. It is applicable only if permission is
+// allowed as all other states are no-op for an origin.
+//
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class PermissionChangeInfo {
+  kInfobarShownPageReloadPermissionUsed = 0,
+
+  kInfobarShownPageReloadPermissionNotUsed = 1,
+
+  kInfobarShownNoPageReloadPermissionUsed = 2,
+
+  kInfobarShownNoPageReloadPermissionNotUsed = 3,
+
+  kInfobarNotShownPageReloadPermissionUsed = 4,
+
+  kInfobarNotShownPageReloadPermissionNotUsed = 5,
+
+  kInfobarNotShownNoPageReloadPermissionUsed = 6,
+
+  kInfobarNotShownNoPageReloadPermissionNotUsed = 7,
+
+  // Always keep at the end.
+  kMaxValue = kInfobarNotShownNoPageReloadPermissionNotUsed
+};
+
 // Provides a convenient way of logging UMA for permission related operations.
 class PermissionUmaUtil {
  public:
@@ -423,6 +452,12 @@
 
   static void RecordEmbargoStatus(PermissionEmbargoStatus embargo_status);
 
+  static void RecordPermissionRecoverySuccessRate(
+      ContentSettingsType permission,
+      bool is_used,
+      bool show_infobar,
+      bool page_reload);
+
   // Recorded when a permission prompt creation is in progress.
   static void RecordPermissionPromptAttempt(
       const std::vector<PermissionRequest*>& requests,
diff --git a/components/permissions/permission_uma_util_unittest.cc b/components/permissions/permission_uma_util_unittest.cc
index 2bf67b4..bfd4063 100644
--- a/components/permissions/permission_uma_util_unittest.cc
+++ b/components/permissions/permission_uma_util_unittest.cc
@@ -347,6 +347,81 @@
                               0);
 }
 
+TEST_F(PermissionUmaUtilTest, PageInfoPermissionReallowedTest) {
+  base::HistogramTester histograms;
+
+  PermissionUmaUtil::RecordPermissionRecoverySuccessRate(
+      ContentSettingsType::MEDIASTREAM_CAMERA, /*is_used=*/true,
+      /*show_infobar=*/true, /*page_reload=*/true);
+  histograms.ExpectBucketCount(
+      "Permissions.PageInfo.Changed.VideoCapture.Reallowed.Outcome",
+      permissions::PermissionChangeInfo::kInfobarShownPageReloadPermissionUsed,
+      1);
+
+  PermissionUmaUtil::RecordPermissionRecoverySuccessRate(
+      ContentSettingsType::MEDIASTREAM_CAMERA, /*is_used=*/false,
+      /*show_infobar=*/true, /*page_reload=*/true);
+  histograms.ExpectBucketCount(
+      "Permissions.PageInfo.Changed.VideoCapture.Reallowed.Outcome",
+      permissions::PermissionChangeInfo::
+          kInfobarShownPageReloadPermissionNotUsed,
+      1);
+
+  PermissionUmaUtil::RecordPermissionRecoverySuccessRate(
+      ContentSettingsType::MEDIASTREAM_CAMERA, /*is_used=*/true,
+      /*show_infobar=*/true, /*page_reload=*/false);
+  histograms.ExpectBucketCount(
+      "Permissions.PageInfo.Changed.VideoCapture.Reallowed.Outcome",
+      permissions::PermissionChangeInfo::
+          kInfobarShownNoPageReloadPermissionUsed,
+      1);
+
+  PermissionUmaUtil::RecordPermissionRecoverySuccessRate(
+      ContentSettingsType::MEDIASTREAM_CAMERA, /*is_used=*/false,
+      /*show_infobar=*/true, /*page_reload=*/false);
+  histograms.ExpectBucketCount(
+      "Permissions.PageInfo.Changed.VideoCapture.Reallowed.Outcome",
+      permissions::PermissionChangeInfo::
+          kInfobarShownNoPageReloadPermissionNotUsed,
+      1);
+
+  PermissionUmaUtil::RecordPermissionRecoverySuccessRate(
+      ContentSettingsType::MEDIASTREAM_CAMERA, /*is_used=*/true,
+      /*show_infobar=*/false, /*page_reload=*/true);
+  histograms.ExpectBucketCount(
+      "Permissions.PageInfo.Changed.VideoCapture.Reallowed.Outcome",
+      permissions::PermissionChangeInfo::
+          kInfobarNotShownPageReloadPermissionUsed,
+      1);
+
+  PermissionUmaUtil::RecordPermissionRecoverySuccessRate(
+      ContentSettingsType::MEDIASTREAM_CAMERA, /*is_used=*/false,
+      /*show_infobar=*/false, /*page_reload=*/true);
+  histograms.ExpectBucketCount(
+      "Permissions.PageInfo.Changed.VideoCapture.Reallowed.Outcome",
+      permissions::PermissionChangeInfo::
+          kInfobarNotShownPageReloadPermissionNotUsed,
+      1);
+
+  PermissionUmaUtil::RecordPermissionRecoverySuccessRate(
+      ContentSettingsType::MEDIASTREAM_CAMERA, /*is_used=*/true,
+      /*show_infobar=*/false, /*page_reload=*/false);
+  histograms.ExpectBucketCount(
+      "Permissions.PageInfo.Changed.VideoCapture.Reallowed.Outcome",
+      permissions::PermissionChangeInfo::
+          kInfobarNotShownNoPageReloadPermissionUsed,
+      1);
+
+  PermissionUmaUtil::RecordPermissionRecoverySuccessRate(
+      ContentSettingsType::MEDIASTREAM_CAMERA, /*is_used=*/false,
+      /*show_infobar=*/false, /*page_reload=*/false);
+  histograms.ExpectBucketCount(
+      "Permissions.PageInfo.Changed.VideoCapture.Reallowed.Outcome",
+      permissions::PermissionChangeInfo::
+          kInfobarNotShownNoPageReloadPermissionNotUsed,
+      1);
+}
+
 TEST_F(PermissionsDelegationUmaUtilTest, SameOriginFrame) {
   base::HistogramTester histograms;
   auto* main_frame = GetMainFrameAndNavigate(kTopLevelUrl);
diff --git a/components/safe_browsing/content/browser/safe_browsing_navigation_observer_manager.cc b/components/safe_browsing/content/browser/safe_browsing_navigation_observer_manager.cc
index b0d252f..ad234e0 100644
--- a/components/safe_browsing/content/browser/safe_browsing_navigation_observer_manager.cc
+++ b/components/safe_browsing/content/browser/safe_browsing_navigation_observer_manager.cc
@@ -37,17 +37,31 @@
 
 namespace {
 
+// The expiration period of a user gesture. Any user gesture that happened 1.0
+// second ago is considered as expired and not relevant to upcoming navigation
+// events.
+static constexpr base::TimeDelta kUserGestureTTL = base::Seconds(1);
+// The expiration period of navigation events and resolved IP addresses. Any
+// navigation related records that happened 2 minutes ago are considered as
+// expired. So we clean up these navigation footprints every 2 minutes.
+static constexpr base::TimeDelta kNavigationFootprintTTL = base::Minutes(2);
+// The maximum number of latest NavigationEvent we keep. It is used to limit
+// memory usage of navigation tracking. This number is picked based on UMA
+// metric "SafeBrowsing.NavigationObserver.NavigationEventCleanUpCount".
+// Lowering it could make room for abuse.
+static const int kNavigationRecordMaxSize = 100;
+// The maximum number of ReferrerChainEntry. It is used to limit the size of
+// reports (e.g. ClientDownloadRequest) we send to SB server.
+static const int kReferrerChainMaxLength = 10;
+
 constexpr size_t kMaxNumberOfNavigationsToAppend = 5;
 
 // Given when an event happened and its TTL, determine if it is already expired.
 // Note, if for some reason this event's timestamp is in the future, this
 // event's timestamp is invalid, hence we treat it as expired.
-bool IsEventExpired(const base::Time& event_time, double ttl_in_second) {
-  double current_time_in_second = base::Time::Now().ToDoubleT();
-  double event_time_in_second = event_time.ToDoubleT();
-  if (current_time_in_second <= event_time_in_second)
-    return true;
-  return current_time_in_second - event_time_in_second > ttl_in_second;
+bool IsEventExpired(const base::Time& event_time, base::TimeDelta ttl) {
+  base::Time now = base::Time::Now();
+  return event_time > now || now - event_time > ttl;
 }
 
 // Helper function to determine if the URL type should be LANDING_REFERRER or
@@ -88,24 +102,23 @@
   }
 }
 
-}  // namespace
+base::TimeDelta GetNavigationFootprintTTL() {
+  if (base::FeatureList::IsEnabled(kReferrerChainParameters)) {
+    return base::Seconds(kReferrerChainEventMaximumAgeSeconds.Get());
+  }
 
-// The expiration period of a user gesture. Any user gesture that happened 1.0
-// second ago is considered as expired and not relevant to upcoming navigation
-// events.
-static const double kUserGestureTTLInSecond = 1.0;
-// The expiration period of navigation events and resolved IP addresses. Any
-// navigation related records that happened 2 minutes ago are considered as
-// expired. So we clean up these navigation footprints every 2 minutes.
-static const double kNavigationFootprintTTLInSecond = 120.0;
-// The maximum number of latest NavigationEvent we keep. It is used to limit
-// memory usage of navigation tracking. This number is picked based on UMA
-// metric "SafeBrowsing.NavigationObserver.NavigationEventCleanUpCount".
-// Lowering it could make room for abuse.
-static const int kNavigationRecordMaxSize = 100;
-// The maximum number of ReferrerChainEntry. It is used to limit the size of
-// reports (e.g. ClientDownloadRequest) we send to SB server.
-static const int kReferrerChainMaxLength = 10;
+  return kNavigationFootprintTTL;
+}
+
+int GetNavigationRecordMaxSize() {
+  if (base::FeatureList::IsEnabled(kReferrerChainParameters)) {
+    return kReferrerChainEventMaximumCount.Get();
+  }
+
+  return kNavigationRecordMaxSize;
+}
+
+}  // namespace
 
 // -------------------------ReferrerChainData-----------------------
 
@@ -303,11 +316,11 @@
 
 std::size_t NavigationEventList::CleanUpNavigationEvents() {
   // Remove any stale NavigationEnvent, if it is older than
-  // kNavigationFootprintTTLInSecond.
+  // `GetNavigationFootprintTTL()`.
   std::size_t removal_count = 0;
   while (!navigation_events_.empty() &&
          IsEventExpired(navigation_events_[0]->last_updated,
-                        kNavigationFootprintTTLInSecond)) {
+                        GetNavigationFootprintTTL())) {
     navigation_events_.pop_front();
     removal_count++;
   }
@@ -315,8 +328,7 @@
   // Clean up expired pending navigation events.
   auto it = pending_navigation_events_.begin();
   while (it != pending_navigation_events_.end()) {
-    if (IsEventExpired(it->second->last_updated,
-                       kNavigationFootprintTTLInSecond)) {
+    if (IsEventExpired(it->second->last_updated, GetNavigationFootprintTTL())) {
       it = pending_navigation_events_.erase(it);
     } else {
       ++it;
@@ -330,7 +342,7 @@
 // static
 bool SafeBrowsingNavigationObserverManager::IsUserGestureExpired(
     const base::Time& timestamp) {
-  return IsEventExpired(timestamp, kUserGestureTTLInSecond);
+  return IsEventExpired(timestamp, kUserGestureTTL);
 }
 
 // static
@@ -387,11 +399,10 @@
 
 SafeBrowsingNavigationObserverManager::SafeBrowsingNavigationObserverManager(
     PrefService* pref_service)
-    : navigation_event_list_(kNavigationRecordMaxSize),
+    : navigation_event_list_(GetNavigationRecordMaxSize()),
       pref_service_(pref_service) {
   // Schedule clean up in 2 minutes.
-  ScheduleNextCleanUpAfterInterval(
-      base::Seconds(kNavigationFootprintTTLInSecond));
+  ScheduleNextCleanUpAfterInterval(GetNavigationFootprintTTL());
 }
 
 void SafeBrowsingNavigationObserverManager::RecordNavigationEvent(
@@ -479,8 +490,7 @@
   CleanUpNavigationEvents();
   CleanUpUserGestures();
   CleanUpIpAddresses();
-  ScheduleNextCleanUpAfterInterval(
-      base::Seconds(kNavigationFootprintTTLInSecond));
+  ScheduleNextCleanUpAfterInterval(GetNavigationFootprintTTL());
 }
 
 SafeBrowsingNavigationObserverManager::AttributionResult
@@ -504,8 +514,8 @@
 
   auto* nav_event = navigation_event_list_.GetNavigationEvent(*nav_event_index);
   AttributionResult result = SUCCESS;
-  AddToReferrerChain(out_referrer_chain, nav_event, GURL(),
-                     ReferrerChainEntry::EVENT_URL);
+  MaybeAddToReferrerChain(out_referrer_chain, nav_event, GURL(),
+                          ReferrerChainEntry::EVENT_URL);
   int user_gesture_count = 0;
   GetRemainingReferrerChain(*nav_event_index, user_gesture_count,
                             user_gesture_count_limit, out_referrer_chain,
@@ -538,8 +548,8 @@
   }
 
   AttributionResult result = SUCCESS;
-  AddToReferrerChain(out_referrer_chain, pending_nav_event, GURL(),
-                     ReferrerChainEntry::EVENT_URL);
+  MaybeAddToReferrerChain(out_referrer_chain, pending_nav_event, GURL(),
+                          ReferrerChainEntry::EVENT_URL);
   int user_gesture_count = 0;
   GetRemainingReferrerChainForNavEvent(
       pending_nav_event, navigation_event_list_.NavigationEventsSize(),
@@ -613,12 +623,13 @@
   // page of this event.
   if (has_user_gesture) {
     user_gesture_count = 1;
-    AddToReferrerChain(
+    MaybeAddToReferrerChain(
         out_referrer_chain, nav_event, initiating_main_frame_url,
         GetURLTypeAndAdjustAttributionResult(user_gesture_count, &result));
   } else {
-    AddToReferrerChain(out_referrer_chain, nav_event, initiating_main_frame_url,
-                       ReferrerChainEntry::CLIENT_REDIRECT);
+    MaybeAddToReferrerChain(out_referrer_chain, nav_event,
+                            initiating_main_frame_url,
+                            ReferrerChainEntry::CLIENT_REDIRECT);
   }
 
   GetRemainingReferrerChain(*nav_event_index, user_gesture_count,
@@ -742,8 +753,8 @@
   while (it != navigation_event_list_.navigation_events().rend()) {
     // Skip navigations that happened after |last_navigation_time_msec|.
     if (it->get()->last_updated.ToJavaTime() < last_navigation_time_msec) {
-      AddToReferrerChain(&navigation_chain, it->get(), GURL(),
-                         ReferrerChainEntry::RECENT_NAVIGATION);
+      MaybeAddToReferrerChain(&navigation_chain, it->get(), GURL(),
+                              ReferrerChainEntry::RECENT_NAVIGATION);
       allowed_entries--;
       if (it->get()->IsUserInitiated()) {
         user_gesture_cnt++;
@@ -781,18 +792,18 @@
 
 void SafeBrowsingNavigationObserverManager::CleanUpUserGestures() {
   for (auto it = user_gesture_map_.begin(); it != user_gesture_map_.end();) {
-    if (IsEventExpired(it->second, kNavigationFootprintTTLInSecond))
+    if (IsEventExpired(it->second, GetNavigationFootprintTTL())) {
       it = user_gesture_map_.erase(it);
-    else
+    } else {
       ++it;
+    }
   }
 }
 
 void SafeBrowsingNavigationObserverManager::CleanUpIpAddresses() {
   for (auto it = host_to_ip_map_.begin(); it != host_to_ip_map_.end();) {
     base::EraseIf(it->second, [](const ResolvedIPAddress& resolved_ip) {
-      return IsEventExpired(resolved_ip.timestamp,
-                            kNavigationFootprintTTLInSecond);
+      return IsEventExpired(resolved_ip.timestamp, GetNavigationFootprintTTL());
     });
     if (it->second.empty())
       it = host_to_ip_map_.erase(it);
@@ -814,11 +825,19 @@
       &SafeBrowsingNavigationObserverManager::CleanUpStaleNavigationFootprints);
 }
 
-void SafeBrowsingNavigationObserverManager::AddToReferrerChain(
+void SafeBrowsingNavigationObserverManager::MaybeAddToReferrerChain(
     ReferrerChain* referrer_chain,
     NavigationEvent* nav_event,
     const GURL& destination_main_frame_url,
     ReferrerChainEntry::URLType type) {
+  // For privacy reasons, we don't actually add the referrer chain events if
+  // they are too old, no matter what `kReferrerChainEventMaximumAgeSeconds` is
+  // set to. By bailing out early here, we still trace the referrer chain and
+  // see if the older events improve the quality of the referrer chain.
+  if (IsEventExpired(nav_event->last_updated, kNavigationFootprintTTL)) {
+    return;
+  }
+
   std::unique_ptr<ReferrerChainEntry> referrer_chain_entry =
       std::make_unique<ReferrerChainEntry>();
   referrer_chain_entry->set_navigation_initiation(
@@ -912,9 +931,9 @@
       last_nav_event_traced_index = *nav_event_index;
       last_nav_event_traced = navigation_event_list_.GetNavigationEvent(
           last_nav_event_traced_index);
-      AddToReferrerChain(out_referrer_chain, last_nav_event_traced,
-                         last_main_frame_url_traced,
-                         ReferrerChainEntry::CLIENT_REDIRECT);
+      MaybeAddToReferrerChain(out_referrer_chain, last_nav_event_traced,
+                              last_main_frame_url_traced,
+                              ReferrerChainEntry::CLIENT_REDIRECT);
       // Stop searching if the size of out_referrer_chain already reached its
       // limit.
       if (!omit_non_user_gestures_is_enabled &&
@@ -938,10 +957,10 @@
     last_nav_event_traced_index = *nav_event_index;
     last_nav_event_traced =
         navigation_event_list_.GetNavigationEvent(last_nav_event_traced_index);
-    AddToReferrerChain(out_referrer_chain, last_nav_event_traced,
-                       last_main_frame_url_traced,
-                       GetURLTypeAndAdjustAttributionResult(
-                           current_user_gesture_count, out_result));
+    MaybeAddToReferrerChain(out_referrer_chain, last_nav_event_traced,
+                            last_main_frame_url_traced,
+                            GetURLTypeAndAdjustAttributionResult(
+                                current_user_gesture_count, out_result));
     // Stop searching if the size of out_referrer_chain already reached its
     // limit.
     if (!omit_non_user_gestures_is_enabled &&
diff --git a/components/safe_browsing/content/browser/safe_browsing_navigation_observer_manager.h b/components/safe_browsing/content/browser/safe_browsing_navigation_observer_manager.h
index 674afbce..d967dd0 100644
--- a/components/safe_browsing/content/browser/safe_browsing_navigation_observer_manager.h
+++ b/components/safe_browsing/content/browser/safe_browsing_navigation_observer_manager.h
@@ -64,7 +64,7 @@
 };
 
 // Struct that manages insertion, cleanup, and lookup of NavigationEvent
-// objects. Its maximum size is kNavigationRecordMaxSize.
+// objects. Its maximum size is `GetNavigationRecordMaxSize()`.
 struct NavigationEventList {
  public:
   explicit NavigationEventList(std::size_t size_limit);
@@ -175,7 +175,7 @@
                                               public KeyedService {
  public:
   // Helper function to check if user gesture is older than
-  // kUserGestureTTLInSecond.
+  // kUserGestureTTL.
   static bool IsUserGestureExpired(const base::Time& timestamp);
 
   // Helper function to strip ref fragment from a URL. Many pages end up with a
@@ -223,7 +223,7 @@
   void OnWebContentDestroyed(content::WebContents* web_contents);
 
   // Removes all the observed NavigationEvents, user gestures, and resolved IP
-  // addresses that are older than kNavigationFootprintTTLInSecond.
+  // addresses that are older than `GetNavigationFootprintTTL()`.
   void CleanUpStaleNavigationFootprints();
 
   // Based on the |event_url| and |event_tab_id|, traces back the observed
@@ -328,25 +328,27 @@
   HostToIpMap* host_to_ip_map() { return &host_to_ip_map_; }
 
   // Remove stale entries from navigation_event_list_ if they are older than
-  // kNavigationFootprintTTLInSecond (2 minutes).
+  // `GetNavigationFootprintTTL()`.
   void CleanUpNavigationEvents();
 
   // Remove stale entries from user_gesture_map_ if they are older than
-  // kNavigationFootprintTTLInSecond (2 minutes).
+  // `GetNavigationFootprintTTL()`.
   void CleanUpUserGestures();
 
   // Remove stale entries from host_to_ip_map_ if they are older than
-  // kNavigationFootprintTTLInSecond (2 minutes).
+  // `GetNavigationFootprintTTL()`.
   void CleanUpIpAddresses();
 
   bool IsCleanUpScheduled() const;
 
   void ScheduleNextCleanUpAfterInterval(base::TimeDelta interval);
 
-  void AddToReferrerChain(ReferrerChain* referrer_chain,
-                          NavigationEvent* nav_event,
-                          const GURL& destination_main_frame_url,
-                          ReferrerChainEntry::URLType type);
+  // Adds the event to the referrer chain, unless it is older than
+  // `GetNavigationFootprintTTL()`.
+  void MaybeAddToReferrerChain(ReferrerChain* referrer_chain,
+                               NavigationEvent* nav_event,
+                               const GURL& destination_main_frame_url,
+                               ReferrerChainEntry::URLType type);
 
   // Helper function to get the remaining referrer chain when we've already
   // traced back |current_user_gesture_count| number of user gestures.
@@ -381,7 +383,7 @@
   // frames, this list of NavigationEvents are ordered by navigation finish
   // time. Entries in navigation_event_list_ will be removed if they are older
   // than 2 minutes since their corresponding navigations finish or there are
-  // more than kNavigationRecordMaxSize entries.
+  // more than `GetNavigationRecordMaxSize()` entries.
   NavigationEventList navigation_event_list_;
 
   // user_gesture_map_ keeps track of the timestamp of last user gesture in
diff --git a/components/safe_browsing/content/browser/safe_browsing_navigation_observer_unittest.cc b/components/safe_browsing/content/browser/safe_browsing_navigation_observer_unittest.cc
index f5d1070..ca4138a 100644
--- a/components/safe_browsing/content/browser/safe_browsing_navigation_observer_unittest.cc
+++ b/components/safe_browsing/content/browser/safe_browsing_navigation_observer_unittest.cc
@@ -115,19 +115,16 @@
   void CreateNonUserGestureReferrerChain() {
     user_gesture_map()->clear();
     base::Time now = base::Time::Now();
-    base::Time half_hour_ago =
-        base::Time::FromDoubleT(now.ToDoubleT() - 30.0 * 60.0);
-    base::Time one_hour_ago =
-        base::Time::FromDoubleT(now.ToDoubleT() - 60.0 * 60.0);
-    base::Time two_hours_ago =
-        base::Time::FromDoubleT(now.ToDoubleT() - 2 * 60.0 * 60.0);
+    base::Time half_second_ago = base::Time::FromDoubleT(now.ToDoubleT() - 0.5);
+    base::Time one_second_ago = base::Time::FromDoubleT(now.ToDoubleT() - 1.0);
+    base::Time two_seconds_ago = base::Time::FromDoubleT(now.ToDoubleT() - 2.0);
 
     // Add 13 navigations and one starting page. The first is BROWSER_INITIATED
     // to A. Then from A to B, then 10 redirects to C, then back to A.
     std::unique_ptr<NavigationEvent> first_navigation =
         std::make_unique<NavigationEvent>();
     first_navigation->original_request_url = GURL("http://A.com");
-    first_navigation->last_updated = two_hours_ago;
+    first_navigation->last_updated = two_seconds_ago;
     first_navigation->navigation_initiation =
         ReferrerChainEntry::BROWSER_INITIATED;
     navigation_event_list()->RecordNavigationEvent(std::move(first_navigation));
@@ -136,7 +133,7 @@
         std::make_unique<NavigationEvent>();
     second_navigation->source_url = GURL("http://A.com");
     second_navigation->original_request_url = GURL("http://B.com");
-    second_navigation->last_updated = one_hour_ago;
+    second_navigation->last_updated = one_second_ago;
     second_navigation->navigation_initiation =
         ReferrerChainEntry::RENDERER_INITIATED_WITH_USER_GESTURE;
     navigation_event_list()->RecordNavigationEvent(
@@ -149,7 +146,7 @@
           std::make_unique<NavigationEvent>();
       navigation->source_url = prev_url;
       navigation->original_request_url = current_url;
-      navigation->last_updated = one_hour_ago;
+      navigation->last_updated = one_second_ago;
       navigation->navigation_initiation =
           ReferrerChainEntry::RENDERER_INITIATED_WITHOUT_USER_GESTURE;
       navigation_event_list()->RecordNavigationEvent(std::move(navigation));
@@ -161,7 +158,7 @@
         std::make_unique<NavigationEvent>();
     last_navigation->source_url = prev_url;
     last_navigation->original_request_url = GURL("http://A.com");
-    last_navigation->last_updated = half_hour_ago;
+    last_navigation->last_updated = half_second_ago;
     last_navigation->navigation_initiation =
         ReferrerChainEntry::RENDERER_INITIATED_WITH_USER_GESTURE;
     navigation_event_list()->RecordNavigationEvent(std::move(last_navigation));
@@ -243,12 +240,9 @@
 TEST_F(SBNavigationObserverTest, TestInfiniteLoop) {
   user_gesture_map()->clear();
   base::Time now = base::Time::Now();
-  base::Time half_hour_ago =
-      base::Time::FromDoubleT(now.ToDoubleT() - 30.0 * 60.0);
-  base::Time one_hour_ago =
-      base::Time::FromDoubleT(now.ToDoubleT() - 60.0 * 60.0);
-  base::Time two_hours_ago =
-      base::Time::FromDoubleT(now.ToDoubleT() - 2 * 60.0 * 60.0);
+  base::Time half_second_ago = base::Time::FromDoubleT(now.ToDoubleT() - 0.5);
+  base::Time one_second_ago = base::Time::FromDoubleT(now.ToDoubleT() - 1.0);
+  base::Time two_seconds_ago = base::Time::FromDoubleT(now.ToDoubleT() - 2.0);
 
   // Add 5 navigations and one starting page. The first is BROWSER_INITIATED
   // to A. Then from A to B, then 2 redirects back and forth between B and C,
@@ -256,7 +250,7 @@
   std::unique_ptr<NavigationEvent> first_navigation =
       std::make_unique<NavigationEvent>();
   first_navigation->original_request_url = GURL("http://A.com");
-  first_navigation->last_updated = two_hours_ago;
+  first_navigation->last_updated = two_seconds_ago;
   first_navigation->navigation_initiation =
       ReferrerChainEntry::BROWSER_INITIATED;
   navigation_event_list()->RecordNavigationEvent(std::move(first_navigation));
@@ -265,7 +259,7 @@
       std::make_unique<NavigationEvent>();
   second_navigation->source_url = GURL("http://A.com");
   second_navigation->original_request_url = GURL("http://B.com");
-  second_navigation->last_updated = one_hour_ago;
+  second_navigation->last_updated = one_second_ago;
   second_navigation->navigation_initiation =
       ReferrerChainEntry::RENDERER_INITIATED_WITH_USER_GESTURE;
   navigation_event_list()->RecordNavigationEvent(std::move(second_navigation));
@@ -276,7 +270,7 @@
         std::make_unique<NavigationEvent>();
     navigation->source_url = prev_url;
     navigation->original_request_url = current_url;
-    navigation->last_updated = one_hour_ago;
+    navigation->last_updated = one_second_ago;
     navigation->navigation_initiation =
         ReferrerChainEntry::RENDERER_INITIATED_WITHOUT_USER_GESTURE;
     navigation_event_list()->RecordNavigationEvent(std::move(navigation));
@@ -289,7 +283,7 @@
       std::make_unique<NavigationEvent>();
   last_navigation->source_url = prev_url;
   last_navigation->original_request_url = GURL("http://A.com");
-  last_navigation->last_updated = half_hour_ago;
+  last_navigation->last_updated = half_second_ago;
   last_navigation->navigation_initiation =
       ReferrerChainEntry::RENDERER_INITIATED_WITH_USER_GESTURE;
   navigation_event_list()->RecordNavigationEvent(std::move(last_navigation));
@@ -610,17 +604,15 @@
 
 TEST_F(SBNavigationObserverTest, TimestampIsDecreasing) {
   base::Time now = base::Time::Now();
-  base::Time one_hour_ago =
-      base::Time::FromDoubleT(now.ToDoubleT() - 60.0 * 60.0);
-  base::Time two_hours_ago =
-      base::Time::FromDoubleT(now.ToDoubleT() - 2 * 60.0 * 60.0);
+  base::Time one_second_ago = base::Time::FromDoubleT(now.ToDoubleT() - 1.0);
+  base::Time two_seconds_ago = base::Time::FromDoubleT(now.ToDoubleT() - 2.0);
 
   // Add three navigations. The first is BROWSER_INITIATED to A. Then from A to
   // B, and then from B back to A.
   std::unique_ptr<NavigationEvent> first_navigation =
       std::make_unique<NavigationEvent>();
   first_navigation->original_request_url = GURL("http://A.com");
-  first_navigation->last_updated = two_hours_ago;
+  first_navigation->last_updated = two_seconds_ago;
   first_navigation->navigation_initiation =
       ReferrerChainEntry::BROWSER_INITIATED;
   navigation_event_list()->RecordNavigationEvent(std::move(first_navigation));
@@ -629,7 +621,7 @@
       std::make_unique<NavigationEvent>();
   second_navigation->source_url = GURL("http://A.com");
   second_navigation->original_request_url = GURL("http://B.com");
-  second_navigation->last_updated = one_hour_ago;
+  second_navigation->last_updated = one_second_ago;
   second_navigation->navigation_initiation =
       ReferrerChainEntry::RENDERER_INITIATED_WITH_USER_GESTURE;
   navigation_event_list()->RecordNavigationEvent(std::move(second_navigation));
@@ -698,14 +690,14 @@
 TEST_F(SBNavigationObserverTest,
        RemoveNonUserGestureEntriesWithExcessiveUserGestureEvents) {
   GURL url = GURL("http://A.com");
-  base::Time half_hour_ago =
-      base::Time::FromDoubleT(base::Time::Now().ToDoubleT() - 30.0 * 60.0);
+  base::Time half_second_ago =
+      base::Time::FromDoubleT(base::Time::Now().ToDoubleT() - 0.5);
   // Append 10 navigation events with user gesture.
   for (int i = 0; i < 10; i++) {
     std::unique_ptr<NavigationEvent> navigation_event =
         std::make_unique<NavigationEvent>();
     navigation_event->source_url = url;
-    navigation_event->last_updated = half_hour_ago;
+    navigation_event->last_updated = half_second_ago;
     navigation_event->navigation_initiation =
         ReferrerChainEntry::RENDERER_INITIATED_WITH_USER_GESTURE;
     navigation_event_list()->RecordNavigationEvent(std::move(navigation_event));
@@ -756,8 +748,7 @@
 
 TEST_F(SBNavigationObserverTest, ChainWorksThroughNewTab) {
   base::Time now = base::Time::Now();
-  base::Time one_hour_ago =
-      base::Time::FromDoubleT(now.ToDoubleT() - 60.0 * 60.0);
+  base::Time one_second_ago = base::Time::FromDoubleT(now.ToDoubleT() - 1.0);
 
   SessionID source_tab = SessionID::NewUnique();
   SessionID target_tab = SessionID::NewUnique();
@@ -768,7 +759,7 @@
       std::make_unique<NavigationEvent>();
   first_navigation->source_url = GURL("http://a.com/");
   first_navigation->original_request_url = GURL("http://b.com/");
-  first_navigation->last_updated = one_hour_ago;
+  first_navigation->last_updated = one_second_ago;
   first_navigation->navigation_initiation =
       ReferrerChainEntry::RENDERER_INITIATED_WITH_USER_GESTURE;
   first_navigation->source_tab_id = source_tab;
@@ -799,13 +790,12 @@
 
 TEST_F(SBNavigationObserverTest, ChainContinuesThroughBrowserInitiated) {
   base::Time now = base::Time::Now();
-  base::Time one_hour_ago =
-      base::Time::FromDoubleT(now.ToDoubleT() - 60.0 * 60.0);
+  base::Time one_second_ago = base::Time::FromDoubleT(now.ToDoubleT() - 1.0);
 
   std::unique_ptr<NavigationEvent> first_navigation =
       std::make_unique<NavigationEvent>();
   first_navigation->original_request_url = GURL("http://a.com/");
-  first_navigation->last_updated = one_hour_ago;
+  first_navigation->last_updated = one_second_ago;
   first_navigation->navigation_initiation =
       ReferrerChainEntry::BROWSER_INITIATED;
   navigation_event_list()->RecordNavigationEvent(std::move(first_navigation));
@@ -830,8 +820,7 @@
 TEST_F(SBNavigationObserverTest,
        CanceledRetargetingNavigationHasCorrectEventUrl) {
   base::Time now = base::Time::Now();
-  base::Time one_hour_ago =
-      base::Time::FromDoubleT(now.ToDoubleT() - 60.0 * 60.0);
+  base::Time one_second_ago = base::Time::FromDoubleT(now.ToDoubleT() - 1.0);
 
   SessionID source_tab = SessionID::NewUnique();
   SessionID target_tab = SessionID::NewUnique();
@@ -843,7 +832,7 @@
       std::make_unique<NavigationEvent>();
   first_navigation->source_url = GURL("http://example.com/a");
   first_navigation->original_request_url = GURL("http://example.com/b");
-  first_navigation->last_updated = one_hour_ago;
+  first_navigation->last_updated = one_second_ago;
   first_navigation->navigation_initiation =
       ReferrerChainEntry::RENDERER_INITIATED_WITH_USER_GESTURE;
   first_navigation->source_tab_id = source_tab;
@@ -902,8 +891,7 @@
 
 TEST_F(SBNavigationObserverTest, SanitizesDataUrls) {
   base::Time now = base::Time::Now();
-  base::Time one_hour_ago =
-      base::Time::FromDoubleT(now.ToDoubleT() - 60.0 * 60.0);
+  base::Time one_second_ago = base::Time::FromDoubleT(now.ToDoubleT() - 1.0);
 
   SessionID tab_id = SessionID::NewUnique();
 
@@ -913,7 +901,7 @@
       std::make_unique<NavigationEvent>();
   first_navigation->source_url = GURL("http://a.com/");
   first_navigation->original_request_url = GURL("data://private_data");
-  first_navigation->last_updated = one_hour_ago;
+  first_navigation->last_updated = one_second_ago;
   first_navigation->navigation_initiation =
       ReferrerChainEntry::RENDERER_INITIATED_WITH_USER_GESTURE;
   first_navigation->source_tab_id = tab_id;
diff --git a/components/safe_browsing/core/common/features.cc b/components/safe_browsing/core/common/features.cc
index 95e6aaab..ee0a19a8 100644
--- a/components/safe_browsing/core/common/features.cc
+++ b/components/safe_browsing/core/common/features.cc
@@ -191,6 +191,17 @@
              "SafeBrowsingRealTimeUrlLookupForEnterpriseAllowlistBypass",
              base::FEATURE_ENABLED_BY_DEFAULT);
 
+BASE_FEATURE(kReferrerChainParameters,
+             "SafeBrowsingReferrerChainParameters",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
+constexpr base::FeatureParam<int> kReferrerChainEventMaximumAgeSeconds{
+    &kReferrerChainParameters, "MaximumEventAgeSeconds", /*default_value=*/120};
+
+constexpr base::FeatureParam<int> kReferrerChainEventMaximumCount{
+    &kReferrerChainParameters, "MaximumEventCount",
+    /*default_value=*/100};
+
 BASE_FEATURE(kSafeBrowsingCsbrrNewDownloadTrigger,
              "SafeBrowsingCsbrrNewDownloadTrigger",
              base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/components/safe_browsing/core/common/features.h b/components/safe_browsing/core/common/features.h
index 21a624b..a02c71d 100644
--- a/components/safe_browsing/core/common/features.h
+++ b/components/safe_browsing/core/common/features.h
@@ -191,6 +191,18 @@
 // Bypass RealTime URL Lookup allowlist for enterprise users.
 BASE_DECLARE_FEATURE(kRealTimeUrlLookupForEnterpriseAllowlistBypass);
 
+// Enables modifying key parameters on the navigation event collection used to
+// populate referrer chains.
+BASE_DECLARE_FEATURE(kReferrerChainParameters);
+
+// The maximum age entry we keep in memory. Older entries are cleaned up. This
+// is independent of the maximum age entry we send to Safe Browsing, which is
+// fixed for privacy reasons.
+extern const base::FeatureParam<int> kReferrerChainEventMaximumAgeSeconds;
+
+// The maximum number of navigation events we keep in memory.
+extern const base::FeatureParam<int> kReferrerChainEventMaximumCount;
+
 // Controls whether download Client Safe Browsing Reports are sent under the
 // new triggers
 BASE_DECLARE_FEATURE(kSafeBrowsingCsbrrNewDownloadTrigger);
diff --git a/components/segmentation_platform/embedder/default_model/device_switcher_model.cc b/components/segmentation_platform/embedder/default_model/device_switcher_model.cc
index 1fae71d..c0b5c9f0 100644
--- a/components/segmentation_platform/embedder/default_model/device_switcher_model.cc
+++ b/components/segmentation_platform/embedder/default_model/device_switcher_model.cc
@@ -85,7 +85,6 @@
       .name = "SyncDeviceInfo"});
   (*sync_input->mutable_additional_args())["wait_for_device_info_in_seconds"] =
       "60";
-  (*sync_input->mutable_additional_args())["active_days_limit"] = "14";
 
   writer.AddOutputConfigForMultiClassClassifier(
       kOutputLabels.begin(), kOutputLabels.size(), kOutputLabels.size(), 0.1);
diff --git a/components/segmentation_platform/internal/execution/processing/sync_device_info_observer.cc b/components/segmentation_platform/internal/execution/processing/sync_device_info_observer.cc
index a7a158b..e5bbb7b 100644
--- a/components/segmentation_platform/internal/execution/processing/sync_device_info_observer.cc
+++ b/components/segmentation_platform/internal/execution/processing/sync_device_info_observer.cc
@@ -156,7 +156,8 @@
        device_info_status_ == DeviceInfoStatus::TIMEOUT_POSTED_BUT_NOT_HIT)) {
     pending_actions_.push_back(base::BindOnce(
         &SyncDeviceInfoObserver::ReadyToFinishProcessing,
-        weak_ptr_factory_.GetWeakPtr(), input, std::move(callback)));
+        weak_ptr_factory_.GetWeakPtr(), input,
+        feature_processor_state.input_context(), std::move(callback)));
 
     if (device_info_status_ == DeviceInfoStatus::TIMEOUT_NOT_POSTED) {
       device_info_status_ = DeviceInfoStatus::TIMEOUT_POSTED_BUT_NOT_HIT;
@@ -168,13 +169,14 @@
     }
   } else {
     ReadyToFinishProcessing(
-        input, std::move(callback),
+        input, feature_processor_state.input_context(), std::move(callback),
         device_info_status_ == DeviceInfoStatus::INFO_AVAILABLE);
   }
 }
 
 void SyncDeviceInfoObserver::ReadyToFinishProcessing(
     const proto::CustomInput& input,
+    scoped_refptr<InputContext> input_context,
     ProcessedCallback callback,
     bool success) {
   if (!success) {
@@ -185,10 +187,14 @@
   }
 
   int active_threshold = kDefaultActiveDaysThreshold;
-  auto it2 = input.additional_args().find("active_days_limit");
-  if (it2 != input.additional_args().end()) {
-    if (!base::StringToInt(it2->second, &active_threshold)) {
-      active_threshold = kDefaultActiveDaysThreshold;
+  if (input_context) {
+    auto input_context_iter =
+        input_context->metadata_args.find("active_days_limit");
+    if (input_context_iter != input_context->metadata_args.end()) {
+      const auto& processed_value = input_context_iter->second;
+      if (processed_value.type == ProcessedValue::INT) {
+        active_threshold = processed_value.int_val;
+      }
     }
   }
   std::map<
diff --git a/components/segmentation_platform/internal/execution/processing/sync_device_info_observer.h b/components/segmentation_platform/internal/execution/processing/sync_device_info_observer.h
index 32b4e36..8866247 100644
--- a/components/segmentation_platform/internal/execution/processing/sync_device_info_observer.h
+++ b/components/segmentation_platform/internal/execution/processing/sync_device_info_observer.h
@@ -9,6 +9,7 @@
 #include "base/feature_list.h"
 #include "base/memory/weak_ptr.h"
 #include "base/time/time.h"
+#include "components/segmentation_platform/public/input_context.h"
 #include "components/segmentation_platform/public/input_delegate.h"
 #include "components/sync_device_info/device_info.h"
 #include "components/sync_device_info/device_info_tracker.h"
@@ -49,6 +50,7 @@
 
   // Called when ready to finish processing.
   void ReadyToFinishProcessing(const proto::CustomInput& input,
+                               scoped_refptr<InputContext> input_context,
                                ProcessedCallback callback,
                                bool success);
 
diff --git a/components/segmentation_platform/internal/execution/processing/sync_device_info_observer_unittest.cc b/components/segmentation_platform/internal/execution/processing/sync_device_info_observer_unittest.cc
index 8be17d0..d760670 100644
--- a/components/segmentation_platform/internal/execution/processing/sync_device_info_observer_unittest.cc
+++ b/components/segmentation_platform/internal/execution/processing/sync_device_info_observer_unittest.cc
@@ -53,6 +53,12 @@
       /*interested_data_types=*/syncer::ModelTypeSet());
 }
 
+scoped_refptr<InputContext> CreateInputContext() {
+  auto input_context = base::MakeRefCounted<InputContext>();
+  input_context->metadata_args.emplace("active_days_limit", 14);
+  return input_context;
+}
+
 class SyncDeviceInfoObserverTest : public testing::Test {
  public:
   SyncDeviceInfoObserverTest() = default;
@@ -165,7 +171,7 @@
       .name = "SyncDeviceInfo"});
   (*sync_input->mutable_additional_args())["wait_for_device_info_in_seconds"] =
       "2";
-  (*sync_input->mutable_additional_args())["active_days_limit"] = "14";
+  state.set_input_context_for_testing(CreateInputContext());
 
   std::unique_ptr<DeviceInfo> local_device_info =
       CreateDeviceInfo("local_device", kLocalDeviceType, kLocalDeviceOS);
@@ -200,7 +206,7 @@
       .name = "SyncDeviceInfo"});
   (*sync_input->mutable_additional_args())["wait_for_device_info_in_seconds"] =
       "2";
-  (*sync_input->mutable_additional_args())["active_days_limit"] = "14";
+  state.set_input_context_for_testing(CreateInputContext());
 
   std::vector<float> expected_result = {0, 0, 0, 0, 0, 1, 0, 0, 0, 0};
   base::RunLoop loop;
@@ -227,7 +233,7 @@
       .name = "SyncDeviceInfo"});
   (*sync_input->mutable_additional_args())["wait_for_device_info_in_seconds"] =
       "0";
-  (*sync_input->mutable_additional_args())["active_days_limit"] = "14";
+  state.set_input_context_for_testing(CreateInputContext());
 
   // Failure flag is set.
   std::vector<float> expected_result = {1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
@@ -255,7 +261,7 @@
       .name = "SyncDeviceInfo"});
   (*sync_input->mutable_additional_args())["wait_for_device_info_in_seconds"] =
       "true";
-  (*sync_input->mutable_additional_args())["active_days_limit"] = "14";
+  state.set_input_context_for_testing(CreateInputContext());
 
   // Failure flag is set.
   std::vector<float> expected_result = {1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
@@ -284,7 +290,7 @@
       .name = "SyncDeviceInfo"});
   (*sync_input->mutable_additional_args())["wait_for_device_info_in_seconds"] =
       "2";
-  (*sync_input->mutable_additional_args())["active_days_limit"] = "14";
+  state.set_input_context_for_testing(CreateInputContext());
 
   // Failure flag is set.
   std::vector<float> expected_result = {1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
diff --git a/components/segmentation_platform/public/proto/model_metadata.proto b/components/segmentation_platform/public/proto/model_metadata.proto
index c700e280..1cfb519 100644
--- a/components/segmentation_platform/public/proto/model_metadata.proto
+++ b/components/segmentation_platform/public/proto/model_metadata.proto
@@ -128,11 +128,12 @@
     // Output type: float
     // Output length: 10
     // Additional arg:
-    //   `active_days_limit`: Number of days after which the device is
-    //   considered not active after last sync.
     //   `wait_for_device_info_in_seconds`: Number of seconds to wait for sync
     //   device info before timeout. If 0, then does not wait for sync and times
     //   out immediately if device info is not available.
+    // InputContext arg:
+    //   `active_days_limit`: Number of days after which the device is
+    //   considered not active after last sync. Must be INT.
     FILL_SYNC_DEVICE_INFO = 5;
   }
 
diff --git a/components/user_education/common/feature_promo_controller.cc b/components/user_education/common/feature_promo_controller.cc
index 4cd85975..6e8c92e 100644
--- a/components/user_education/common/feature_promo_controller.cc
+++ b/components/user_education/common/feature_promo_controller.cc
@@ -508,15 +508,17 @@
     DCHECK_EQ(current_iph_feature_, iph_feature);
     tutorial_promo_handle_ = CloseBubbleAndContinuePromo(*iph_feature);
     DCHECK(tutorial_promo_handle_.is_valid());
-    if (tutorial_service_->StartTutorial(
-            tutorial_id, GetAnchorContext(),
-            base::BindOnce(&FeaturePromoControllerCommon::OnTutorialComplete,
-                           weak_ptr_factory_.GetWeakPtr(),
-                           base::Unretained(iph_feature)),
-            base::BindOnce(&FeaturePromoControllerCommon::OnTutorialAborted,
-                           weak_ptr_factory_.GetWeakPtr(),
-                           base::Unretained(iph_feature))))
+    tutorial_service_->StartTutorial(
+        tutorial_id, GetAnchorContext(),
+        base::BindOnce(&FeaturePromoControllerCommon::OnTutorialComplete,
+                       weak_ptr_factory_.GetWeakPtr(),
+                       base::Unretained(iph_feature)),
+        base::BindOnce(&FeaturePromoControllerCommon::OnTutorialAborted,
+                       weak_ptr_factory_.GetWeakPtr(),
+                       base::Unretained(iph_feature)));
+    if (tutorial_service_->IsRunningTutorial()) {
       tutorial_service_->LogIPHLinkClicked(tutorial_id, true);
+    }
   }
 }
 
diff --git a/components/user_education/common/tutorial_service.cc b/components/user_education/common/tutorial_service.cc
index 7e0eef6..e217abb 100644
--- a/components/user_education/common/tutorial_service.cc
+++ b/components/user_education/common/tutorial_service.cc
@@ -9,6 +9,9 @@
 
 #include "base/auto_reset.h"
 #include "base/callback_list.h"
+#include "base/functional/bind.h"
+#include "base/logging.h"
+#include "base/time/time.h"
 #include "components/user_education/common/help_bubble.h"
 #include "components/user_education/common/help_bubble_factory_registry.h"
 #include "components/user_education/common/tutorial.h"
@@ -17,6 +20,17 @@
 
 namespace user_education {
 
+namespace {
+// How long a tutorial has to go without a bubble before we assume it's broken
+// and abort it.
+constexpr base::TimeDelta kBrokenTutorialTimeout = base::Seconds(15);
+// How long a tutorial has to go before the first bubble is shown before we
+// assume it's been broken or abandoned and abort it. This is longer than the
+// above because we want to allow the user time to navigate to the surface that
+// triggers the tutorial.
+constexpr base::TimeDelta kTutorialNotStartedTimeout = base::Seconds(60);
+}  // namespace
+
 TutorialService::TutorialCreationParams::TutorialCreationParams(
     TutorialDescription* description,
     ui::ElementContext context)
@@ -35,21 +49,28 @@
 
 TutorialService::~TutorialService() = default;
 
-bool TutorialService::StartTutorial(TutorialIdentifier id,
+void TutorialService::StartTutorial(TutorialIdentifier id,
                                     ui::ElementContext context,
                                     CompletedCallback completed_callback,
                                     AbortedCallback aborted_callback) {
-  // Overriding an existing running tutorial is not supported. In this case
-  // return false to the caller.
-  if (running_tutorial_)
-    return false;
+  // End the current tutorial, if any.
+  if (running_tutorial_) {
+    if (is_final_bubble_) {
+      // The current tutorial is showing the final congratulatory bubble, so it
+      // is effectively complete.
+      CompleteTutorial();
+    } else {
+      running_tutorial_->Abort();
+    }
+  }
+  is_final_bubble_ = false;
 
   // Get the description from the tutorial registry.
   TutorialDescription* description =
       tutorial_registry_->GetTutorialDescription(id);
   CHECK(description);
 
-  // Construct the tutorial from the dsecription.
+  // Construct the tutorial from the description.
   running_tutorial_ =
       Tutorial::Builder::BuildFromDescription(*description, this, context);
 
@@ -61,11 +82,16 @@
   running_tutorial_creation_params_ =
       std::make_unique<TutorialCreationParams>(description, context);
 
+  // Before starting the tutorial, set a timeout just in case the user never
+  // actually gets to a place where they can launch the first bubble.
+  broken_tutorial_timer_.Start(
+      FROM_HERE, kTutorialNotStartedTimeout,
+      base::BindOnce(&TutorialService::OnBrokenTutorial,
+                     base::Unretained(this)));
+
   // Start the tutorial and mark the params used to created it for restarting.
   running_tutorial_->Start();
   toggle_focus_count_ = 0;
-
-  return true;
 }
 
 void TutorialService::LogIPHLinkClicked(TutorialIdentifier id,
@@ -126,17 +152,20 @@
 
   // If the tutorial had been restarted and then aborted, The tutorial should be
   // considered completed.
-  if (running_tutorial_was_restarted_)
-    return CompleteTutorial();
+  if (running_tutorial_was_restarted_) {
+    CompleteTutorial();
+    return;
+  }
 
-  if (abort_step.has_value())
-    running_tutorial_creation_params_->description_->histograms
-        ->RecordAbortStep(abort_step.value());
-
-  // Log the failure of completion for the tutorial.
-  if (running_tutorial_creation_params_->description_->histograms)
+  if (running_tutorial_creation_params_->description_->histograms) {
+    if (abort_step.has_value()) {
+      running_tutorial_creation_params_->description_->histograms
+          ->RecordAbortStep(abort_step.value());
+    }
     running_tutorial_creation_params_->description_->histograms->RecordComplete(
         false);
+  }
+
   UMA_HISTOGRAM_BOOLEAN("Tutorial.Completion", false);
 
   // Reset the tutorial and call the external abort callback.
@@ -153,6 +182,21 @@
   }
 }
 
+void TutorialService::OnNonFinalBubbleClosed(HelpBubble* bubble) {
+  LOG(WARNING) << "On non final bubble closed.";
+  if (bubble != currently_displayed_bubble_.get()) {
+    return;
+  }
+
+  bubble_closed_subscription_ = base::CallbackListSubscription();
+  currently_displayed_bubble_.reset();
+
+  broken_tutorial_timer_.Start(
+      FROM_HERE, kBrokenTutorialTimeout,
+      base::BindOnce(&TutorialService::OnBrokenTutorial,
+                     base::Unretained(this)));
+}
+
 void TutorialService::CompleteTutorial() {
   DCHECK(running_tutorial_);
 
@@ -177,24 +221,27 @@
                                        bool is_last_step) {
   DCHECK(running_tutorial_);
   currently_displayed_bubble_ = std::move(bubble);
+  broken_tutorial_timer_.Stop();
   if (is_last_step) {
-    final_bubble_closed_subscription_ =
+    is_final_bubble_ = true;
+    bubble_closed_subscription_ =
         currently_displayed_bubble_->AddOnCloseCallback(base::BindOnce(
             [](TutorialService* service, user_education::HelpBubble*) {
               service->CompleteTutorial();
             },
             base::Unretained(this)));
   } else {
-    // If this was not the final bubble, we shouldn't be subscribed to a
-    // different "final bubble".
-    DCHECK(!final_bubble_closed_subscription_);
+    is_final_bubble_ = false;
+    bubble_closed_subscription_ =
+        currently_displayed_bubble_->AddOnCloseCallback(base::BindOnce(
+            &TutorialService::OnNonFinalBubbleClosed, base::Unretained(this)));
   }
 }
 
 void TutorialService::HideCurrentBubbleIfShowing() {
   if (!currently_displayed_bubble_)
     return;
-  final_bubble_closed_subscription_ = base::CallbackListSubscription();
+  bubble_closed_subscription_ = base::CallbackListSubscription();
   currently_displayed_bubble_.reset();
 }
 
@@ -204,6 +251,7 @@
 
 void TutorialService::ResetRunningTutorial() {
   DCHECK(running_tutorial_);
+  broken_tutorial_timer_.Stop();
   running_tutorial_.reset();
   running_tutorial_creation_params_.reset();
   running_tutorial_was_restarted_ = false;
@@ -215,4 +263,10 @@
     ++toggle_focus_count_;
 }
 
+void TutorialService::OnBrokenTutorial() {
+  if (running_tutorial_ && !currently_displayed_bubble_) {
+    running_tutorial_->Abort();
+  }
+}
+
 }  // namespace user_education
diff --git a/components/user_education/common/tutorial_service.h b/components/user_education/common/tutorial_service.h
index fad65a0..b9151c6 100644
--- a/components/user_education/common/tutorial_service.h
+++ b/components/user_education/common/tutorial_service.h
@@ -12,6 +12,7 @@
 #include "base/functional/callback_forward.h"
 #include "base/functional/callback_helpers.h"
 #include "base/memory/raw_ptr.h"
+#include "base/timer/timer.h"
 #include "components/user_education/common/tutorial.h"
 #include "components/user_education/common/tutorial_identifier.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
@@ -39,7 +40,7 @@
   using AbortedCallback = base::OnceClosure;
 
   // Returns true if there is a currently running tutorial.
-  bool IsRunningTutorial() const;
+  virtual bool IsRunningTutorial() const;
 
   // Sets the current help bubble stored by the service.
   void SetCurrentBubble(std::unique_ptr<HelpBubble> bubble, bool is_last_step);
@@ -48,7 +49,8 @@
   void HideCurrentBubbleIfShowing();
 
   // Starts the tutorial by looking for the id in the Tutorial Registry.
-  virtual bool StartTutorial(
+  // Any existing tutorial is canceled.
+  virtual void StartTutorial(
       TutorialIdentifier id,
       ui::ElementContext context,
       CompletedCallback completed_callback = base::DoNothing(),
@@ -93,6 +95,10 @@
     ui::ElementContext context_;
   };
 
+  // Called when a non-final bubble is closed. Used to trigger the broken
+  // tutorial timeout.
+  void OnNonFinalBubbleClosed(HelpBubble* bubble);
+
   // Calls the completion code for the running tutorial.
   // TODO (dpenning): allow for registering a callback that performs any
   // IPH/other code on completion of tutorial
@@ -104,6 +110,10 @@
   // Tracks when the user toggles focus to a help bubble via the keyboard.
   void OnFocusToggledForAccessibility(HelpBubble* bubble);
 
+  // Called when there has been no bubble visible for enough time that the
+  // current tutorial should probably be aborted.
+  void OnBrokenTutorial();
+
   // Creation params for the last started tutorial. Used to restart the
   // tutorial after it has been completed.
   std::unique_ptr<TutorialCreationParams> running_tutorial_creation_params_;
@@ -128,7 +138,14 @@
   std::unique_ptr<HelpBubble> currently_displayed_bubble_;
 
   // Listens for when the final bubble in a Tutorial is closed.
-  base::CallbackListSubscription final_bubble_closed_subscription_;
+  base::CallbackListSubscription bubble_closed_subscription_;
+
+  // Whether the currently-displayed bubble is the final one.
+  bool is_final_bubble_ = false;
+
+  // Used to check for broken tutorials - when no bubble is visible for an
+  // unexpected period of time.
+  base::OneShotTimer broken_tutorial_timer_;
 
   // Pointers to the registries used for constructing and showing tutorials and
   // help bubbles.
diff --git a/components/user_education/common/tutorial_unittest.cc b/components/user_education/common/tutorial_unittest.cc
index c0df87b6..e1b7b56 100644
--- a/components/user_education/common/tutorial_unittest.cc
+++ b/components/user_education/common/tutorial_unittest.cc
@@ -9,6 +9,7 @@
 #include "base/test/bind.h"
 #include "base/test/gtest_util.h"
 #include "base/test/mock_callback.h"
+#include "base/test/task_environment.h"
 #include "components/strings/grit/components_strings.h"
 #include "components/user_education/common/help_bubble_factory_registry.h"
 #include "components/user_education/common/help_bubble_params.h"
@@ -89,7 +90,15 @@
 
 }  // namespace
 
-class TutorialTest : public testing::Test {};
+class TutorialTest : public testing::Test {
+ public:
+  TutorialTest() = default;
+  ~TutorialTest() override = default;
+
+ protected:
+  base::test::TaskEnvironment task_environment_{
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+};
 
 TEST_F(TutorialTest, TutorialBuilder) {
   const auto bubble_factory_registry =
@@ -306,6 +315,66 @@
       ClickCloseButton(service.currently_displayed_bubble_for_testing()));
 }
 
+TEST_F(TutorialTest, StartTutorialAbortsExistingTutorial) {
+  UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed);
+  UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, aborted);
+
+  const auto bubble_factory_registry =
+      CreateTestTutorialBubbleFactoryRegistry();
+  TutorialRegistry registry;
+  TestTutorialService service(&registry, bubble_factory_registry.get());
+
+  // build elements and keep them for triggering show/hide
+  ui::test::TestElement element_1(kTestIdentifier1, kTestContext1);
+  element_1.Show();
+
+  // Build the tutorial Description. This has two steps, the second of which
+  // will not
+  TutorialDescription description;
+  description.steps.emplace_back(IDS_OK, IDS_OK,
+                                 ui::InteractionSequence::StepType::kShown,
+                                 kTestIdentifier1, "", HelpBubbleArrow::kNone);
+  description.steps.emplace_back(IDS_OK, IDS_OK,
+                                 ui::InteractionSequence::StepType::kShown,
+                                 kTestIdentifier2, "", HelpBubbleArrow::kNone);
+  registry.AddTutorial(kTestTutorial1, std::move(description));
+
+  service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get(),
+                        aborted.Get());
+  EXPECT_CALL_IN_SCOPE(
+      aborted, Run, service.StartTutorial(kTestTutorial1, element_1.context()));
+  EXPECT_TRUE(service.IsRunningTutorial());
+}
+
+TEST_F(TutorialTest, StartTutorialCompletesExistingTutorial) {
+  UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed);
+  UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, aborted);
+
+  const auto bubble_factory_registry =
+      CreateTestTutorialBubbleFactoryRegistry();
+  TutorialRegistry registry;
+  TestTutorialService service(&registry, bubble_factory_registry.get());
+
+  // build elements and keep them for triggering show/hide
+  ui::test::TestElement element_1(kTestIdentifier1, kTestContext1);
+  element_1.Show();
+
+  // Build the tutorial Description. This has two steps, the second of which
+  // will not
+  TutorialDescription description;
+  description.steps.emplace_back(IDS_OK, IDS_OK,
+                                 ui::InteractionSequence::StepType::kShown,
+                                 kTestIdentifier1, "", HelpBubbleArrow::kNone);
+  registry.AddTutorial(kTestTutorial1, std::move(description));
+
+  service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get(),
+                        aborted.Get());
+  EXPECT_CALL_IN_SCOPE(
+      completed, Run,
+      service.StartTutorial(kTestTutorial1, element_1.context()));
+  EXPECT_TRUE(service.IsRunningTutorial());
+}
+
 TEST_F(TutorialTest, TutorialWithCustomEvent) {
   UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed);
 
@@ -535,4 +604,94 @@
       service.currently_displayed_bubble_for_testing()->Close());
 }
 
+TEST_F(TutorialTest, TimeoutBeforeFirstBubble) {
+  UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed);
+  UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted);
+
+  const auto bubble_factory_registry =
+      CreateTestTutorialBubbleFactoryRegistry();
+  TutorialRegistry registry;
+  TestTutorialService service(&registry, bubble_factory_registry.get());
+
+  ui::test::TestElement el(kTestIdentifier1, kTestContext1);
+
+  TutorialDescription description;
+  description.steps.emplace_back(IDS_OK, IDS_OK,
+                                 ui::InteractionSequence::StepType::kShown,
+                                 kTestIdentifier1, "", HelpBubbleArrow::kNone);
+  registry.AddTutorial(kTestTutorial1, std::move(description));
+
+  service.StartTutorial(kTestTutorial1, el.context(), completed.Get(),
+                        aborted.Get());
+  EXPECT_FALSE(service.currently_displayed_bubble_for_testing());
+  EXPECT_CALL_IN_SCOPE(aborted, Run,
+                       task_environment_.FastForwardUntilNoTasksRemain());
+}
+
+TEST_F(TutorialTest, TimeoutBetweenBubbles) {
+  UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed);
+  UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted);
+
+  const auto bubble_factory_registry =
+      CreateTestTutorialBubbleFactoryRegistry();
+  TutorialRegistry registry;
+  TestTutorialService service(&registry, bubble_factory_registry.get());
+
+  ui::test::TestElement el1(kTestIdentifier1, kTestContext1);
+  ui::test::TestElement el2(kTestIdentifier1, kTestContext1);
+  el1.Show();
+
+  TutorialDescription description;
+  description.steps.emplace_back(
+      IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown,
+      kTestIdentifier1, "", HelpBubbleArrow::kNone,
+      ui::CustomElementEventType(), /* must_remain_visible */ false);
+  description.steps.emplace_back(IDS_OK, IDS_OK,
+                                 ui::InteractionSequence::StepType::kShown,
+                                 kTestIdentifier2, "", HelpBubbleArrow::kNone);
+  registry.AddTutorial(kTestTutorial1, std::move(description));
+
+  service.StartTutorial(kTestTutorial1, el1.context(), completed.Get(),
+                        aborted.Get());
+
+  // This closes the bubble but does not advance the tutorial.
+  el1.Hide();
+  EXPECT_FALSE(service.currently_displayed_bubble_for_testing());
+  EXPECT_CALL_IN_SCOPE(aborted, Run,
+                       task_environment_.FastForwardUntilNoTasksRemain());
+}
+
+TEST_F(TutorialTest, NoTimeoutIfBubbleShowing) {
+  UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed);
+  UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted);
+
+  const auto bubble_factory_registry =
+      CreateTestTutorialBubbleFactoryRegistry();
+  TutorialRegistry registry;
+  TestTutorialService service(&registry, bubble_factory_registry.get());
+
+  ui::test::TestElement el1(kTestIdentifier1, kTestContext1);
+  ui::test::TestElement el2(kTestIdentifier1, kTestContext1);
+  el1.Show();
+
+  TutorialDescription description;
+  description.steps.emplace_back(IDS_OK, IDS_OK,
+                                 ui::InteractionSequence::StepType::kShown,
+                                 kTestIdentifier1, "", HelpBubbleArrow::kNone);
+  description.steps.emplace_back(IDS_OK, IDS_OK,
+                                 ui::InteractionSequence::StepType::kShown,
+                                 kTestIdentifier2, "", HelpBubbleArrow::kNone);
+  registry.AddTutorial(kTestTutorial1, std::move(description));
+
+  service.StartTutorial(kTestTutorial1, el1.context(), completed.Get(),
+                        aborted.Get());
+
+  // Since there is a bubble, there is no timeout.
+  EXPECT_TRUE(service.currently_displayed_bubble_for_testing());
+  task_environment_.FastForwardUntilNoTasksRemain();
+
+  // When we exit and destroy the service, the callback will be called.
+  EXPECT_CALL(aborted, Run).Times(1);
+}
+
 }  // namespace user_education
diff --git a/components/user_education/test/test_help_bubble.cc b/components/user_education/test/test_help_bubble.cc
index 2065e1fc..11349ca 100644
--- a/components/user_education/test/test_help_bubble.cc
+++ b/components/user_education/test/test_help_bubble.cc
@@ -4,7 +4,9 @@
 
 #include "components/user_education/test/test_help_bubble.h"
 
+#include "base/callback_list.h"
 #include "base/memory/weak_ptr.h"
+#include "ui/base/interaction/element_identifier.h"
 #include "ui/base/interaction/element_test_util.h"
 #include "ui/base/interaction/element_tracker.h"
 #include "ui/base/interaction/framework_specific_implementation.h"
@@ -17,9 +19,15 @@
 // static
 constexpr int TestHelpBubble::kNoButtonWithTextIndex;
 
-TestHelpBubble::TestHelpBubble(ui::ElementContext context,
+TestHelpBubble::TestHelpBubble(ui::TrackedElement* element,
                                HelpBubbleParams params)
-    : context_(context), params_(std::move(params)) {}
+    : element_(element), params_(std::move(params)) {
+  element_hidden_subscription_ =
+      ui::ElementTracker::GetElementTracker()->AddElementHiddenCallback(
+          element->identifier(), element->context(),
+          base::BindRepeating(&TestHelpBubble::OnElementHidden,
+                              base::Unretained(this)));
+}
 
 TestHelpBubble::~TestHelpBubble() {
   // Needs to be called here while we still have access to derived class
@@ -69,18 +77,29 @@
 }
 
 void TestHelpBubble::CloseBubbleImpl() {
-  context_ = ui::ElementContext();
+  element_ = nullptr;
+  element_hidden_subscription_ = base::CallbackListSubscription();
 }
 
 ui::ElementContext TestHelpBubble::GetContext() const {
-  return context_;
+  return element_ ? element_->context() : ui::ElementContext();
+}
+
+void TestHelpBubble::OnElementHidden(ui::TrackedElement* element) {
+  if (element == element_) {
+    if (is_open()) {
+      Close();
+    } else {
+      element_ = nullptr;
+      element_hidden_subscription_ = base::CallbackListSubscription();
+    }
+  }
 }
 
 std::unique_ptr<HelpBubble> TestHelpBubbleFactory::CreateBubble(
     ui::TrackedElement* element,
     HelpBubbleParams params) {
-  return std::make_unique<TestHelpBubble>(element->context(),
-                                          std::move(params));
+  return std::make_unique<TestHelpBubble>(element, std::move(params));
 }
 
 bool TestHelpBubbleFactory::CanBuildBubbleForTrackedElement(
diff --git a/components/user_education/test/test_help_bubble.h b/components/user_education/test/test_help_bubble.h
index b74e94e2..b73f7f2 100644
--- a/components/user_education/test/test_help_bubble.h
+++ b/components/user_education/test/test_help_bubble.h
@@ -8,10 +8,12 @@
 #include <memory>
 
 #include "base/auto_reset.h"
+#include "base/callback_list.h"
 #include "components/user_education/common/help_bubble.h"
 #include "components/user_education/common/help_bubble_factory.h"
 #include "components/user_education/common/help_bubble_params.h"
 #include "ui/base/interaction/element_identifier.h"
+#include "ui/base/interaction/element_tracker.h"
 
 namespace ui {
 class TrackedElement;
@@ -23,7 +25,7 @@
  public:
   static constexpr int kNoButtonWithTextIndex = -1;
 
-  TestHelpBubble(ui::ElementContext context, HelpBubbleParams params);
+  TestHelpBubble(ui::TrackedElement* element, HelpBubbleParams params);
   ~TestHelpBubble() override;
 
   DECLARE_FRAMEWORK_SPECIFIC_METADATA()
@@ -54,7 +56,10 @@
   ui::ElementContext GetContext() const override;
 
  private:
-  ui::ElementContext context_;
+  void OnElementHidden(ui::TrackedElement* element);
+
+  base::raw_ptr<ui::TrackedElement> element_;
+  base::CallbackListSubscription element_hidden_subscription_;
   HelpBubbleParams params_;
   int focus_count_ = 0;
 
diff --git a/components/user_education/webui/help_bubble_handler_unittest.cc b/components/user_education/webui/help_bubble_handler_unittest.cc
index c13f569..7b52c3f 100644
--- a/components/user_education/webui/help_bubble_handler_unittest.cc
+++ b/components/user_education/webui/help_bubble_handler_unittest.cc
@@ -431,8 +431,7 @@
   params.buttons.emplace_back(std::move(button_params));
 
   std::unique_ptr<HelpBubble> help_bubble =
-      std::make_unique<test::TestHelpBubble>(element->context(),
-                                             std::move(params));
+      std::make_unique<test::TestHelpBubble>(element, std::move(params));
 
   // Call the floating help bubble created method and ensure that the correct
   // message is sent over to the client.
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index f1cb16f..87055e4 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -2511,6 +2511,8 @@
       "devtools/protocol/native_input_event_builder_mac.mm",
       "font_access/font_enumeration_data_source_mac.h",
       "font_access/font_enumeration_data_source_mac.mm",
+      "gpu/browser_child_process_backgrounded_bridge.h",
+      "gpu/browser_child_process_backgrounded_bridge.mm",
       "gpu/ca_transaction_gpu_coordinator.cc",
       "gpu/ca_transaction_gpu_coordinator.h",
       "media/desktop_media_window_registry_mac.mm",
diff --git a/content/browser/accessibility/browser_accessibility_manager.h b/content/browser/accessibility/browser_accessibility_manager.h
index 316d4a53..253874d 100644
--- a/content/browser/accessibility/browser_accessibility_manager.h
+++ b/content/browser/accessibility/browser_accessibility_manager.h
@@ -257,10 +257,6 @@
   // Retrieve the bounds of the parent View in screen coordinates.
   gfx::Rect GetViewBoundsInScreenCoordinates() const;
 
-  // Fire an event telling native assistive technology to move focus to the
-  // given find in page result.
-  void ActivateFindInPageResult(int request_id, int match_index);
-
   // Called when the renderer process has notified us of tree changes. Returns
   // false in fatal-error conditions, in which case the caller should destroy
   // the manager.
diff --git a/content/browser/attribution_reporting/attribution_data_host_manager_impl.cc b/content/browser/attribution_reporting/attribution_data_host_manager_impl.cc
index 744bce4..a45ceb9 100644
--- a/content/browser/attribution_reporting/attribution_data_host_manager_impl.cc
+++ b/content/browser/attribution_reporting/attribution_data_host_manager_impl.cc
@@ -14,6 +14,7 @@
 #include "base/containers/flat_tree.h"
 #include "base/functional/bind.h"
 #include "base/functional/function_ref.h"
+#include "base/functional/overloaded.h"
 #include "base/metrics/field_trial_params.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/time/time.h"
@@ -210,61 +211,22 @@
   AttributionInputEvent input_event;
 };
 
-struct AttributionDataHostManagerImpl::NavigationRedirectSourceRegistrations {
-  // Source origin to use for all registrations on a redirect chain. Will not
-  // change over the course of the redirect chain.
+struct AttributionDataHostManagerImpl::SourceRegistrations {
+  // Source origin to use for all registrations on a navigation redirect or
+  // beacon chain. Will not change over the course of the chain.
   SuitableOrigin source_origin;
 
   // Number of source data we are waiting to be decoded/received.
   size_t pending_source_data = 0;
 
-  // True if navigation has completed, regardless of success or failure. If
-  // true, no further calls will be made to
-  // `NotifyNavigationRedirectRegistration()`.
-  bool navigation_complete = false;
-
-  // The time the first registration header was received for the redirect chain.
-  // Will not change over the course of the redirect chain.
-  base::TimeTicks register_time;
-
-  // Input event associated with the navigation.
-  AttributionInputEvent input_event;
-
-  // Will not change over the course of the redirect chain.
-  AttributionNavigationType nav_type;
-
-  // Whether the navigation is initiated within a fenced frame. Will not
-  // change over the course of the redirect chain.
-  bool is_within_fenced_frame;
-
-  GlobalRenderFrameHostId render_frame_id;
-};
-
-struct AttributionDataHostManagerImpl::BeaconSourceRegistrations {
-  // Source origin to use for all registrations on a beacon. Will not
-  // change over the course of the beacon.
-  SuitableOrigin source_origin;
-
-  // Number of source data we are waiting to be decoded/received.
-  size_t pending_source_data = 0;
-
-  // Navigation source data that has been received as part of this beacon.
-  // Navigation sources cannot be processed until `navigation_complete` is set
-  // to true.
-  std::vector<StorableSource> sources;
-
   // True if navigation has completed. `absl::nullopt` if it's an event beacon.
   absl::optional<bool> navigation_complete;
 
-  // True if the beacon has completed. If true, no further calls will be made to
-  // `NotifyFencedFrameReportingBeaconData()`.
-  bool beacon_complete = false;
-
-  // The time the beacon was sent. Will be null when the beacon was started but
-  // not actually sent.
+  // The time the first registration header was received. Will be null when the
+  // beacon was started but no data was received yet.
   base::TimeTicks register_time;
 
-  // Whether the beacon was initiated within a fenced frame.
+  // Whether the registration was initiated within a fenced frame.
   bool is_within_fenced_frame;
 
   // Input event associated with the navigation.
@@ -272,6 +234,66 @@
   AttributionInputEvent input_event;
 
   GlobalRenderFrameHostId render_frame_id;
+
+  struct NavigationRedirect {
+    blink::AttributionSrcToken attribution_src_token;
+
+    // Will not change over the course of the redirect chain.
+    AttributionNavigationType nav_type;
+  };
+
+  struct Beacon {
+    BeaconId id;
+
+    // Navigation source data that has been received as part of this beacon.
+    // Navigation sources cannot be processed until `navigation_complete` is set
+    // to true.
+    std::vector<StorableSource> sources;
+
+    // True if the beacon has completed. If true, no further calls will be made
+    // to `NotifyFencedFrameReportingBeaconData()`.
+    bool beacon_complete = false;
+  };
+
+  absl::variant<NavigationRedirect, Beacon> data;
+
+  bool operator<(const SourceRegistrations& other) const {
+    return Id() < other.Id();
+  }
+
+  Beacon& GetBeacon() {
+    DCHECK(absl::holds_alternative<Beacon>(data));
+    return absl::get<Beacon>(data);
+  }
+
+  NavigationRedirect& GetNavigationRedirect() {
+    DCHECK(absl::holds_alternative<NavigationRedirect>(data));
+    return absl::get<NavigationRedirect>(data);
+  }
+
+  friend bool operator<(const SourceRegistrations& a,
+                        const SourceRegistrationsId& b) {
+    return a.Id() < b;
+  }
+
+  friend bool operator<(const SourceRegistrationsId& a,
+                        const SourceRegistrations& b) {
+    return a < b.Id();
+  }
+
+ private:
+  SourceRegistrationsId Id() const {
+    return absl::visit(
+        base::Overloaded{
+            [](const NavigationRedirect& redirect) {
+              return SourceRegistrationsId(redirect.attribution_src_token);
+            },
+            [](const Beacon& beacon) {
+              return SourceRegistrationsId(beacon.id);
+            },
+        },
+        data);
+  }
 };
 
 AttributionDataHostManagerImpl::AttributionDataHostManagerImpl(
@@ -354,16 +376,20 @@
     return;
   }
 
-  auto [it, inserted] = redirect_registrations_.try_emplace(
-      attribution_src_token,
-      NavigationRedirectSourceRegistrations{
-          .source_origin = source_origin,
-          .register_time = base::TimeTicks::Now(),
-          .input_event = input_event,
-          .nav_type = nav_type,
-          .is_within_fenced_frame = is_within_fenced_frame,
-          .render_frame_id = render_frame_id});
-  DCHECK(!it->second.navigation_complete);
+  auto [it, inserted] = registrations_.insert(SourceRegistrations{
+      .source_origin = source_origin,
+      .navigation_complete = false,
+      .register_time = base::TimeTicks::Now(),
+      .is_within_fenced_frame = is_within_fenced_frame,
+      .input_event = std::move(input_event),
+      .render_frame_id = render_frame_id,
+      .data =
+          SourceRegistrations::NavigationRedirect{
+              .attribution_src_token = attribution_src_token,
+              .nav_type = nav_type,
+          },
+  });
+  DCHECK(it->navigation_complete == false);
 
   // Treat ongoing redirect registrations within a chain as a data host for the
   // purpose of trigger queuing.
@@ -371,13 +397,13 @@
     data_hosts_in_source_mode_++;
   }
 
-  it->second.pending_source_data++;
+  it->pending_source_data++;
 
   // Send the data to the decoder, but track that we are now waiting on a new
   // registration.
   data_decoder::DataDecoder::ParseJsonIsolated(
       header_value,
-      base::BindOnce(&AttributionDataHostManagerImpl::OnRedirectSourceParsed,
+      base::BindOnce(&AttributionDataHostManagerImpl::OnSourceParsed,
                      weak_factory_.GetWeakPtr(), attribution_src_token,
                      std::move(reporting_origin), header_value));
 }
@@ -403,22 +429,15 @@
     RecordNavigationDataHostStatus(NavigationDataHostStatus::kNotFound);
   }
 
-  auto redirect_it = redirect_registrations_.find(attribution_src_token);
-  if (redirect_it == redirect_registrations_.end()) {
+  auto redirect_it = registrations_.find(attribution_src_token);
+  if (redirect_it == registrations_.end()) {
     return;
   }
 
-  NavigationRedirectSourceRegistrations& registrations = redirect_it->second;
+  DCHECK(redirect_it->navigation_complete == false);
+  redirect_it->navigation_complete = true;
 
-  DCHECK(!registrations.navigation_complete);
-  registrations.navigation_complete = true;
-
-  if (registrations.pending_source_data == 0u) {
-    // We have finished processing all sources on this redirect chain, cleanup
-    // the map.
-    OnSourceEligibleDataHostFinished(registrations.register_time);
-    redirect_registrations_.erase(redirect_it);
-  }
+  MaybeOnRegistrationsFinished(attribution_src_token);
 }
 
 void AttributionDataHostManagerImpl::NotifyNavigationFailure(
@@ -436,26 +455,20 @@
 
     // We are not guaranteed to be processing redirect registrations for a given
     // navigation.
-    auto redirect_it = redirect_registrations_.find(*attribution_src_token);
-    if (redirect_it != redirect_registrations_.end()) {
-      NavigationRedirectSourceRegistrations& registrations =
-          redirect_it->second;
+    if (auto redirect_it = registrations_.find(*attribution_src_token);
+        redirect_it != registrations_.end()) {
+      DCHECK(redirect_it->navigation_complete == false);
+      redirect_it->navigation_complete = true;
 
-      DCHECK(!registrations.navigation_complete);
-      registrations.navigation_complete = true;
-
-      if (registrations.pending_source_data == 0u) {
-        OnSourceEligibleDataHostFinished(redirect_it->second.register_time);
-        redirect_registrations_.erase(redirect_it);
-      }
+      MaybeOnRegistrationsFinished(*attribution_src_token);
     }
   }
 
-  auto beacon_it =
-      beacon_registrations_.find(BeaconId(NavigationBeaconId(navigation_id)));
-  if (beacon_it != beacon_registrations_.end()) {
-    OnSourceEligibleDataHostFinished(beacon_it->second.register_time);
-    beacon_registrations_.erase(beacon_it);
+  if (auto beacon_it =
+          registrations_.find(BeaconId(NavigationBeaconId(navigation_id)));
+      beacon_it != registrations_.end()) {
+    OnSourceEligibleDataHostFinished(beacon_it->register_time);
+    registrations_.erase(beacon_it);
   }
 }
 
@@ -463,22 +476,22 @@
     int64_t navigation_id) {
   NavigationBeaconId beacon_id(navigation_id);
 
-  auto it = beacon_registrations_.find(BeaconId(beacon_id));
-  if (it == beacon_registrations_.end()) {
+  auto it = registrations_.find(BeaconId(beacon_id));
+  if (it == registrations_.end()) {
     return;
   }
 
-  BeaconSourceRegistrations& registrations = it->second;
-  DCHECK(registrations.navigation_complete == false);
-  registrations.navigation_complete = true;
+  DCHECK(it->navigation_complete == false);
+  it->navigation_complete = true;
 
-  for (StorableSource& source : registrations.sources) {
-    attribution_manager_->HandleSource(std::move(source),
-                                       registrations.render_frame_id);
+  std::vector<StorableSource>& sources = it->GetBeacon().sources;
+
+  for (StorableSource& source : sources) {
+    attribution_manager_->HandleSource(std::move(source), it->render_frame_id);
   }
-  registrations.sources.clear();
+  sources.clear();
 
-  MaybeOnBeaconRegistrationsFinished(beacon_id);
+  MaybeOnRegistrationsFinished(beacon_id);
 }
 
 const AttributionDataHostManagerImpl::ReceiverContext*
@@ -733,80 +746,40 @@
   delayed_triggers_.clear();
 }
 
-void AttributionDataHostManagerImpl::OnRedirectSourceParsed(
-    const blink::AttributionSrcToken& attribution_src_token,
-    const SuitableOrigin& reporting_origin,
-    const std::string& header_value,
-    data_decoder::DataDecoder::ValueOrError result) {
-  auto it = redirect_registrations_.find(attribution_src_token);
-
-  // The registration may no longer be tracked in the event the navigation
-  // failed.
-  if (it == redirect_registrations_.end()) {
-    return;
-  }
-
-  DCHECK_GT(it->second.pending_source_data, 0u);
-  NavigationRedirectSourceRegistrations& registrations = it->second;
-  registrations.pending_source_data--;
-
-  absl::optional<StorableSource> source =
-      ParseStorableSource(std::move(result), header_value, reporting_origin,
-                          registrations.source_origin, SourceType::kNavigation,
-                          registrations.is_within_fenced_frame);
-
-  if (source.has_value()) {
-    base::UmaHistogramEnumeration(
-        "Conversions.SourceRegistration.NavigationType.Foreground",
-        registrations.nav_type);
-    attribution_manager_->HandleSource(std::move(*source),
-                                       registrations.render_frame_id);
-  }
-
-  if (registrations.pending_source_data == 0u &&
-      registrations.navigation_complete) {
-    // We have finished processing all sources on this redirect chain, cleanup
-    // the map.
-    OnSourceEligibleDataHostFinished(registrations.register_time);
-    redirect_registrations_.erase(it);
-  }
-}
-
 void AttributionDataHostManagerImpl::NotifyFencedFrameReportingBeaconStarted(
     BeaconId beacon_id,
     SuitableOrigin source_origin,
     bool is_within_fenced_frame,
     AttributionInputEvent input_event,
     GlobalRenderFrameHostId render_frame_id) {
-  bool is_navigation = absl::holds_alternative<NavigationBeaconId>(beacon_id);
+  const bool is_navigation =
+      absl::holds_alternative<NavigationBeaconId>(beacon_id);
 
-  auto [it, inserted] = beacon_registrations_.try_emplace(
-      beacon_id, BeaconSourceRegistrations{
-                     .source_origin = std::move(source_origin),
-                     .is_within_fenced_frame = is_within_fenced_frame,
-                     .input_event = input_event,
-                     .render_frame_id = render_frame_id});
-
-  if (!inserted) {
-    return;
-  }
-
-  if (is_navigation) {
-    it->second.navigation_complete.emplace(false);
-  }
+  registrations_.insert(SourceRegistrations{
+      .source_origin = std::move(source_origin),
+      .navigation_complete =
+          is_navigation ? absl::make_optional(false) : absl::nullopt,
+      .is_within_fenced_frame = is_within_fenced_frame,
+      .input_event = std::move(input_event),
+      .render_frame_id = render_frame_id,
+      .data =
+          SourceRegistrations::Beacon{
+              .id = beacon_id,
+          },
+  });
 }
 
 void AttributionDataHostManagerImpl::NotifyFencedFrameReportingBeaconSent(
     BeaconId beacon_id) {
-  auto it = beacon_registrations_.find(beacon_id);
+  auto it = registrations_.find(beacon_id);
 
   // The registration may no longer be tracked in the event the navigation
   // failed.
-  if (it == beacon_registrations_.end()) {
+  if (it == registrations_.end()) {
     return;
   }
 
-  it->second.register_time = base::TimeTicks::Now();
+  it->register_time = base::TimeTicks::Now();
 
   // Treat ongoing beacon registrations as a data host for the purpose of
   // trigger queuing.
@@ -818,90 +791,70 @@
     url::Origin reporting_origin,
     const net::HttpResponseHeaders* headers,
     bool is_final_response) {
-  auto it = beacon_registrations_.find(beacon_id);
+  auto it = registrations_.find(beacon_id);
 
   // The registration may no longer be tracked in the event the navigation
   // failed.
-  if (it == beacon_registrations_.end()) {
+  if (it == registrations_.end()) {
     return;
   }
 
-  DCHECK(!it->second.beacon_complete);
-  it->second.beacon_complete = is_final_response;
+  SourceRegistrations::Beacon& data = it->GetBeacon();
+  DCHECK(!data.beacon_complete);
+  data.beacon_complete = is_final_response;
 
   absl::optional<SuitableOrigin> suitable_reporting_origin =
       SuitableOrigin::Create(std::move(reporting_origin));
   if (!suitable_reporting_origin) {
-    MaybeOnBeaconRegistrationsFinished(beacon_id);
+    MaybeOnRegistrationsFinished(beacon_id);
     return;
   }
 
   if (!headers) {
-    MaybeOnBeaconRegistrationsFinished(beacon_id);
+    MaybeOnRegistrationsFinished(beacon_id);
     return;
   }
 
   std::string source_header;
   if (!headers->GetNormalizedHeader(kAttributionReportingRegisterSourceHeader,
                                     &source_header)) {
-    MaybeOnBeaconRegistrationsFinished(beacon_id);
+    MaybeOnRegistrationsFinished(beacon_id);
     return;
   }
 
-  it->second.pending_source_data++;
+  it->pending_source_data++;
 
   data_decoder::DataDecoder::ParseJsonIsolated(
       source_header,
-      base::BindOnce(&AttributionDataHostManagerImpl::OnBeaconSourceParsed,
+      base::BindOnce(&AttributionDataHostManagerImpl::OnSourceParsed,
                      weak_factory_.GetWeakPtr(), beacon_id,
                      std::move(*suitable_reporting_origin), source_header));
 }
 
-void AttributionDataHostManagerImpl::OnBeaconSourceParsed(
-    BeaconId beacon_id,
+void AttributionDataHostManagerImpl::OnSourceParsed(
+    SourceRegistrationsId id,
     const SuitableOrigin& reporting_origin,
     const std::string& header_value,
     data_decoder::DataDecoder::ValueOrError result) {
-  auto it = beacon_registrations_.find(beacon_id);
+  auto it = registrations_.find(id);
 
   // The registration may no longer be tracked in the event the navigation
   // failed.
-  if (it == beacon_registrations_.end()) {
+  if (it == registrations_.end()) {
     return;
   }
 
-  BeaconSourceRegistrations& registrations = it->second;
+  SourceRegistrations& registrations = *it;
   DCHECK_GT(registrations.pending_source_data, 0u);
   registrations.pending_source_data--;
 
-  absl::optional<StorableSource> source =
-      ParseStorableSource(std::move(result), header_value, reporting_origin,
-                          registrations.source_origin,
-                          absl::holds_alternative<NavigationBeaconId>(beacon_id)
-                              ? SourceType::kNavigation
-                              : SourceType::kEvent,
-                          registrations.is_within_fenced_frame);
-
-  if (source.has_value()) {
-    if (registrations.navigation_complete.value_or(true)) {
-      attribution_manager_->HandleSource(std::move(*source),
-                                         registrations.render_frame_id);
-    } else {
-      registrations.sources.push_back(std::move(*source));
-    }
+  auto source_type = SourceType::kNavigation;
+  if (const auto* beacon =
+          absl::get_if<SourceRegistrations::Beacon>(&registrations.data);
+      beacon && absl::holds_alternative<EventBeaconId>(beacon->id)) {
+    source_type = SourceType::kEvent;
   }
 
-  MaybeOnBeaconRegistrationsFinished(beacon_id);
-}
-
-absl::optional<StorableSource>
-AttributionDataHostManagerImpl::ParseStorableSource(
-    data_decoder::DataDecoder::ValueOrError result,
-    const std::string& header_value,
-    const attribution_reporting::SuitableOrigin& reporting_origin,
-    const attribution_reporting::SuitableOrigin& source_origin,
-    SourceType source_type,
-    bool is_within_fenced_frame) {
   base::expected<StorableSource, SourceRegistrationError> source =
       base::unexpected(SourceRegistrationError::kInvalidJson);
   if (result.has_value()) {
@@ -910,8 +863,9 @@
           std::move(*result).TakeDict());
       if (registration.has_value()) {
         source.emplace(reporting_origin, std::move(*registration),
-                       /*source_time=*/base::Time::Now(), source_origin,
-                       source_type, is_within_fenced_frame);
+                       /*source_time=*/base::Time::Now(),
+                       registrations.source_origin, source_type,
+                       registrations.is_within_fenced_frame);
       } else {
         source = base::unexpected(registration.error());
       }
@@ -920,31 +874,54 @@
     }
   }
 
-  if (!source.has_value()) {
+  if (source.has_value()) {
+    absl::visit(
+        base::Overloaded{
+            [&](const SourceRegistrations::NavigationRedirect& redirect) {
+              base::UmaHistogramEnumeration(
+                  "Conversions.SourceRegistration.NavigationType.Foreground",
+                  redirect.nav_type);
+              attribution_manager_->HandleSource(std::move(*source),
+                                                 registrations.render_frame_id);
+            },
+            [&](SourceRegistrations::Beacon& beacon) {
+              if (registrations.navigation_complete.value_or(true)) {
+                attribution_manager_->HandleSource(
+                    std::move(*source), registrations.render_frame_id);
+              } else {
+                beacon.sources.push_back(std::move(*source));
+              }
+            },
+        },
+        registrations.data);
+  } else {
     attribution_manager_->NotifyFailedSourceRegistration(
-        header_value, source_origin, reporting_origin, source_type,
-        source.error());
+        header_value, registrations.source_origin, reporting_origin,
+        source_type, source.error());
     attribution_reporting::RecordSourceRegistrationError(source.error());
-    return absl::nullopt;
   }
 
-  return std::move(*source);
+  MaybeOnRegistrationsFinished(id);
 }
 
-void AttributionDataHostManagerImpl::MaybeOnBeaconRegistrationsFinished(
-    BeaconId beacon_id) {
-  auto it = beacon_registrations_.find(beacon_id);
-  if (it == beacon_registrations_.end()) {
+void AttributionDataHostManagerImpl::MaybeOnRegistrationsFinished(
+    SourceRegistrationsId id) {
+  auto it = registrations_.find(id);
+  if (it == registrations_.end()) {
     return;
   }
 
-  BeaconSourceRegistrations& registrations = it->second;
-  if (registrations.pending_source_data == 0u &&
-      registrations.navigation_complete.value_or(true) &&
-      registrations.beacon_complete) {
-    OnSourceEligibleDataHostFinished(registrations.register_time);
-    beacon_registrations_.erase(it);
+  if (it->pending_source_data > 0u || !it->navigation_complete.value_or(true)) {
+    return;
   }
+
+  if (const auto* beacon = absl::get_if<SourceRegistrations::Beacon>(&it->data);
+      beacon && !beacon->beacon_complete) {
+    return;
+  }
+
+  OnSourceEligibleDataHostFinished(it->register_time);
+  registrations_.erase(it);
 }
 
 }  // namespace content
diff --git a/content/browser/attribution_reporting/attribution_data_host_manager_impl.h b/content/browser/attribution_reporting/attribution_data_host_manager_impl.h
index 45ca2c0..226b153 100644
--- a/content/browser/attribution_reporting/attribution_data_host_manager_impl.h
+++ b/content/browser/attribution_reporting/attribution_data_host_manager_impl.h
@@ -12,6 +12,7 @@
 
 #include "base/containers/circular_deque.h"
 #include "base/containers/flat_map.h"
+#include "base/containers/flat_set.h"
 #include "base/functional/function_ref.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
@@ -25,13 +26,10 @@
 #include "mojo/public/cpp/bindings/receiver_set.h"
 #include "services/data_decoder/public/cpp/data_decoder.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
 #include "third_party/blink/public/common/tokens/tokens.h"
 #include "third_party/blink/public/mojom/conversions/attribution_data_host.mojom.h"
 
-#if BUILDFLAG(IS_ANDROID)
-#include "third_party/abseil-cpp/absl/types/variant.h"
-#endif
-
 namespace attribution_reporting {
 class SuitableOrigin;
 
@@ -48,7 +46,6 @@
 
 class AttributionManager;
 class AttributionTrigger;
-class StorableSource;
 
 struct GlobalRenderFrameHostId;
 
@@ -121,12 +118,11 @@
   struct NavigationDataHost;
 
   // Represents a set of attribution sources which registered in a top-level
-  // navigation redirect chain, and associated info to process them.
-  struct NavigationRedirectSourceRegistrations;
+  // navigation redirect or a beacon chain, and associated info to process them.
+  struct SourceRegistrations;
 
-  // Represents a set of attribution sources which registered in a beacon, and
-  // associated info to process them.
-  struct BeaconSourceRegistrations;
+  using SourceRegistrationsId =
+      absl::variant<blink::AttributionSrcToken, BeaconId>;
 
 #if BUILDFLAG(IS_ANDROID)
   struct OsTrigger;
@@ -152,27 +148,13 @@
   void OnReceiverDisconnected();
   void OnSourceEligibleDataHostFinished(base::TimeTicks register_time);
 
-  void OnRedirectSourceParsed(
-      const blink::AttributionSrcToken& attribution_src_token,
+  void OnSourceParsed(
+      SourceRegistrationsId,
       const attribution_reporting::SuitableOrigin& reporting_origin,
       const std::string& header_value,
       data_decoder::DataDecoder::ValueOrError result);
 
-  void OnBeaconSourceParsed(
-      BeaconId beacon_id,
-      const attribution_reporting::SuitableOrigin& reporting_origin,
-      const std::string& header_value,
-      data_decoder::DataDecoder::ValueOrError result);
-
-  absl::optional<StorableSource> ParseStorableSource(
-      data_decoder::DataDecoder::ValueOrError result,
-      const std::string& header_value,
-      const attribution_reporting::SuitableOrigin& reporting_origin,
-      const attribution_reporting::SuitableOrigin& source_origin,
-      attribution_reporting::mojom::SourceType,
-      bool is_within_fenced_frame);
-
-  void MaybeOnBeaconRegistrationsFinished(BeaconId beacon_id);
+  void MaybeOnRegistrationsFinished(SourceRegistrationsId);
 
   void HandleTrigger(TriggerPayload, GlobalRenderFrameHostId);
   void MaybeBufferTrigger(
@@ -193,14 +175,9 @@
   base::flat_map<blink::AttributionSrcToken, NavigationDataHost>
       navigation_data_host_map_;
 
-  // Stores registrations received for redirects within a navigation with a
-  // given token.
-  base::flat_map<blink::AttributionSrcToken,
-                 NavigationRedirectSourceRegistrations>
-      redirect_registrations_;
-
-  // Stores registrations received for a beacon.
-  base::flat_map<BeaconId, BeaconSourceRegistrations> beacon_registrations_;
+  // Stores registrations received for redirects within a navigation or a
+  // beacon.
+  base::flat_set<SourceRegistrations> registrations_;
 
   // The number of connected receivers that may register a source. Used to
   // determine whether to buffer triggers. Event receivers are counted here
diff --git a/content/browser/browser_child_process_host_impl.cc b/content/browser/browser_child_process_host_impl.cc
index 50f72fe..04a9950 100644
--- a/content/browser/browser_child_process_host_impl.cc
+++ b/content/browser/browser_child_process_host_impl.cc
@@ -369,6 +369,14 @@
                             PROCESS_TYPE_MAX);
 }
 
+#if !BUILDFLAG(IS_ANDROID)
+void BrowserChildProcessHostImpl::SetProcessBackgrounded(bool is_background) {
+  DCHECK(child_process_);
+  DCHECK(!child_process_->IsStarting());
+  child_process_->SetProcessBackgrounded(is_background);
+}
+#endif  // !BUILDFLAG(IS_ANDROID)
+
 #if BUILDFLAG(IS_ANDROID)
 void BrowserChildProcessHostImpl::EnableWarmUpConnection() {
   can_use_warm_up_connection_ = true;
diff --git a/content/browser/browser_child_process_host_impl.h b/content/browser/browser_child_process_host_impl.h
index 3ed6fa6..1dd30c9 100644
--- a/content/browser/browser_child_process_host_impl.h
+++ b/content/browser/browser_child_process_host_impl.h
@@ -128,6 +128,10 @@
 
   static void HistogramBadMessageTerminated(ProcessType process_type);
 
+#if !BUILDFLAG(IS_ANDROID)
+  void SetProcessBackgrounded(bool is_background);
+#endif  // !BUILDFLAG(IS_ANDROID)
+
 #if BUILDFLAG(IS_ANDROID)
   void EnableWarmUpConnection();
   void DumpProcessStack();
diff --git a/content/browser/devtools/devtools_http_handler_unittest.cc b/content/browser/devtools/devtools_http_handler_unittest.cc
index 27b8703..a48ec25 100644
--- a/content/browser/devtools/devtools_http_handler_unittest.cc
+++ b/content/browser/devtools/devtools_http_handler_unittest.cc
@@ -188,13 +188,7 @@
   content::BrowserTaskEnvironment task_environment_;
 };
 
-// TODO(https://crbug.com/1420865): Failing on iOS port.
-#if BUILDFLAG(IS_IOS)
-#define MAYBE_TestStartStop DISABLED_TestStartStop
-#else
-#define MAYBE_TestStartStop TestStartStop
-#endif
-TEST_F(DevToolsHttpHandlerTest, MAYBE_TestStartStop) {
+TEST_F(DevToolsHttpHandlerTest, TestStartStop) {
   base::RunLoop run_loop, run_loop_2;
   auto factory = std::make_unique<DummyServerSocketFactory>(
       run_loop.QuitClosure(), run_loop_2.QuitClosure());
@@ -208,13 +202,7 @@
   run_loop_2.Run();
 }
 
-// TODO(https://crbug.com/1420865): Failing on iOS port.
-#if BUILDFLAG(IS_IOS)
-#define MAYBE_TestServerSocketFailed DISABLED_TestServerSocketFailed
-#else
-#define MAYBE_TestServerSocketFailed TestServerSocketFailed
-#endif
-TEST_F(DevToolsHttpHandlerTest, MAYBE_TestServerSocketFailed) {
+TEST_F(DevToolsHttpHandlerTest, TestServerSocketFailed) {
   base::RunLoop run_loop, run_loop_2;
   auto factory = std::make_unique<FailingServerSocketFactory>(
       run_loop.QuitClosure(), run_loop_2.QuitClosure());
@@ -231,13 +219,7 @@
   run_loop_2.Run();
 }
 
-// TODO(https://crbug.com/1420865): Failing on iOS port.
-#if BUILDFLAG(IS_IOS)
-#define MAYBE_TestDevToolsActivePort DISABLED_TestDevToolsActivePort
-#else
-#define MAYBE_TestDevToolsActivePort TestDevToolsActivePort
-#endif
-TEST_F(DevToolsHttpHandlerTest, MAYBE_TestDevToolsActivePort) {
+TEST_F(DevToolsHttpHandlerTest, TestDevToolsActivePort) {
   base::RunLoop run_loop, run_loop_2;
   base::ScopedTempDir temp_dir;
   EXPECT_TRUE(temp_dir.CreateUniqueTempDir());
@@ -267,14 +249,7 @@
   EXPECT_EQ(static_cast<int>(kDummyPort), port);
 }
 
-// TODO(https://crbug.com/1420865): Failing on iOS port.
-#if BUILDFLAG(IS_IOS)
-#define MAYBE_MutatingActionsiRequireSafeVerb \
-  DISABLED_MutatingActionsiRequireSafeVerb
-#else
-#define MAYBE_MutatingActionsiRequireSafeVerb MutatingActionsiRequireSafeVerb
-#endif
-TEST_F(DevToolsHttpHandlerTest, MAYBE_MutatingActionsiRequireSafeVerb) {
+TEST_F(DevToolsHttpHandlerTest, MutatingActionsiRequireSafeVerb) {
   base::RunLoop run_loop, run_loop_2;
   auto* factory = new TCPServerSocketFactory(run_loop.QuitClosure(),
                                              run_loop_2.QuitClosure());
@@ -320,13 +295,7 @@
   run_loop_2.Run();
 }
 
-// TODO(https://crbug.com/1420865): Failing on iOS port.
-#if BUILDFLAG(IS_IOS)
-#define MAYBE_TestJsonNew DISABLED_TestJsonNew
-#else
-#define MAYBE_TestJsonNew TestJsonNew
-#endif
-TEST_F(DevToolsHttpHandlerTest, MAYBE_TestJsonNew) {
+TEST_F(DevToolsHttpHandlerTest, TestJsonNew) {
   base::RunLoop run_loop, run_loop_2;
   auto* factory = new TCPServerSocketFactory(run_loop.QuitClosure(),
                                              run_loop_2.QuitClosure());
@@ -443,16 +412,8 @@
   base::RunLoop run_loop_2_;
 };
 
-// TODO(https://crbug.com/1420865): Failing on iOS port.
-#if BUILDFLAG(IS_IOS)
-#define MAYBE_TestRejectsWebSocketConnectionsWithOrigin \
-  DISABLED_TestRejectsWebSocketConnectionsWithOrigin
-#else
-#define MAYBE_TestRejectsWebSocketConnectionsWithOrigin \
-  TestRejectsWebSocketConnectionsWithOrigin
-#endif
 TEST_F(DevToolsWebSocketHandlerTest,
-       MAYBE_TestRejectsWebSocketConnectionsWithOrigin) {
+       TestRejectsWebSocketConnectionsWithOrigin) {
   int port = StartServer();
 
   std::string debugging_url = GetWebSocketDebuggingURL(port);
@@ -477,16 +438,7 @@
   StopServer();
 }
 
-// TODO(https://crbug.com/1420865): Failing on iOS port.
-#if BUILDFLAG(IS_IOS)
-#define MAYBE_TestAllowsCLIOverrideAllowsOriginsStar \
-  DISABLED_TestAllowsCLIOverrideAllowsOriginsStar
-#else
-#define MAYBE_TestAllowsCLIOverrideAllowsOriginsStar \
-  TestAllowsCLIOverrideAllowsOriginsStar
-#endif
-TEST_F(DevToolsWebSocketHandlerTest,
-       MAYBE_TestAllowsCLIOverrideAllowsOriginsStar) {
+TEST_F(DevToolsWebSocketHandlerTest, TestAllowsCLIOverrideAllowsOriginsStar) {
   base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
       switches::kRemoteAllowOrigins, "*");
   int port = StartServer();
@@ -506,15 +458,7 @@
   StopServer();
 }
 
-// TODO(https://crbug.com/1420865): Failing on iOS port.
-#if BUILDFLAG(IS_IOS)
-#define MAYBE_TestAllowsCLIOverrideAllowsOrigins \
-  DISABLED_TestAllowsCLIOverrideAllowsOrigins
-#else
-#define MAYBE_TestAllowsCLIOverrideAllowsOrigins \
-  TestAllowsCLIOverrideAllowsOrigins
-#endif
-TEST_F(DevToolsWebSocketHandlerTest, MAYBE_TestAllowsCLIOverrideAllowsOrigins) {
+TEST_F(DevToolsWebSocketHandlerTest, TestAllowsCLIOverrideAllowsOrigins) {
   base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
       switches::kRemoteAllowOrigins, "http://localhost");
   int port = StartServer();
diff --git a/content/browser/devtools/protocol/fedcm_handler.cc b/content/browser/devtools/protocol/fedcm_handler.cc
index a2da8b8..aa35d3f 100644
--- a/content/browser/devtools/protocol/fedcm_handler.cc
+++ b/content/browser/devtools/protocol/fedcm_handler.cc
@@ -44,9 +44,32 @@
 
 void FedCmHandler::OnDialogShown() {
   DCHECK(frontend_);
-  if (enabled_) {
-    frontend_->DialogShown();
+  if (!enabled_) {
+    return;
   }
+
+  auto* auth_request = GetFederatedAuthRequest();
+  const auto* idp_data =
+      auth_request ? &auth_request->GetSortedIdpData() : nullptr;
+  DCHECK(idp_data);
+  DCHECK(!idp_data->empty());
+
+  auto accounts = std::make_unique<Array<FedCm::Account>>();
+  for (const auto& data : *idp_data) {
+    for (const IdentityRequestAccount& account : data.accounts) {
+      std::unique_ptr<FedCm::Account> entry =
+          FedCm::Account::Create()
+              .SetAccountId(account.id)
+              .SetEmail(account.email)
+              .SetName(account.name)
+              .SetGivenName(account.given_name)
+              .SetPictureUrl(account.picture.spec())
+              .SetIdpConfigUrl(data.idp_metadata.config_url.spec())
+              .Build();
+      accounts->push_back(std::move(entry));
+    }
+  }
+  frontend_->DialogShown(std::move(accounts));
 }
 
 FederatedAuthRequestPageData* FedCmHandler::GetPageData() {
@@ -57,4 +80,12 @@
   return PageUserData<FederatedAuthRequestPageData>::GetOrCreateForPage(page);
 }
 
+FederatedAuthRequestImpl* FedCmHandler::GetFederatedAuthRequest() {
+  FederatedAuthRequestPageData* page_data = GetPageData();
+  if (!page_data) {
+    return nullptr;
+  }
+  return page_data->PendingWebIdentityRequest();
+}
+
 }  // namespace content::protocol
diff --git a/content/browser/devtools/protocol/fedcm_handler.h b/content/browser/devtools/protocol/fedcm_handler.h
index 1c24e0a..da03173 100644
--- a/content/browser/devtools/protocol/fedcm_handler.h
+++ b/content/browser/devtools/protocol/fedcm_handler.h
@@ -13,6 +13,7 @@
 #include "content/common/content_export.h"
 
 namespace content {
+class FederatedAuthRequestImpl;
 class FederatedAuthRequestPageData;
 }  // namespace content
 
@@ -45,6 +46,7 @@
   DispatchResponse Disable() override;
 
   FederatedAuthRequestPageData* GetPageData();
+  FederatedAuthRequestImpl* GetFederatedAuthRequest();
 
   RenderFrameHostImpl* frame_host_ = nullptr;
   std::unique_ptr<FedCm::Frontend> frontend_;
diff --git a/content/browser/gpu/browser_child_process_backgrounded_bridge.h b/content/browser/gpu/browser_child_process_backgrounded_bridge.h
new file mode 100644
index 0000000..3bbf76be
--- /dev/null
+++ b/content/browser/gpu/browser_child_process_backgrounded_bridge.h
@@ -0,0 +1,55 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_GPU_BROWSER_CHILD_PROCESS_BACKGROUNDED_BRIDGE_H_
+#define CONTENT_BROWSER_GPU_BROWSER_CHILD_PROCESS_BACKGROUNDED_BRIDGE_H_
+
+#include <objc/objc.h>
+
+#include "base/memory/raw_ptr.h"
+#include "base/process/port_provider_mac.h"
+#include "base/scoped_observation.h"
+#include "content/common/content_export.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace content {
+
+class BrowserChildProcessHostImpl;
+
+// This class ensures that the backgrounded state of `process` mirrors the
+// backgrounded state of the browser process.
+class CONTENT_EXPORT BrowserChildProcessBackgroundedBridge
+    : public base::PortProvider::Observer {
+ public:
+  explicit BrowserChildProcessBackgroundedBridge(
+      BrowserChildProcessHostImpl* process);
+  ~BrowserChildProcessBackgroundedBridge() override;
+
+  void SimulateBrowserProcessForegroundedForTesting();
+  void SimulateBrowserProcessBackgroundedForTesting();
+
+  static void SetOSNotificationsEnabledForTesting(bool enabled);
+
+ private:
+  void Initialize();
+
+  void OnReceivedTaskPort(base::ProcessHandle process) override;
+
+  void OnBrowserProcessForegrounded();
+  void OnBrowserProcessBackgrounded();
+
+  raw_ptr<BrowserChildProcessHostImpl> process_;
+
+  base::ScopedObservation<base::PortProvider, base::PortProvider::Observer>
+      scoped_port_provider_observer_{this};
+
+  // Registration IDs for NSApplicationDidBecomeActiveNotification and
+  // NSApplicationDidResignActiveNotification.
+  id did_become_active_observer_ = nil;
+  id did_resign_active_observer_ = nil;
+};
+
+}  // namespace content
+
+#endif  // CONTENT_BROWSER_GPU_BROWSER_CHILD_PROCESS_BACKGROUNDED_BRIDGE_H_
diff --git a/content/browser/gpu/browser_child_process_backgrounded_bridge.mm b/content/browser/gpu/browser_child_process_backgrounded_bridge.mm
new file mode 100644
index 0000000..f4f3a52
--- /dev/null
+++ b/content/browser/gpu/browser_child_process_backgrounded_bridge.mm
@@ -0,0 +1,117 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/gpu/browser_child_process_backgrounded_bridge.h"
+
+#import <AppKit/AppKit.h>
+#import <Foundation/Foundation.h>
+
+#include "base/process/process.h"
+#include "content/browser/browser_child_process_host_impl.h"
+#include "content/public/browser/child_process_data.h"
+
+namespace content {
+
+namespace {
+
+bool g_notifications_enabled = true;
+
+}  // namespace
+
+BrowserChildProcessBackgroundedBridge::BrowserChildProcessBackgroundedBridge(
+    BrowserChildProcessHostImpl* process)
+    : process_(process) {
+  base::PortProvider* port_provider =
+      BrowserChildProcessHost::GetPortProvider();
+  if (port_provider->TaskForPid(process_->GetData().GetProcess().Pid()) !=
+      MACH_PORT_NULL) {
+    Initialize();
+  } else {
+    // The process has launched but the task port is not available yet. Defer
+    // initialization until it is.
+    scoped_port_provider_observer_.Observe(port_provider);
+  }
+}
+
+BrowserChildProcessBackgroundedBridge::
+    ~BrowserChildProcessBackgroundedBridge() {
+  if (did_become_active_observer_) {
+    [NSNotificationCenter.defaultCenter
+        removeObserver:did_become_active_observer_];
+  }
+  if (did_resign_active_observer_) {
+    [NSNotificationCenter.defaultCenter
+        removeObserver:did_resign_active_observer_];
+  }
+}
+
+void BrowserChildProcessBackgroundedBridge::
+    SimulateBrowserProcessForegroundedForTesting() {
+  OnBrowserProcessForegrounded();
+}
+
+void BrowserChildProcessBackgroundedBridge::
+    SimulateBrowserProcessBackgroundedForTesting() {
+  OnBrowserProcessBackgrounded();
+}
+
+// static
+void BrowserChildProcessBackgroundedBridge::SetOSNotificationsEnabledForTesting(
+    bool enabled) {
+  g_notifications_enabled = enabled;
+}
+
+void BrowserChildProcessBackgroundedBridge::Initialize() {
+  // Do the initial ajustment based on the initial value of the
+  // TASK_CATEGORY_POLICY role of the browser process.
+  base::SelfPortProvider self_port_provider;
+  process_->SetProcessBackgrounded(
+      base::Process::Current().IsProcessBackgrounded(&self_port_provider));
+
+  if (!g_notifications_enabled) {
+    return;
+  }
+
+  // Now subscribe to both NSApplicationDidBecomeActiveNotification and
+  // NSApplicationDidResignActiveNotification, which are sent when the browser
+  // process becomes foreground and background, respectively. The blocks
+  // implicity captures `this`. It is safe to do so since the subscriptions are
+  // removed in the destructor
+  did_become_active_observer_ = [NSNotificationCenter.defaultCenter
+      addObserverForName:NSApplicationDidBecomeActiveNotification
+                  object:nil
+                   queue:nil
+              usingBlock:^(NSNotification* notification) {
+                OnBrowserProcessForegrounded();
+              }];
+  did_resign_active_observer_ = [NSNotificationCenter.defaultCenter
+      addObserverForName:NSApplicationDidResignActiveNotification
+                  object:nil
+                   queue:nil
+              usingBlock:^(NSNotification* notification) {
+                OnBrowserProcessBackgrounded();
+              }];
+}
+
+void BrowserChildProcessBackgroundedBridge::OnReceivedTaskPort(
+    base::ProcessHandle process_handle) {
+  if (process_->GetData().GetProcess().Handle() != process_handle) {
+    return;
+  }
+
+  // Just received the task port for the target process. It is now possible to
+  // change its TASK_CATEGORY_POLICY.
+  scoped_port_provider_observer_.Reset();
+  Initialize();
+}
+
+void BrowserChildProcessBackgroundedBridge::OnBrowserProcessForegrounded() {
+  process_->SetProcessBackgrounded(false);
+}
+
+void BrowserChildProcessBackgroundedBridge::OnBrowserProcessBackgrounded() {
+  process_->SetProcessBackgrounded(true);
+}
+
+}  // namespace content
diff --git a/content/browser/gpu/browser_child_process_backgrounded_bridge_browsertest.mm b/content/browser/gpu/browser_child_process_backgrounded_bridge_browsertest.mm
new file mode 100644
index 0000000..554148d
--- /dev/null
+++ b/content/browser/gpu/browser_child_process_backgrounded_bridge_browsertest.mm
@@ -0,0 +1,156 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/gpu/browser_child_process_backgrounded_bridge.h"
+
+#include "base/process/process.h"
+#include "base/test/scoped_feature_list.h"
+#include "content/browser/gpu/gpu_process_host.h"
+#include "content/public/browser/browser_child_process_host.h"
+#include "content/public/browser/child_process_launcher_utils.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/content_browser_test.h"
+#include "gpu/config/gpu_finch_features.h"
+
+namespace content {
+
+bool IsProcessBackgrounded(base::ProcessId pid) {
+  base::Process process = base::Process::Open(pid);
+  if (process.is_current()) {
+    base::SelfPortProvider self_port_provider;
+    return process.IsProcessBackgrounded(&self_port_provider);
+  }
+
+  return process.IsProcessBackgrounded(
+      content::BrowserChildProcessHost::GetPortProvider());
+}
+
+void SetProcessBackgrounded(base::ProcessId pid, bool backgrounded) {
+  base::Process process = base::Process::Open(pid);
+  if (process.is_current()) {
+    base::SelfPortProvider self_port_provider;
+    process.SetProcessBackgrounded(&self_port_provider, backgrounded);
+    return;
+  }
+
+  process.SetProcessBackgrounded(
+      content::BrowserChildProcessHost::GetPortProvider(), backgrounded);
+}
+
+class BrowserChildProcessBackgroundedBridgeTest
+    : public content::ContentBrowserTest,
+      public base::PortProvider::Observer,
+      public testing::WithParamInterface<bool> {
+ public:
+  void SetUp() override {
+    scoped_feature_list_.InitAndEnableFeature(
+        features::kAdjustGpuProcessPriority);
+    content::BrowserChildProcessBackgroundedBridge::
+        SetOSNotificationsEnabledForTesting(false);
+    content::ContentBrowserTest::SetUp();
+  }
+
+  void TearDown() override {
+    content::ContentBrowserTest::TearDown();
+    content::BrowserChildProcessBackgroundedBridge::
+        SetOSNotificationsEnabledForTesting(true);
+  }
+
+  // Waits until the port for the GPU process is available.
+  void WaitForPort() {
+    auto* port_provider = content::BrowserChildProcessHost::GetPortProvider();
+    DCHECK(port_provider->TaskForPid(
+               content::GpuProcessHost::Get()->process_id()) == MACH_PORT_NULL);
+    port_provider->AddObserver(this);
+    base::RunLoop run_loop;
+
+    quit_closure_ = run_loop.QuitClosure();
+    run_loop.Run();
+  }
+
+  void EnsureBackgroundedStateChange() {
+    // Do a round-trip to the process launcher task to ensure any queued task is
+    // run.
+    base::RunLoop run_loop;
+    GetProcessLauncherTaskRunner()->PostTaskAndReply(
+        FROM_HERE, base::DoNothing(), run_loop.QuitClosure());
+    run_loop.Run();
+  }
+
+ private:
+  void OnReceivedTaskPort(base::ProcessHandle process_handle) override {
+    if (process_handle != content::GpuProcessHost::Get()->process_id()) {
+      return;
+    }
+
+    content::BrowserChildProcessHost::GetPortProvider()->RemoveObserver(this);
+    std::move(quit_closure_).Run();
+  }
+
+  base::test::ScopedFeatureList scoped_feature_list_;
+
+  base::OnceClosure quit_closure_;
+};
+
+IN_PROC_BROWSER_TEST_F(BrowserChildProcessBackgroundedBridgeTest,
+                       InitiallyForegrounded) {
+  // Set the browser process as foregrounded.
+  SetProcessBackgrounded(base::Process::Current().Pid(), false);
+
+  // Wait until we receive the port for the GPU process.
+  WaitForPort();
+
+  // Ensure that the initial backgrounded state changed.
+  EnsureBackgroundedStateChange();
+
+  auto* gpu_process_host = content::GpuProcessHost::Get();
+  EXPECT_TRUE(gpu_process_host);
+  EXPECT_FALSE(IsProcessBackgrounded(gpu_process_host->process_id()));
+}
+
+IN_PROC_BROWSER_TEST_F(BrowserChildProcessBackgroundedBridgeTest,
+                       InitiallyBackgrounded) {
+  // Set the browser process as backgrounded.
+  SetProcessBackgrounded(base::Process::Current().Pid(), true);
+
+  // Wait until we receive the port for the GPU process.
+  WaitForPort();
+
+  // Ensure that the initial backgrounded state changed.
+  EnsureBackgroundedStateChange();
+
+  auto* gpu_process_host = content::GpuProcessHost::Get();
+  EXPECT_TRUE(gpu_process_host);
+  EXPECT_TRUE(IsProcessBackgrounded(gpu_process_host->process_id()));
+}
+
+IN_PROC_BROWSER_TEST_F(BrowserChildProcessBackgroundedBridgeTest,
+                       OnBackgroundedStateChanged) {
+  // Wait until we receive the port for the GPU process.
+  WaitForPort();
+
+  auto* gpu_process_host = content::GpuProcessHost::Get();
+  EXPECT_TRUE(gpu_process_host);
+
+  auto* bridge =
+      gpu_process_host->browser_child_process_backgrounded_bridge_for_testing();
+  ASSERT_TRUE(bridge);
+
+  bridge->SimulateBrowserProcessForegroundedForTesting();
+  EnsureBackgroundedStateChange();
+
+  EXPECT_FALSE(IsProcessBackgrounded(gpu_process_host->process_id()));
+
+  bridge->SimulateBrowserProcessBackgroundedForTesting();
+  EnsureBackgroundedStateChange();
+
+  EXPECT_TRUE(IsProcessBackgrounded(gpu_process_host->process_id()));
+
+  bridge->SimulateBrowserProcessForegroundedForTesting();
+  EnsureBackgroundedStateChange();
+
+  EXPECT_FALSE(IsProcessBackgrounded(gpu_process_host->process_id()));
+}
+
+}  // namespace content
\ No newline at end of file
diff --git a/content/browser/gpu/gpu_process_host.cc b/content/browser/gpu/gpu_process_host.cc
index 04f216e4..c674795c 100644
--- a/content/browser/gpu/gpu_process_host.cc
+++ b/content/browser/gpu/gpu_process_host.cc
@@ -112,6 +112,7 @@
 #endif
 
 #if BUILDFLAG(IS_MAC)
+#include "content/browser/gpu/browser_child_process_backgrounded_bridge.h"
 #include "content/browser/gpu/ca_transaction_gpu_coordinator.h"
 #endif
 
@@ -928,6 +929,14 @@
     process_id_ = process_->GetProcess().Pid();
     DCHECK_NE(base::kNullProcessId, process_id_);
     gpu_host_->SetProcessId(process_id_);
+
+#if BUILDFLAG(IS_MAC)
+    if (base::FeatureList::IsEnabled(features::kAdjustGpuProcessPriority)) {
+      browser_child_process_backgrounded_bridge_ =
+          std::make_unique<BrowserChildProcessBackgroundedBridge>(
+              process_.get());
+    }
+#endif
   }
 }
 
diff --git a/content/browser/gpu/gpu_process_host.h b/content/browser/gpu/gpu_process_host.h
index 5e17fdf0..d395951 100644
--- a/content/browser/gpu/gpu_process_host.h
+++ b/content/browser/gpu/gpu_process_host.h
@@ -51,6 +51,7 @@
 class BrowserChildProcessHostImpl;
 
 #if BUILDFLAG(IS_MAC)
+class BrowserChildProcessBackgroundedBridge;
 class CATransactionGPUCoordinator;
 #endif
 
@@ -128,6 +129,13 @@
 
   viz::GpuHostImpl* gpu_host() { return gpu_host_.get(); }
 
+#if BUILDFLAG(IS_MAC)
+  BrowserChildProcessBackgroundedBridge*
+  browser_child_process_backgrounded_bridge_for_testing() {
+    return browser_child_process_backgrounded_bridge_.get();
+  }
+#endif
+
  private:
   enum class GpuTerminationOrigin {
     kUnknownOrigin = 0,
@@ -254,6 +262,11 @@
 
 #if BUILDFLAG(IS_MAC)
   scoped_refptr<CATransactionGPUCoordinator> ca_transaction_gpu_coordinator_;
+
+  // Ensures the backgrounded state of the GPU process mirrors the backgrounded
+  // state of the browser process.
+  std::unique_ptr<BrowserChildProcessBackgroundedBridge>
+      browser_child_process_backgrounded_bridge_;
 #endif
 
   // Track the URLs of the pages which have live offscreen contexts,
diff --git a/content/browser/renderer_host/input/touch_input_browsertest.cc b/content/browser/renderer_host/input/touch_input_browsertest.cc
index d1232354..44ad4f5 100644
--- a/content/browser/renderer_host/input/touch_input_browsertest.cc
+++ b/content/browser/renderer_host/input/touch_input_browsertest.cc
@@ -143,11 +143,19 @@
             filter->WaitForAck());
 
   // The same is true for release because there is no touch-end handler.
+  //
+  // TODO(https://crbug.com/1417126): a suspicious flake in AR/XR tests forced
+  // us to disable `kDroppedTouchSequenceIncludesTouchEnd` then override the
+  // expectation here!
   filter = AddFilter(WebInputEvent::Type::kTouchEnd);
   touch.ReleasePoint(0);
   SendTouchEvent(&touch);
-  EXPECT_EQ(blink::mojom::InputEventResultState::kNoConsumerExists,
-            filter->WaitForAck());
+  blink::mojom::InputEventResultState expected_touchend_result =
+      base::FeatureList::IsEnabled(
+          blink::features::kDroppedTouchSequenceIncludesTouchEnd)
+          ? blink::mojom::InputEventResultState::kNoConsumerExists
+          : blink::mojom::InputEventResultState::kNotConsumed;
+  EXPECT_EQ(expected_touchend_result, filter->WaitForAck());
 }
 
 IN_PROC_BROWSER_TEST_F(TouchInputBrowserTest, TouchStartNoConsume) {
diff --git a/content/browser/renderer_host/input/touch_selection_controller_client_aura.cc b/content/browser/renderer_host/input/touch_selection_controller_client_aura.cc
index 5348a1fe..54ee4dd 100644
--- a/content/browser/renderer_host/input/touch_selection_controller_client_aura.cc
+++ b/content/browser/renderer_host/input/touch_selection_controller_client_aura.cc
@@ -22,7 +22,6 @@
 #include "ui/base/clipboard/clipboard.h"
 #include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
 #include "ui/base/pointer/touch_editing_controller.h"
-#include "ui/base/ui_base_features.h"
 #include "ui/events/event_observer.h"
 #include "ui/gfx/geometry/point_conversions.h"
 #include "ui/gfx/geometry/size_conversions.h"
diff --git a/content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc b/content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc
index 178c4ff..2944a24 100644
--- a/content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc
+++ b/content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc
@@ -12,9 +12,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
 #include "base/task/single_thread_task_runner.h"
-#include "base/test/scoped_feature_list.h"
 #include "base/test/test_timeouts.h"
-#include "build/chromeos_buildflags.h"
 #include "content/browser/renderer_host/render_widget_host_view_aura.h"
 #include "content/browser/renderer_host/render_widget_host_view_child_frame.h"
 #include "content/browser/renderer_host/render_widget_host_view_event_handler.h"
@@ -35,7 +33,6 @@
 #include "ui/aura/window.h"
 #include "ui/aura/window_tree_host.h"
 #include "ui/base/pointer/touch_editing_controller.h"
-#include "ui/base/ui_base_features.h"
 #include "ui/display/display_switches.h"
 #include "ui/events/event_sink.h"
 #include "ui/events/event_utils.h"
@@ -176,12 +173,7 @@
 
 class TouchSelectionControllerClientAuraTest : public ContentBrowserTest {
  public:
-  TouchSelectionControllerClientAuraTest() {
-#if BUILDFLAG(IS_CHROMEOS)
-    scoped_feature_list_.InitAndEnableFeature(
-        features::kTouchTextEditingRedesign);
-#endif  // BUILDFLAG(IS_CHROMEOS)
-  }
+  TouchSelectionControllerClientAuraTest() = default;
 
   TouchSelectionControllerClientAuraTest(
       const TouchSelectionControllerClientAuraTest&) = delete;
@@ -261,8 +253,6 @@
 
   raw_ptr<TestTouchSelectionControllerClientAura> selection_controller_client_ =
       nullptr;
-
-  base::test::ScopedFeatureList scoped_feature_list_;
 };
 
 class TouchSelectionControllerClientAuraCAPFeatureTest
@@ -948,7 +938,6 @@
             rwhva->selection_controller()->GetVisibleRectBetweenBounds());
 }
 
-#if BUILDFLAG(IS_CHROMEOS)
 // Tests that the select all command in the quick menu works correctly and that
 // the touch handles and quick menu are shown after the command is executed.
 IN_PROC_BROWSER_TEST_P(TouchSelectionControllerClientAuraCAPFeatureTest,
@@ -1042,7 +1031,6 @@
       selection_controller_client()->GetActiveMenuClient()->GetSelectedText(),
       u"Text");
 }
-#endif
 
 class TouchSelectionControllerClientAuraScaleFactorTest
     : public TouchSelectionControllerClientAuraTest {
diff --git a/content/browser/renderer_host/render_widget_host_unittest.cc b/content/browser/renderer_host/render_widget_host_unittest.cc
index d701c92..ebf9539 100644
--- a/content/browser/renderer_host/render_widget_host_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_unittest.cc
@@ -90,7 +90,7 @@
 #include "content/browser/renderer_host/test_render_widget_host_view_ios_factory.h"
 #endif
 
-#if defined(USE_AURA) || BUILDFLAG(IS_MAC)
+#if defined(USE_AURA) || BUILDFLAG(IS_APPLE)
 #include "content/browser/compositor/test/test_image_transport_factory.h"
 #endif
 
@@ -566,7 +566,7 @@
     site_instance_group_ = base::WrapRefCounted(new SiteInstanceGroup(
         SiteInstanceImpl::NextBrowsingInstanceId(), process_.get()));
     sink_ = &process_->sink();
-#if defined(USE_AURA) || BUILDFLAG(IS_MAC)
+#if defined(USE_AURA) || BUILDFLAG(IS_APPLE)
     ImageTransportFactory::SetFactory(
         std::make_unique<TestImageTransportFactory>());
 #endif
@@ -637,14 +637,13 @@
     site_instance_group_.reset();
     process_.reset();
     browser_context_.reset();
-#if defined(USE_AURA) || BUILDFLAG(IS_MAC)
+#if defined(USE_AURA) || BUILDFLAG(IS_APPLE)
     ImageTransportFactory::Terminate();
+#endif
+#if defined(USE_AURA) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID)
     display::Screen::SetScreenInstance(nullptr);
     screen_.reset();
 #endif
-#if BUILDFLAG(IS_ANDROID)
-    display::Screen::SetScreenInstance(nullptr);
-#endif
 
     // Process all pending tasks to avoid leaks.
     base::RunLoop().RunUntilIdle();
diff --git a/content/browser/renderer_host/render_widget_host_view_aura.cc b/content/browser/renderer_host/render_widget_host_view_aura.cc
index 4cd6bcb..abf1c83d 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura.cc
@@ -2627,12 +2627,8 @@
       ui::GestureConfiguration::GetInstance()->long_press_time_in_ms());
   tsc_config.tap_slop = ui::GestureConfiguration::GetInstance()
                             ->max_touch_move_in_pixels_for_click();
-#if BUILDFLAG(IS_CHROMEOS)
   tsc_config.enable_longpress_drag_selection =
       features::IsTouchTextEditingRedesignEnabled();
-#else
-  tsc_config.enable_longpress_drag_selection = false;
-#endif
   selection_controller_ = std::make_unique<ui::TouchSelectionController>(
       selection_controller_client_.get(), tsc_config);
 }
diff --git a/content/browser/webid/federated_auth_request_impl.cc b/content/browser/webid/federated_auth_request_impl.cc
index d2ec41d..5b85abe 100644
--- a/content/browser/webid/federated_auth_request_impl.cc
+++ b/content/browser/webid/federated_auth_request_impl.cc
@@ -946,13 +946,14 @@
     }
   }
 
+  DCHECK(idp_data_for_display_.empty());
+
   // TODO(crbug.com/1383384): Handle auto_reauthn for multi IDP.
   bool idp_enabled_auto_reauthn = true;
-  std::vector<IdentityProviderData> idp_data_for_display;
   for (const auto& idp : idp_order_) {
     auto idp_info_it = idp_infos_.find(idp);
     if (idp_info_it != idp_infos_.end() && idp_info_it->second->data) {
-      idp_data_for_display.push_back(*idp_info_it->second->data);
+      idp_data_for_display_.push_back(*idp_info_it->second->data);
       idp_enabled_auto_reauthn &= idp_info_it->second->auto_reauthn;
     }
   }
@@ -1004,7 +1005,7 @@
     IdentityRequestAccount account{*auto_reauthn_account};
     IdentityProviderData idp{*auto_reauthn_idp};
     idp.accounts = {account};
-    idp_data_for_display = {idp};
+    idp_data_for_display_ = {idp};
   }
 
   // TODO(crbug.com/1408520): opt-out affordance is not included in the origin
@@ -1030,7 +1031,7 @@
   // IDPs are failing in the multi IDP case.
   request_dialog_controller_->ShowAccountsDialog(
       WebContents::FromRenderFrameHost(&render_frame_host()),
-      top_frame_url_for_display, iframe_url_for_display, idp_data_for_display,
+      top_frame_url_for_display, iframe_url_for_display, idp_data_for_display_,
       auto_reauthn ? SignInMode::kAuto : SignInMode::kExplicit,
       show_auto_reauthn_checkbox,
       base::BindOnce(&FederatedAuthRequestImpl::OnAccountSelected,
@@ -1580,6 +1581,7 @@
   select_account_time_ = base::TimeTicks();
   token_response_time_ = base::TimeTicks();
   idp_infos_.clear();
+  idp_data_for_display_.clear();
   fetch_data_ = FetchData();
   idp_order_.clear();
   metrics_endpoints_.clear();
diff --git a/content/browser/webid/federated_auth_request_impl.h b/content/browser/webid/federated_auth_request_impl.h
index 7b664f6a..07e8336 100644
--- a/content/browser/webid/federated_auth_request_impl.h
+++ b/content/browser/webid/federated_auth_request_impl.h
@@ -87,11 +87,6 @@
   // Rejects the pending request if it has not been resolved naturally yet.
   void OnRejectRequest();
 
-  // For use by the devtools protocol for browser automation.
-  IdentityRequestDialogController* GetDialogController() {
-    return request_dialog_controller_.get();
-  }
-
   struct IdentityProviderGetInfo {
     IdentityProviderGetInfo(blink::mojom::IdentityProviderConfigPtr,
                             bool auto_reauthn,
@@ -123,6 +118,15 @@
     absl::optional<IdentityProviderData> data;
   };
 
+  // For use by the devtools protocol for browser automation.
+  IdentityRequestDialogController* GetDialogController() {
+    return request_dialog_controller_.get();
+  }
+
+  const std::vector<IdentityProviderData>& GetSortedIdpData() const {
+    return idp_data_for_display_;
+  }
+
  private:
   friend class FederatedAuthRequestImplTest;
 
@@ -293,6 +297,7 @@
 
   // Populated by MaybeShowAccountsDialog().
   base::flat_map<GURL, std::unique_ptr<IdentityProviderInfo>> idp_infos_;
+  std::vector<IdentityProviderData> idp_data_for_display_;
 
   raw_ptr<FederatedIdentityApiPermissionContextDelegate>
       api_permission_delegate_ = nullptr;
diff --git a/content/public/renderer/render_thread.cc b/content/public/renderer/render_thread.cc
index b34be7e..e43b7ed1 100644
--- a/content/public/renderer/render_thread.cc
+++ b/content/public/renderer/render_thread.cc
@@ -6,7 +6,7 @@
 
 #include "base/no_destructor.h"
 #include "base/threading/thread_checker_impl.h"
-#include "base/threading/thread_local.h"
+#include "third_party/abseil-cpp/absl/base/attributes.h"
 
 namespace content {
 
@@ -14,10 +14,7 @@
 
 // Keep the global RenderThread in a TLS slot so it is impossible to access
 // incorrectly from the wrong thread.
-base::ThreadLocalPointer<RenderThread>& GetRenderThreadLocalPointer() {
-  static base::NoDestructor<base::ThreadLocalPointer<RenderThread>> tls;
-  return *tls;
-}
+ABSL_CONST_INIT thread_local RenderThread* render_thread = nullptr;
 
 static const base::ThreadCheckerImpl& GetThreadChecker() {
   static base::NoDestructor<base::ThreadCheckerImpl> checker;
@@ -27,7 +24,7 @@
 }  // namespace
 
 RenderThread* RenderThread::Get() {
-  return GetRenderThreadLocalPointer().Get();
+  return render_thread;
 }
 
 bool RenderThread::IsMainThread() {
@@ -35,12 +32,8 @@
   return GetThreadChecker().CalledOnValidThread();
 }
 
-RenderThread::RenderThread() {
-  GetRenderThreadLocalPointer().Set(this);
-}
+RenderThread::RenderThread() : resetter_(&render_thread, this) {}
 
-RenderThread::~RenderThread() {
-  GetRenderThreadLocalPointer().Set(nullptr);
-}
+RenderThread::~RenderThread() = default;
 
 }  // namespace content
diff --git a/content/public/renderer/render_thread.h b/content/public/renderer/render_thread.h
index 254a8884..c4373348 100644
--- a/content/public/renderer/render_thread.h
+++ b/content/public/renderer/render_thread.h
@@ -9,6 +9,7 @@
 #include <stdint.h>
 #include <memory>
 
+#include "base/auto_reset.h"
 #include "base/functional/callback_forward.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/task/single_thread_task_runner.h"
@@ -117,6 +118,9 @@
   // https://github.com/WICG/attribution-reporting-api/blob/main/app_to_web.md.
   virtual attribution_reporting::mojom::OsSupport
   GetOsSupportForAttributionReporting() = 0;
+
+ private:
+  const base::AutoReset<RenderThread*> resetter_;
 };
 
 }  // namespace content
diff --git a/content/public/test/network_service_test_helper.cc b/content/public/test/network_service_test_helper.cc
index 215dc4fb6..9e0c1c8 100644
--- a/content/public/test/network_service_test_helper.cc
+++ b/content/public/test/network_service_test_helper.cc
@@ -19,6 +19,7 @@
 #include "base/task/current_thread.h"
 #include "base/task/task_traits.h"
 #include "base/task/thread_pool.h"
+#include "base/threading/thread_restrictions.h"
 #include "build/build_config.h"
 #include "content/public/common/content_features.h"
 #include "content/public/common/content_switches.h"
diff --git a/content/public/utility/utility_thread.cc b/content/public/utility/utility_thread.cc
index cc26ab9..881ff6f 100644
--- a/content/public/utility/utility_thread.cc
+++ b/content/public/utility/utility_thread.cc
@@ -4,27 +4,25 @@
 
 #include "content/public/utility/utility_thread.h"
 
-#include "base/lazy_instance.h"
-#include "base/threading/thread_local.h"
+#include "third_party/abseil-cpp/absl/base/attributes.h"
 
 namespace content {
 
+namespace {
+
 // Keep the global UtilityThread in a TLS slot so it is impossible to access
 // incorrectly from the wrong thread.
-static base::LazyInstance<base::ThreadLocalPointer<UtilityThread>>::Leaky
-    lazy_tls = LAZY_INSTANCE_INITIALIZER;
+ABSL_CONST_INIT thread_local UtilityThread* utility_thread = nullptr;
+
+}  // namespace
 
 UtilityThread* UtilityThread::Get() {
-  return lazy_tls.Pointer()->Get();
+  return utility_thread;
 }
 
-UtilityThread::UtilityThread() {
-  lazy_tls.Pointer()->Set(this);
-}
+UtilityThread::UtilityThread() : resetter_(&utility_thread, this) {}
 
-UtilityThread::~UtilityThread() {
-  lazy_tls.Pointer()->Set(nullptr);
-}
+UtilityThread::~UtilityThread() = default;
 
 }  // namespace content
 
diff --git a/content/public/utility/utility_thread.h b/content/public/utility/utility_thread.h
index 491cd08..9d0934b 100644
--- a/content/public/utility/utility_thread.h
+++ b/content/public/utility/utility_thread.h
@@ -5,6 +5,7 @@
 #ifndef CONTENT_PUBLIC_UTILITY_UTILITY_THREAD_H_
 #define CONTENT_PUBLIC_UTILITY_UTILITY_THREAD_H_
 
+#include "base/auto_reset.h"
 #include "build/build_config.h"
 #include "content/common/content_export.h"
 #include "content/public/child/child_thread.h"
@@ -30,6 +31,9 @@
   // Initializes blink with web sandbox support.
   virtual void EnsureBlinkInitializedWithSandboxSupport() = 0;
 #endif
+
+ private:
+  const base::AutoReset<UtilityThread*> resetter_;
 };
 
 }  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index e1357e0..540caba0 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -1967,6 +1967,7 @@
       "../browser/accessibility/hit_testing_mac_browsertest.mm",
       "../browser/form_controls_browsertest_mac.h",
       "../browser/form_controls_browsertest_mac.mm",
+      "../browser/gpu/browser_child_process_backgrounded_bridge_browsertest.mm",
       "../browser/keyboard_lock_browsertest_mac.mm",
       "../browser/pointer_lock_browsertest_mac.mm",
       "../browser/renderer_host/render_frame_host_impl_mac_browsertest.mm",
diff --git a/content/test/data/browsing_data/media_license.js b/content/test/data/browsing_data/media_license.js
index 535bec3a..80d3cd7 100644
--- a/content/test/data/browsing_data/media_license.js
+++ b/content/test/data/browsing_data/media_license.js
@@ -37,32 +37,37 @@
       '{"keys":[{"kty":"oct","k":"tQ0bJVWb6b0KPL6KtZIy_A","kid":"LwVHf8JLtPrv2GUXFW2v_A"}],"type":"persistent-license"}');
 
   savedSessionId = session.sessionId;
-  session.update(license).then(success_, failure_);
+  return session.update(license).then(() => true, () => false);
 }
 
-function setMediaLicense() {
+async function setMediaLicense() {
   var te = new TextEncoder();
   var initData = te.encode('{"kids":["LwVHf8JLtPrv2GUXFW2v_A"]}');
 
-  createPersistentSession()
-      .then(function(session) {
-        // generateRequest() will trigger a 'message' event, which we need to
-        // wait for in order to call update() which provides the license.
-        session.addEventListener('message', handleMessageEvent, false);
-        return session.generateRequest('keyids', initData);
-      })
-      // Success is reported from handleMessageEvent().
-      .catch(failure_);
+  try {
+    const session = await createPersistentSession();
+    // generateRequest() will trigger a 'message' event, which we need to
+    // wait for in order to call update() which provides the license.
+    const handled = new Promise((resolve, reject) => {
+      session.addEventListener('message', (e) => {
+        handleMessageEvent(e).then(resolve, reject)
+      }, false);
+    });
+    await session.generateRequest('keyids', initData);
+    await handled;
+    return true;
+  } catch {
+    return false;
+  }
 }
 
-function hasMediaLicense() {
-  createPersistentSession()
-      .then(function(session) {
-        return session.load(savedSessionId);
-      })
-      .then(function(result) {
-        // |result| is a boolean, indicating if the session was loaded or not.
-        domAutomationController.send(result);
-      })
-      .catch(failure_);
+async function hasMediaLicense() {
+  try {
+    const session = await createPersistentSession();
+    const result = await session.load(savedSessionId);
+    // |result| is a boolean, indicating if the session was loaded or not.
+    return result;
+  }catch {
+    return false;
+  }
 }
\ No newline at end of file
diff --git a/content/test/data/browsing_data/site_data.html b/content/test/data/browsing_data/site_data.html
index a1a15b1..33c77051 100644
--- a/content/test/data/browsing_data/site_data.html
+++ b/content/test/data/browsing_data/site_data.html
@@ -2,14 +2,6 @@
 <script src="media_license.js"></script>
 <script>
 
-  function success_() {
-    domAutomationController.send(true);
-  }
-
-  function failure_() {
-    domAutomationController.send(false);
-  }
-
   function isHttps_() {
     return location.protocol === 'https:';
   }
@@ -17,108 +9,158 @@
   function setCookie() {
     const samesite_none_secure = '; SameSite=None; Secure';
     document.cookie = 'foo=bar; Max-Age=1000' + (isHttps_() ? samesite_none_secure : '');
-    success_();
+    return true;
+  }
+  function setCookieAsync() {
+    // Provided for Java.
+    domAutomationController.send(setCookie());
   }
 
   function hasCookie() {
-    domAutomationController.send(document.cookie.indexOf('foo=bar') != -1);
+    return document.cookie.indexOf('foo=bar') != -1;
+  }
+  function hasCookieAsync() {
+    // Provided for Java.
+    domAutomationController.send(hasCookie());
   }
 
   function setSessionCookie() {
     const samesite_none_secure = '; SameSite=None; Secure';
     document.cookie = 'bar=session' + (isHttps_() ? samesite_none_secure : '');
-    success_();
+    return true;
   }
 
   function hasSessionCookie() {
-    domAutomationController.send(document.cookie == 'bar=session');
+    return document.cookie == 'bar=session';
   }
 
   function setCookieStore() {
-    cookieStore.set({
+    return cookieStore.set({
       name: 'cookie-name', value: 'cookie-value', sameSite: 'none'
-    }).then(success_).catch(failure_);
+    })
+    .then(() => true)
+    .catch(() => false);
   }
 
   function hasCookieStore() {
-    cookieStore.get('cookie-name').then(function (cookie) {
-      domAutomationController.send(cookie.value == 'cookie-value');
-    }).catch(failure_);
+    return cookieStore.get('cookie-name')
+    .then((cookie) => cookie.value == 'cookie-value')
+    .catch(() => false);
   }
 
   function setLocalStorage() {
     localStorage.setItem('foo', 'bar');
-    success_();
+    return true;
+  }
+  function setLocalStorageAsync() {
+    // Provided for Java.
+    domAutomationController.send(setLocalStorage());
   }
 
   function hasLocalStorage() {
     try {
-      domAutomationController.send(localStorage.getItem('foo') == 'bar');
+      return localStorage.getItem('foo') == 'bar';
     } catch (e) {
-      failure_();
+      return false;
     }
   }
+  function hasLocalStorageAsync() {
+    // Provided for Java.
+    domAutomationController.send(hasLocalStorage());
+  }
 
   function setSessionStorage() {
     sessionStorage.setItem('foo', 'bar');
-    success_();
+    return true;
   }
 
   function hasSessionStorage() {
     try {
-      domAutomationController.send(sessionStorage.getItem('foo') == 'bar');
+      return sessionStorage.getItem('foo') == 'bar';
     } catch (e) {
-      failure_();
+      return false;
     }
   }
 
   function setServiceWorker() {
-    navigator.serviceWorker.register('empty_worker.js').then(function() {
-      navigator.serviceWorker.ready.then(success_);
-    }).catch(failure_);
+    return navigator.serviceWorker.register('empty_worker.js')
+    .then(() => navigator.serviceWorker.ready)
+    .then(() => true)
+    .catch(() => false);
+  }
+  async function setServiceWorkerAsync() {
+    // Provided for Java.
+    domAutomationController.send(await setServiceWorker());
   }
 
   function hasServiceWorker() {
-    navigator.serviceWorker.getRegistrations().then(function (registrations) {
-      domAutomationController.send(registrations.length > 0);
-    }).catch(failure_);
+    return navigator.serviceWorker.getRegistrations()
+    .then((registrations) => registrations.length > 0)
+    .catch(() => false);
+  }
+  async function hasServiceWorkerAsync() {
+    // Provided for Java.
+    domAutomationController.send(await hasServiceWorker());
   }
 
   function setCacheStorage() {
-    caches.open("cache").then(function (cache) {
-      cache.put("/foo", new Response("bar")).then(success_);
-    }).catch(failure_);
+    return caches.open("cache")
+    .then((cache) => cache.put("/foo", new Response("bar")))
+    .then(() => true)
+    .catch(() => false);
+  }
+  async function setCacheStorageAsync() {
+    // Provided for Java.
+    domAutomationController.send(await setCacheStorage());
   }
 
   function hasCacheStorage() {
-    caches.open("cache").then(function (cache) {
-      cache.keys().then(function (keys) {
-        domAutomationController.send(keys.length > 0);
-      });
-    }).catch(failure_);
+    return caches.open("cache")
+    .then((cache) => cache.keys())
+    .then((keys) => keys.length > 0)
+    .catch(() => false);
+  }
+  async function hasCacheStorageAsync() {
+    // Provided for Java.
+    domAutomationController.send(await hasCacheStorage());
   }
 
-  function openFile_(name, options, callback, error) {
-    window.webkitRequestFileSystem(TEMPORARY, 1024, function (fs) {
-      fs.root.getFile(name, options, callback, error);
-    }, error);
+  async function openFile_(name, options, callback, error) {
+    const fs = await new Promise((resolve, reject) => {
+      window.webkitRequestFileSystem(TEMPORARY, 1024, resolve, reject);
+    });
+    return new Promise((resolve, reject) => {
+      fs.root.getFile(name, options, resolve, reject);
+    });
   }
 
   function setFileSystem() {
-    openFile_('foo.txt', { create: true, exclusive: true }, success_, failure_);
+    return openFile_('foo.txt', { create: true, exclusive: true })
+    .then(() => true)
+    .catch(() => false);
+  }
+  async function setFileSystemAsync() {
+    // Provided for Java.
+    domAutomationController.send(await setFileSystem());
   }
 
   function hasFileSystem() {
-    openFile_('foo.txt', { create: false }, success_, failure_);
+    return openFile_('foo.txt', { create: false })
+    .then(() => true)
+    .catch(() => false);
+  }
+  async function hasFileSystemAsync() {
+    // Provided for Java.
+    domAutomationController.send(await hasFileSystem());
   }
 
   async function setFileSystemAccess() {
     try {
       let dir = await navigator.storage.getDirectory();
       await dir.getFileHandle('foo.txt', {create: true});
-      success_();
+      return true;
     } catch (e) {
-      failure_();
+      return false;
     }
   }
 
@@ -126,100 +168,126 @@
     try {
       let dir = await navigator.storage.getDirectory();
       await dir.getFileHandle('foo.txt', { create: false });
-      success_();
+      return true;
     } catch (e) {
-      failure_();
+      return false;
     }
   }
 
-  function setWebSql() {
+  async function setWebSql() {
     try {
       var db = openDatabase('testdb', '1.0', 'a test db', 1024);
-      db.transaction(function (tx) {
-        tx.executeSql('CREATE TABLE IF NOT EXISTS foo (text)');
-        tx.executeSql('INSERT INTO foo (text) VALUES ("bar")');
-      }, failure_, success_);
+      await new Promise((resolve, reject) => {
+        db.transaction(function (tx) {
+          tx.executeSql('CREATE TABLE IF NOT EXISTS foo (text)');
+          tx.executeSql('INSERT INTO foo (text) VALUES ("bar")');
+        }, reject, resolve);
+      });
+      return true;
     } catch (e) {
-      return failure_();
+      return false;
     }
   }
+  async function setWebSqlAsync() {
+    // Provided for Java.
+    domAutomationController.send(await setWebSql());
+  }
 
-  function hasWebSql() {
+  async function hasWebSql() {
     try {
       var db = openDatabase('testdb', '1.0', 'a test db', 1024);
       var num_results;
-      db.transaction(function (tx) {
-        tx.executeSql('CREATE TABLE IF NOT EXISTS foo (text)');
-        tx.executeSql('SELECT * FROM foo', [], function (tx, results) {
-          num_results = results.rows.length;
-        });
-      }, null, function() { domAutomationController.send(num_results > 0); });
+      await new Promise((resolve, reject) => {
+        db.transaction(function (tx) {
+          tx.executeSql('CREATE TABLE IF NOT EXISTS foo (text)');
+          tx.executeSql('SELECT * FROM foo', [], function (tx, results) {
+            num_results = results.rows.length;
+          });
+        }, reject, resolve);
+      });
+      return num_results > 0;
     } catch (e) {
-      return failure_();
+      return false;
     }
   }
+  async function hasWebSqlAsync() {
+    // Provided for Java.
+    domAutomationController.send(await hasWebSql());
+  }
 
   function setIndexedDb() {
     var open = indexedDB.open('db', 2);
     open.onupgradeneeded = function() {
       open.result.createObjectStore('store');
     }
-    open.onsuccess = function() {
-      open.result.close();
-      success_();
-    }
-    open.onerror = failure_;
+    return new Promise((resolve) => {
+      open.onsuccess = function () {
+        open.result.close();
+        resolve(true);
+      }
+      open.onerror = () => resolve(false);
+    });
+  }
+  async function setIndexedDbAsync() {
+    domAutomationController.send(await setIndexedDb());
   }
 
   function hasIndexedDb() {
     var open = indexedDB.open('db');
-    open.onsuccess = function() {
-      var hasStore = open.result.objectStoreNames.contains('store');
-      open.result.close();
-      domAutomationController.send(hasStore);
-    }
-    open.onerror = failure_;
+    return new Promise((resolve) => {
+      open.onsuccess = function() {
+        var hasStore = open.result.objectStoreNames.contains('store');
+        open.result.close();
+        resolve(hasStore);
+      }
+      open.onerror = () => resolve(false);
+    });
+  }
+  async function hasIndexedDbAsync() {
+    domAutomationController.send(await hasIndexedDb());
   }
 
   function worker_(command) {
     let worker = new Worker("site_data_worker.js");
-    worker.onmessage = e => {
-      domAutomationController.send(e.data);
-    };
-    worker.postMessage(command)
+    return new Promise((resolve) => {
+      worker.onmessage = e => {
+        resolve(e.data);
+      };
+      worker.postMessage(command);
+    });
   }
 
   function hasWorkerFileSystemAccess() {
-    worker_("hasFileSystemAccess");
+    return worker_("hasFileSystemAccess");
   }
 
   function setWorkerFileSystemAccess() {
-    worker_("setFileSystemAccess");
+    return worker_("setFileSystemAccess");
   }
 
   function hasWorkerCacheStorage() {
-    worker_("hasCacheStorage");
+    return worker_("hasCacheStorage");
   }
 
   function setWorkerCacheStorage() {
-    worker_("setCacheStorage");
+    return worker_("setCacheStorage");
   }
 
   function hasWorkerIndexedDb() {
-    worker_("hasIndexedDb");
+    return worker_("hasIndexedDb");
   }
 
   function setWorkerIndexedDb() {
-    worker_("setIndexedDb");
+    return worker_("setIndexedDb");
   }
 
   function setHistory() {
     history.pushState({}, "foo");
-    success_();
+    return true;
   }
 
   function hasHistory() {
-    domAutomationController.send(history.length > 1);
+    return history.length > 1;
   }
 
   let sharedWorker; // Global variable to keep worker alive.
@@ -233,39 +301,44 @@
   }
 
   function setSharedWorker() {
-    connectSharedWorker().then(() => {
+    return connectSharedWorker()
+    .then(() => {
       sharedWorker.port.postMessage({ "value": "foo" });
-      success_();
-    }).catch(failure_);
+      return true;
+    })
+    .catch(() => false);
   }
 
   async function hasSharedWorker() {
-    connectSharedWorker().then(() => {
-      sharedWorker.port.onmessage = e => {
-        domAutomationController.send(e.data.value === "foo");
-        sharedWorker.port.onmessage = null; // Only send a single response via domAutomationController.
-      };
-      sharedWorker.port.postMessage({});
-    }).catch(failure_);
+    return connectSharedWorker()
+    .then(() => {
+      return new Promise((resolve) => {
+        sharedWorker.port.onmessage = e => {
+          resolve(e.data.value === "foo");
+        };
+        sharedWorker.port.postMessage({});
+      });
+    })
+    .catch(() => false);
   }
 
   let lock;
   function setWebLock() {
-    navigator.locks.request("foo", l => {
-      lock = new Promise((res, rej) => { });
-      // Now lock will be held while |lock| exists.
-      success_();
-      return lock;
-    }).catch(failure_);
+    return new Promise((resolve) => {
+      navigator.locks.request("foo", () => {
+        lock = new Promise(() => { });
+        // Now lock will be held while |lock| exists.
+        resolve(true);
+        return lock;
+      })
+    })
+    .catch(() => false);
   }
 
   function hasWebLock() {
-    navigator.locks.query().then(locks => {
-      if (locks.held.length)
-        domAutomationController.send(locks.held[0].name === "foo");
-      else
-        failure_();
-    }).catch(failure_);
+    return navigator.locks.query()
+    .then(locks => !!locks.held.length && locks.held[0].name === "foo")
+    .catch(() => false);
   }
 </script>
 
diff --git a/extensions/common/mojom/api_permission_id.mojom b/extensions/common/mojom/api_permission_id.mojom
index 5dd0932..80da98e 100644
--- a/extensions/common/mojom/api_permission_id.mojom
+++ b/extensions/common/mojom/api_permission_id.mojom
@@ -90,7 +90,7 @@
   kDeprecated_ExternallyConnectableAllUrls = 63,
   kFeedbackPrivate = 64,
   kFileBrowserHandler = 65,
-  kFileBrowserHandlerInternal = 66,
+  kDeleted_FileBrowserHandlerInternal = 66,
   kFileManagerPrivate = 67,
   kFileSystem = 68,
   kFileSystemDirectory = 69,
diff --git a/extensions/common/permissions/permission_set.cc b/extensions/common/permissions/permission_set.cc
index d063981..e20c0f9 100644
--- a/extensions/common/permissions/permission_set.cc
+++ b/extensions/common/permissions/permission_set.cc
@@ -27,7 +27,6 @@
       explicit_hosts_(std::move(explicit_hosts)),
       scriptable_hosts_(std::move(scriptable_hosts)) {
   CleanExplicitHostPaths();
-  InitImplicitPermissions();
   InitEffectiveHosts();
 }
 
@@ -215,9 +214,7 @@
 void PermissionSet::SetAPIPermissions(APIPermissionSet new_apis) {
   apis_ = std::move(new_apis);
   // Since we're rewriting the API permissions, we need to re-initialize the
-  // value of whether to warn about all hosts and re-add any implicit API
-  // permissions.
-  InitImplicitPermissions();
+  // value of whether to warn about all hosts.
   api_permissions_should_warn_all_hosts_ = UNINITIALIZED;
 }
 
@@ -279,12 +276,6 @@
     explicit_hosts_.AddPattern(std::move(pattern));
 }
 
-void PermissionSet::InitImplicitPermissions() {
-  // The fileBrowserHandler permission implies the internal version as well.
-  if (apis_.find(APIPermissionID::kFileBrowserHandler) != apis_.end())
-    apis_.insert(APIPermissionID::kFileBrowserHandlerInternal);
-}
-
 void PermissionSet::InitEffectiveHosts() {
   effective_hosts_ =
       URLPatternSet::CreateUnion(explicit_hosts(), scriptable_hosts());
diff --git a/extensions/common/permissions/permission_set.h b/extensions/common/permissions/permission_set.h
index a0fa0c8..47ec754 100644
--- a/extensions/common/permissions/permission_set.h
+++ b/extensions/common/permissions/permission_set.h
@@ -138,9 +138,6 @@
   // "/*", and we implicitly make this change.
   void CleanExplicitHostPaths();
 
-  // Adds permissions implied independently of other context.
-  void InitImplicitPermissions();
-
   // Initializes the effective host permission based on the data in this set.
   void InitEffectiveHosts();
 
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
index 96c5e96e..e74db90f 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
@@ -2830,6 +2830,10 @@
                                                          GLenum binaryformat,
                                                          const void* binary,
                                                          GLsizei length) {
+#if 1  // No binary shader support.
+  InsertError(GL_INVALID_ENUM, "Invalid enum.");
+  return error::kNoError;
+#else
   std::vector<GLuint> service_shaders(n, 0);
   for (GLsizei i = 0; i < n; i++) {
     service_shaders[i] = GetShaderServiceID(shaders[i], resources_);
@@ -2837,6 +2841,7 @@
   api()->glShaderBinaryFn(n, service_shaders.data(), binaryformat, binary,
                           length);
   return error::kNoError;
+#endif
 }
 
 error::Error GLES2DecoderPassthroughImpl::DoShaderSource(GLuint shader,
diff --git a/gpu/config/gpu_finch_features.cc b/gpu/config/gpu_finch_features.cc
index 330aeb2..a19185d 100644
--- a/gpu/config/gpu_finch_features.cc
+++ b/gpu/config/gpu_finch_features.cc
@@ -188,6 +188,12 @@
 #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_IOS)
 // Enable use of Metal for OOP rasterization.
 BASE_FEATURE(kMetal, "Metal", base::FEATURE_DISABLED_BY_DEFAULT);
+
+// If enabled, the TASK_CATEGORY_POLICY value of the GPU process will be
+// adjusted to match the one from the browser process every time it changes.
+BASE_FEATURE(kAdjustGpuProcessPriority,
+             "AdjustGpuProcessPriority",
+             base::FEATURE_DISABLED_BY_DEFAULT);
 #endif
 
 // Causes us to use the SharedImageManager, removing support for the old
diff --git a/gpu/config/gpu_finch_features.h b/gpu/config/gpu_finch_features.h
index a20b868..fac08e7 100644
--- a/gpu/config/gpu_finch_features.h
+++ b/gpu/config/gpu_finch_features.h
@@ -48,6 +48,8 @@
 
 #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_IOS)
 GPU_EXPORT BASE_DECLARE_FEATURE(kMetal);
+
+GPU_EXPORT BASE_DECLARE_FEATURE(kAdjustGpuProcessPriority);
 #endif
 
 GPU_EXPORT BASE_DECLARE_FEATURE(kSharedImageManager);
diff --git a/ios/chrome/browser/ui/content_suggestions/content_suggestions_header_view.mm b/ios/chrome/browser/ui/content_suggestions/content_suggestions_header_view.mm
index ade41c2e..44038da 100644
--- a/ios/chrome/browser/ui/content_suggestions/content_suggestions_header_view.mm
+++ b/ios/chrome/browser/ui/content_suggestions/content_suggestions_header_view.mm
@@ -96,6 +96,7 @@
     NSLayoutConstraint* fakeLocationBarHeightConstraint;
 @property(nonatomic, strong) NSLayoutConstraint* fakeToolbarTopConstraint;
 @property(nonatomic, strong) NSLayoutConstraint* hintLabelLeadingConstraint;
+@property(nonatomic, strong) NSLayoutConstraint* hintLabelTrailingConstraint;
 // In the new layout, the hint label should always be at least inside the fake
 // omnibox. When the fake omnibox is shrunk, the position from the leading side
 // of the search field should yield. This constraint is not defined for the old
@@ -256,6 +257,9 @@
   // is taking the full width, there are few points that are not accessible and
   // allow to select the content below it.
   self.searchHintLabel.isAccessibilityElement = NO;
+  [self.searchHintLabel
+      setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
+                                      forAxis:UILayoutConstraintAxisHorizontal];
 
   // Voice search.
   self.voiceSearchButton =
@@ -324,13 +328,15 @@
       constraintLessThanOrEqualToAnchor:self.fakeLocationBar.trailingAnchor
                                constant:-kEndButtonFakeboxTrailingSpace];
 
+  // The voice search button is always on the leading side, even if the Lens
+  // button is visible.
+  self.hintLabelTrailingConstraint = [self.searchHintLabel.trailingAnchor
+      constraintLessThanOrEqualToAnchor:self.voiceSearchButton.leadingAnchor];
+  self.hintLabelTrailingConstraint.priority = UILayoutPriorityDefaultHigh;
   [NSLayoutConstraint activateConstraints:@[
     [self.voiceSearchButton.centerYAnchor
         constraintEqualToAnchor:self.fakeLocationBar.centerYAnchor],
-    // The voice search button is always on the left, even if the Lens button is
-    // visible.
-    [self.searchHintLabel.trailingAnchor
-        constraintLessThanOrEqualToAnchor:self.voiceSearchButton.leadingAnchor],
+    self.hintLabelTrailingConstraint,
     self.endButtonTrailingMarginConstraint,
     self.endButtonTrailingConstraint,
   ]];
@@ -500,6 +506,14 @@
       (kEndButtonFakeboxTrailingSpace - kEndButtonOmniboxTrailingSpace) *
           percent;
 
+  // Offset the hint label constraints with half of the change in width
+  // from the original scale, since constraints are calculated before
+  // transformations are applied. This prevents the label from overlapping
+  // with other UI elements.
+  CGFloat additionalScaleOffset =
+      (content_suggestions::kHintTextScale * (1 - percent)) *
+      self.searchHintLabel.bounds.size.width * 0.5;
+  self.hintLabelTrailingConstraint.constant = -additionalScaleOffset;
   if (base::FeatureList::IsEnabled(kNewNTPOmniboxLayout)) {
     // A similar positioning scheme is applied to the leading-edge-aligned
     // hint label as the trailing-edge-aligned buttons.
@@ -508,20 +522,8 @@
         kHintLabelFakeboxLeadingSpace -
         (kHintLabelFakeboxLeadingSpace - kHintLabelOmniboxLeadingSpace) *
             percent;
-
-    // Offset the hint label constraint with half of the change in width
-    // from the original scale, since constraints are calculated before
-    // transformations are applied. This is only necessary with the new NTP
-    // omnibox layout as the new layout does not have a center-aligned hint
-    // label.
-    CGFloat scaleValue =
-        1 + (content_suggestions::kHintTextScale * (1 - percent));
-    self.searchHintLabel.transform =
-        CGAffineTransformMakeScale(scaleValue, scaleValue);
     self.hintLabelLeadingConstraint.constant =
-        desiredLeadingSpace + (1 - percent) *
-                                  content_suggestions::kHintTextScale *
-                                  self.searchHintLabel.bounds.size.width * 0.5;
+        desiredLeadingSpace + additionalScaleOffset;
   } else {
     self.hintLabelLeadingConstraint.constant =
         subviewsDiff + ntp_header::kCenteredHintLabelSidePadding;
diff --git a/ios/chrome/browser/ui/tabs/DEPS b/ios/chrome/browser/ui/tabs/DEPS
deleted file mode 100644
index c55a5355..0000000
--- a/ios/chrome/browser/ui/tabs/DEPS
+++ /dev/null
@@ -1,5 +0,0 @@
-specific_include_rules = {
-  "tab_view\.mm": [
-    "+third_party/google_toolbox_for_mac/src/iPhone/GTMFadeTruncatingLabel.h",
-  ],
-}
diff --git a/media/audio/audio_manager_unittest.cc b/media/audio/audio_manager_unittest.cc
index d34c6d8..739e7785 100644
--- a/media/audio/audio_manager_unittest.cc
+++ b/media/audio/audio_manager_unittest.cc
@@ -234,6 +234,7 @@
     // Flush the message loop to run any shutdown tasks posted by AudioManager.
     if (audio_manager_) {
       audio_manager_->Shutdown();
+      device_info_accessor_.reset();
       audio_manager_.reset();
     }
 
diff --git a/media/audio/audio_output_device.h b/media/audio/audio_output_device.h
index abf8432..d396830 100644
--- a/media/audio/audio_output_device.h
+++ b/media/audio/audio_output_device.h
@@ -129,6 +129,8 @@
                        bool play_automatically) override;
   void OnIPCClosed() override;
 
+  AudioOutputIPC* GetIpcForTesting() { return ipc_.get(); }
+
  protected:
   // Magic required by ref_counted.h to avoid any code deleting the object
   // accidentally while there are references to it.
diff --git a/media/audio/audio_output_device_unittest.cc b/media/audio/audio_output_device_unittest.cc
index 89d675e..d094d6e8 100644
--- a/media/audio/audio_output_device_unittest.cc
+++ b/media/audio/audio_output_device_unittest.cc
@@ -102,21 +102,27 @@
   MOCK_METHOD1(OnDeviceInfoReceived, void(OutputDeviceInfo));
 
  protected:
+  MockAudioOutputIPC* audio_output_ipc() {
+    return static_cast<MockAudioOutputIPC*>(audio_device_->GetIpcForTesting());
+  }
+
   base::test::TaskEnvironment task_env_{
       base::test::TaskEnvironment::TimeSource::MOCK_TIME};
   AudioParameters default_audio_parameters_;
   StrictMock<MockRenderCallback> callback_;
-  raw_ptr<MockAudioOutputIPC> audio_output_ipc_;  // owned by audio_device_
-  scoped_refptr<AudioOutputDevice> audio_device_;
   OutputDeviceStatus device_status_;
 
  private:
   int CalculateMemorySize();
 
+  // These may need to outlive `audio_device_`.
   UnsafeSharedMemoryRegion shared_memory_region_;
   WritableSharedMemoryMapping shared_memory_mapping_;
   CancelableSyncSocket browser_socket_;
   CancelableSyncSocket renderer_socket_;
+
+ protected:
+  scoped_refptr<AudioOutputDevice> audio_device_;
 };
 
 AudioOutputDeviceTest::AudioOutputDeviceTest()
@@ -126,9 +132,7 @@
   SetDevice(kDefaultDeviceId);
 }
 
-AudioOutputDeviceTest::~AudioOutputDeviceTest() {
-  audio_device_ = nullptr;
-}
+AudioOutputDeviceTest::~AudioOutputDeviceTest() = default;
 
 void AudioOutputDeviceTest::CreateDevice(const std::string& device_id,
                                          base::TimeDelta timeout) {
@@ -136,16 +140,15 @@
   if (audio_device_)
     StopAudioDevice();
 
-  audio_output_ipc_ = new NiceMock<MockAudioOutputIPC>();
   audio_device_ = new AudioOutputDevice(
-      base::WrapUnique(audio_output_ipc_.get()),
+      std::make_unique<NiceMock<MockAudioOutputIPC>>(),
       task_env_.GetMainThreadTaskRunner(),
       AudioSinkParameters(base::UnguessableToken(), device_id), timeout);
 }
 
 void AudioOutputDeviceTest::SetDevice(const std::string& device_id) {
   CreateDevice(device_id);
-  EXPECT_CALL(*audio_output_ipc_,
+  EXPECT_CALL(*audio_output_ipc(),
               RequestDeviceAuthorization(audio_device_.get(),
                                          base::UnguessableToken(), device_id));
   audio_device_->RequestDeviceAuthorization();
@@ -165,7 +168,7 @@
 void AudioOutputDeviceTest::ReceiveAuthorization(OutputDeviceStatus status) {
   device_status_ = status;
   if (device_status_ != OUTPUT_DEVICE_STATUS_OK)
-    EXPECT_CALL(*audio_output_ipc_, CloseStream());
+    EXPECT_CALL(*audio_output_ipc(), CloseStream());
 
   audio_device_->OnDeviceAuthorized(device_status_, default_audio_parameters_,
                                     kDefaultDeviceId);
@@ -174,7 +177,7 @@
 
 void AudioOutputDeviceTest::StartAudioDevice() {
   if (device_status_ == OUTPUT_DEVICE_STATUS_OK)
-    EXPECT_CALL(*audio_output_ipc_, CreateStream(audio_device_.get(), _));
+    EXPECT_CALL(*audio_output_ipc(), CreateStream(audio_device_.get(), _));
   else
     EXPECT_CALL(callback_, OnRenderError());
 
@@ -210,7 +213,7 @@
 
 void AudioOutputDeviceTest::StopAudioDevice() {
   if (device_status_ == OUTPUT_DEVICE_STATUS_OK)
-    EXPECT_CALL(*audio_output_ipc_, CloseStream());
+    EXPECT_CALL(*audio_output_ipc(), CloseStream());
 
   audio_device_->Stop();
   task_env_.FastForwardBy(base::TimeDelta());
@@ -218,7 +221,7 @@
 
 void AudioOutputDeviceTest::FlushAudioDevice() {
   if (device_status_ == OUTPUT_DEVICE_STATUS_OK)
-    EXPECT_CALL(*audio_output_ipc_, FlushStream());
+    EXPECT_CALL(*audio_output_ipc(), FlushStream());
 
   audio_device_->Flush();
   task_env_.FastForwardBy(base::TimeDelta());
@@ -255,7 +258,7 @@
 
   // Expect us to shutdown IPC but not to render anything despite the stream
   // getting created.
-  EXPECT_CALL(*audio_output_ipc_, CloseStream());
+  EXPECT_CALL(*audio_output_ipc(), CloseStream());
   CallOnStreamCreated();
 }
 
@@ -265,7 +268,7 @@
   StartAudioDevice();
   StopAudioDevice();
 
-  EXPECT_CALL(*audio_output_ipc_,
+  EXPECT_CALL(*audio_output_ipc(),
               RequestDeviceAuthorization(audio_device_.get(),
                                          base::UnguessableToken(), _));
   StartAudioDevice();
@@ -294,14 +297,13 @@
 TEST_F(AudioOutputDeviceTest, AuthorizationFailsBeforeInitialize_NoError) {
   // Clear audio device set by fixture.
   StopAudioDevice();
-  audio_output_ipc_ = new NiceMock<MockAudioOutputIPC>();
   audio_device_ = new AudioOutputDevice(
-      base::WrapUnique(audio_output_ipc_.get()),
+      std::make_unique<NiceMock<MockAudioOutputIPC>>(),
       task_env_.GetMainThreadTaskRunner(),
       AudioSinkParameters(base::UnguessableToken(), kDefaultDeviceId),
       kAuthTimeout);
   EXPECT_CALL(
-      *audio_output_ipc_,
+      *audio_output_ipc(),
       RequestDeviceAuthorization(audio_device_.get(), base::UnguessableToken(),
                                  kDefaultDeviceId));
 
@@ -321,10 +323,10 @@
 TEST_F(AudioOutputDeviceTest, AuthorizationTimedOut) {
   CreateDevice(kNonDefaultDeviceId);
   EXPECT_CALL(
-      *audio_output_ipc_,
+      *audio_output_ipc(),
       RequestDeviceAuthorization(audio_device_.get(), base::UnguessableToken(),
                                  kNonDefaultDeviceId));
-  EXPECT_CALL(*audio_output_ipc_, CloseStream());
+  EXPECT_CALL(*audio_output_ipc(), CloseStream());
 
   // Request authorization; no reply from the browser.
   audio_device_->RequestDeviceAuthorization();
@@ -339,7 +341,7 @@
 TEST_F(AudioOutputDeviceTest, GetOutputDeviceInfoAsync_Error) {
   CreateDevice(kUnauthorizedDeviceId, base::TimeDelta());
   EXPECT_CALL(
-      *audio_output_ipc_,
+      *audio_output_ipc(),
       RequestDeviceAuthorization(audio_device_.get(), base::UnguessableToken(),
                                  kUnauthorizedDeviceId));
   audio_device_->RequestDeviceAuthorization();
@@ -366,7 +368,7 @@
 TEST_F(AudioOutputDeviceTest, GetOutputDeviceInfoAsync_Okay) {
   CreateDevice(kDefaultDeviceId, base::TimeDelta());
   EXPECT_CALL(
-      *audio_output_ipc_,
+      *audio_output_ipc(),
       RequestDeviceAuthorization(audio_device_.get(), base::UnguessableToken(),
                                  kDefaultDeviceId));
   audio_device_->RequestDeviceAuthorization();
diff --git a/media/base/media_switches.cc b/media/base/media_switches.cc
index d250ce6..9a935487e 100644
--- a/media/base/media_switches.cc
+++ b/media/base/media_switches.cc
@@ -539,7 +539,7 @@
 // information on the quality of the session using RTCP logs.
 BASE_FEATURE(kEnableRtcpReporting,
              "EnableRtcpReporting",
-             base::FEATURE_DISABLED_BY_DEFAULT);
+             base::FEATURE_ENABLED_BY_DEFAULT);
 
 // Approach original pre-REC MSE object URL autorevoking behavior, though await
 // actual attempt to use the object URL for attachment to perform revocation.
diff --git a/remoting/host/DEPS b/remoting/host/DEPS
index 5d4d463b..f64775b 100644
--- a/remoting/host/DEPS
+++ b/remoting/host/DEPS
@@ -22,7 +22,6 @@
   "+services/device/public",
   "+services/device/wake_lock/power_save_blocker",
   "+services/network",
-  "+third_party/google_toolbox_for_mac",
   "+third_party/grpc",
   "+third_party/jsoncpp",
   "+third_party/skia",
diff --git a/remoting/host/input_monitor/BUILD.gn b/remoting/host/input_monitor/BUILD.gn
index afc5d12..f930949 100644
--- a/remoting/host/input_monitor/BUILD.gn
+++ b/remoting/host/input_monitor/BUILD.gn
@@ -48,7 +48,6 @@
       "local_keyboard_input_monitor_mac.mm",
       "local_mouse_input_monitor_mac.mm",
     ]
-    deps += [ "//third_party/google_toolbox_for_mac" ]
   }
 
   if (remoting_use_x11) {
diff --git a/remoting/host/input_monitor/local_hotkey_input_monitor_mac.mm b/remoting/host/input_monitor/local_hotkey_input_monitor_mac.mm
index 48317f9..453bd38 100644
--- a/remoting/host/input_monitor/local_hotkey_input_monitor_mac.mm
+++ b/remoting/host/input_monitor/local_hotkey_input_monitor_mac.mm
@@ -4,10 +4,8 @@
 
 #include "remoting/host/input_monitor/local_hotkey_input_monitor.h"
 
-#include "base/memory/raw_ptr.h"
-#import "base/task/single_thread_task_runner.h"
-
 #import <AppKit/AppKit.h>
+#import <Carbon/Carbon.h>
 
 #include <cstdint>
 #include <utility>
@@ -18,18 +16,16 @@
 #include "base/location.h"
 #include "base/logging.h"
 #include "base/mac/scoped_cftyperef.h"
+#include "base/mac/scoped_nsobject.h"
 #include "base/memory/ptr_util.h"
+#include "base/memory/raw_ptr.h"
 #include "base/memory/ref_counted.h"
 #include "base/sequence_checker.h"
 #include "base/synchronization/lock.h"
+#import "base/task/single_thread_task_runner.h"
 #include "base/task/single_thread_task_runner.h"
-#import "third_party/google_toolbox_for_mac/src/AppKit/GTMCarbonEvent.h"
 #include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
 
-// Esc Key Code is 53.
-// http://boredzo.org/blog/wp-content/uploads/2007/05/IMTx-virtual-keycodes.pdf
-static const NSUInteger kEscKeyCode = 53;
-
 namespace remoting {
 namespace {
 
@@ -65,60 +61,53 @@
 }  // namespace
 }  // namespace remoting
 
-@interface LocalHotkeyInputMonitorManager : NSObject {
- @private
-  GTMCarbonHotKey* _hotKey;
-  raw_ptr<remoting::LocalHotkeyInputMonitorMac::EventHandler> _monitor;
-}
+@interface LocalHotkeyInputMonitorManager : NSObject
 
 - (instancetype)initWithMonitor:
     (remoting::LocalHotkeyInputMonitorMac::EventHandler*)monitor;
 
-// Called when the hotKey is hit.
-- (void)hotKeyHit:(GTMCarbonHotKey*)hotKey;
-
-// Must be called when the LocalHotkeyInputMonitorManager is no longer needed.
-// Similar to NSTimer in that more than a simple release is required.
-- (void)invalidate;
-
 @end
 
-@implementation LocalHotkeyInputMonitorManager
+@implementation LocalHotkeyInputMonitorManager {
+  id _eventMonitor;
+
+  raw_ptr<remoting::LocalHotkeyInputMonitorMac::EventHandler> _monitor;
+}
 
 - (instancetype)initWithMonitor:
     (remoting::LocalHotkeyInputMonitorMac::EventHandler*)monitor {
   if ((self = [super init])) {
     _monitor = monitor;
 
-    GTMCarbonEventDispatcherHandler* handler =
-        [GTMCarbonEventDispatcherHandler sharedEventDispatcherHandler];
-    _hotKey = [handler
-        registerHotKey:kEscKeyCode
-             modifiers:(NSEventModifierFlagOption | NSEventModifierFlagControl)
-                target:self
-                action:@selector(hotKeyHit:)
-              userInfo:nil
-           whenPressed:YES];
-    if (!_hotKey) {
-      LOG(ERROR) << "registerHotKey failed.";
-      [self release];
-      return nil;
-    }
+    auto eventHandler = ^NSEvent*(NSEvent* event) {
+      const NSEventModifierFlags requiredModifiers =
+          NSEventModifierFlagOption | NSEventModifierFlagControl;
+      if ((event.keyCode == kVK_Escape) &&
+          (event.modifierFlags & requiredModifiers)) {
+        // Trigger the callback.
+        _monitor->OnDisconnectShortcut();
+
+        // Stop the event propagation.
+        return nil;
+      }
+
+      // Otherwise, let the event continue propagating.
+      return event;
+    };
+
+    _eventMonitor =
+        [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown
+                                              handler:eventHandler];
   }
   return self;
 }
 
-- (void)hotKeyHit:(GTMCarbonHotKey*)hotKey {
-  _monitor->OnDisconnectShortcut();
-}
-
-- (void)invalidate {
-  if (_hotKey) {
-    GTMCarbonEventDispatcherHandler* handler =
-        [GTMCarbonEventDispatcherHandler sharedEventDispatcherHandler];
-    [handler unregisterHotKey:_hotKey];
-    _hotKey = nullptr;
+- (void)dealloc {
+  if (_eventMonitor) {
+    [NSEvent removeMonitor:_eventMonitor];
   }
+
+  [super dealloc];
 }
 
 @end
@@ -156,7 +145,7 @@
   // Task runner on which |window_| is created.
   scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner_;
 
-  LocalHotkeyInputMonitorManager* manager_;
+  base::scoped_nsobject<LocalHotkeyInputMonitorManager> manager_;
 
   // Invoked in the |caller_task_runner_| thread to report session disconnect
   // requests.
@@ -184,7 +173,6 @@
     base::OnceClosure disconnect_callback)
     : caller_task_runner_(caller_task_runner),
       ui_task_runner_(ui_task_runner),
-      manager_(nil),
       disconnect_callback_(std::move(disconnect_callback)) {
   DCHECK(disconnect_callback_);
 }
@@ -204,21 +192,19 @@
 }
 
 LocalHotkeyInputMonitorMac::Core::~Core() {
-  DCHECK_EQ(manager_, nil);
+  DCHECK_EQ(manager_.get(), nil);
 }
 
 void LocalHotkeyInputMonitorMac::Core::StartOnUiThread() {
   DCHECK(ui_task_runner_->BelongsToCurrentThread());
 
-  manager_ = [[LocalHotkeyInputMonitorManager alloc] initWithMonitor:this];
+  manager_.reset([[LocalHotkeyInputMonitorManager alloc] initWithMonitor:this]);
 }
 
 void LocalHotkeyInputMonitorMac::Core::StopOnUiThread() {
   DCHECK(ui_task_runner_->BelongsToCurrentThread());
 
-  [manager_ invalidate];
-  [manager_ release];
-  manager_ = nil;
+  manager_.reset();
 }
 
 void LocalHotkeyInputMonitorMac::Core::OnDisconnectShortcut() {
diff --git a/services/device/generic_sensor/sensor_provider_impl.h b/services/device/generic_sensor/sensor_provider_impl.h
index da593bc..a9820ba 100644
--- a/services/device/generic_sensor/sensor_provider_impl.h
+++ b/services/device/generic_sensor/sensor_provider_impl.h
@@ -6,7 +6,6 @@
 #define SERVICES_DEVICE_GENERIC_SENSOR_SENSOR_PROVIDER_IMPL_H_
 
 #include "base/memory/read_only_shared_memory_region.h"
-#include "base/task/single_thread_task_runner.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/receiver_set.h"
 #include "mojo/public/cpp/bindings/unique_receiver_set.h"
diff --git a/testing/buildbot/chromium.chromiumos.json b/testing/buildbot/chromium.chromiumos.json
index 9d0c2ce2..27aa429 100644
--- a/testing/buildbot/chromium.chromiumos.json
+++ b/testing/buildbot/chromium.chromiumos.json
@@ -5793,9 +5793,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -5807,8 +5807,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -5892,9 +5892,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -5906,8 +5906,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
@@ -5965,9 +5965,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -5979,8 +5979,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -6061,9 +6061,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -6075,8 +6075,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
@@ -6116,9 +6116,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -6130,8 +6130,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -6215,9 +6215,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -6229,8 +6229,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
diff --git a/testing/buildbot/chromium.coverage.json b/testing/buildbot/chromium.coverage.json
index 9b15bbd..9a45a30 100644
--- a/testing/buildbot/chromium.coverage.json
+++ b/testing/buildbot/chromium.coverage.json
@@ -25656,9 +25656,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -25670,8 +25670,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -25755,9 +25755,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -25769,8 +25769,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
@@ -25828,9 +25828,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -25842,8 +25842,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -25924,9 +25924,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -25938,8 +25938,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
@@ -25979,9 +25979,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -25993,8 +25993,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -26078,9 +26078,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -26092,8 +26092,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
diff --git a/testing/buildbot/chromium.fyi.json b/testing/buildbot/chromium.fyi.json
index 288be4af..cc2758ed 100644
--- a/testing/buildbot/chromium.fyi.json
+++ b/testing/buildbot/chromium.fyi.json
@@ -48693,9 +48693,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -48706,8 +48706,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -48792,9 +48792,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -48805,8 +48805,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
@@ -48865,9 +48865,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -48878,8 +48878,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -48961,9 +48961,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -48974,8 +48974,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
@@ -49016,9 +49016,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -49029,8 +49029,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -49115,9 +49115,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -49128,8 +49128,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
@@ -50554,9 +50554,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -50567,8 +50567,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -50653,9 +50653,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -50666,8 +50666,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
@@ -50726,9 +50726,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -50739,8 +50739,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -50822,9 +50822,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -50835,8 +50835,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
@@ -50877,9 +50877,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -50890,8 +50890,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -50976,9 +50976,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -50989,8 +50989,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
@@ -51664,9 +51664,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -51677,8 +51677,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -51760,9 +51760,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -51773,8 +51773,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
diff --git a/testing/buildbot/chromium.memory.json b/testing/buildbot/chromium.memory.json
index 78c7fde..fc248d1 100644
--- a/testing/buildbot/chromium.memory.json
+++ b/testing/buildbot/chromium.memory.json
@@ -18497,12 +18497,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -18514,8 +18514,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -18605,12 +18605,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -18622,8 +18622,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
@@ -18689,12 +18689,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -18706,8 +18706,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -18794,12 +18794,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -18811,8 +18811,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
@@ -18855,12 +18855,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 111.0.5563.54",
+        "description": "Run with ash-chrome version 112.0.5615.18",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -18872,8 +18872,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5563.54",
-              "revision": "version:111.0.5563.54"
+              "location": "lacros_version_skew_tests_v112.0.5615.18",
+              "revision": "version:112.0.5615.18"
             }
           ],
           "dimension_sets": [
@@ -18963,12 +18963,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 110.0.5481.181",
+        "description": "Run with ash-chrome version 111.0.5563.71",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -18980,8 +18980,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v110.0.5481.181",
-              "revision": "version:110.0.5481.181"
+              "location": "lacros_version_skew_tests_v111.0.5563.71",
+              "revision": "version:111.0.5563.71"
             }
           ],
           "dimension_sets": [
diff --git a/testing/buildbot/variants.pyl b/testing/buildbot/variants.pyl
index a3e729a3..c130ddb 100644
--- a/testing/buildbot/variants.pyl
+++ b/testing/buildbot/variants.pyl
@@ -54,32 +54,32 @@
   },
   'LACROS_VERSION_SKEW_BETA': {
     'args': [
-      '--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.54/test_ash_chrome',
+      '--ash-chrome-path-override=../../lacros_version_skew_tests_v112.0.5615.18/test_ash_chrome',
     ],
-    'description': 'Run with ash-chrome version 111.0.5563.54',
+    'description': 'Run with ash-chrome version 112.0.5615.18',
     'identifier': 'Lacros version skew testing ash beta',
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/linux-ash-chromium/x86_64/ash.zip',
-          'location': 'lacros_version_skew_tests_v111.0.5563.54',
-          'revision': 'version:111.0.5563.54',
+          'location': 'lacros_version_skew_tests_v112.0.5615.18',
+          'revision': 'version:112.0.5615.18',
         },
       ],
     },
   },
   'LACROS_VERSION_SKEW_STABLE': {
     'args': [
-      '--ash-chrome-path-override=../../lacros_version_skew_tests_v110.0.5481.181/test_ash_chrome',
+      '--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5563.71/test_ash_chrome',
     ],
-    'description': 'Run with ash-chrome version 110.0.5481.181',
+    'description': 'Run with ash-chrome version 111.0.5563.71',
     'identifier': 'Lacros version skew testing ash stable',
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/linux-ash-chromium/x86_64/ash.zip',
-          'location': 'lacros_version_skew_tests_v110.0.5481.181',
-          'revision': 'version:110.0.5481.181',
+          'location': 'lacros_version_skew_tests_v111.0.5563.71',
+          'revision': 'version:111.0.5563.71',
         },
       ],
     },
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index cccdcc8b..92d2aeb 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -296,6 +296,21 @@
             ]
         }
     ],
+    "AndroidMetricsAsyncMetricLogging": [
+        {
+            "platforms": [
+                "android_webview"
+            ],
+            "experiments": [
+                {
+                    "name": "AndroidMetricsAsyncMetricLogging",
+                    "enable_features": [
+                        "AndroidMetricsAsyncMetricLogging"
+                    ]
+                }
+            ]
+        }
+    ],
     "AndroidOmniboxUxRevampPhase2": [
         {
             "platforms": [
@@ -560,36 +575,6 @@
             ]
         }
     ],
-    "ArcEnableTTSCacheSetupRelaunch": [
-        {
-            "platforms": [
-                "chromeos"
-            ],
-            "experiments": [
-                {
-                    "name": "Enabled",
-                    "enable_features": [
-                        "ArcEnableTTSCacheSetup"
-                    ]
-                }
-            ]
-        }
-    ],
-    "ArcEnableTTSCaching": [
-        {
-            "platforms": [
-                "chromeos"
-            ],
-            "experiments": [
-                {
-                    "name": "Enabled",
-                    "enable_features": [
-                        "ArcEnableTTSCaching"
-                    ]
-                }
-            ]
-        }
-    ],
     "ArcGameMode": [
         {
             "platforms": [
diff --git a/third_party/.gitignore b/third_party/.gitignore
index 8f47cc9..81a57c70 100644
--- a/third_party/.gitignore
+++ b/third_party/.gitignore
@@ -295,6 +295,7 @@
 /win_toolchain/.timestamps
 /win_toolchain/files
 /wix
+/wlcs/src
 /wuffs/src
 /xdg-utils
 /xnnpack/src
diff --git a/third_party/aosp_dalvik/3pp/3pp.pb b/third_party/aosp_dalvik/3pp/3pp.pb
index 0f0a1e0d..234d92ad 100644
--- a/third_party/aosp_dalvik/3pp/3pp.pb
+++ b/third_party/aosp_dalvik/3pp/3pp.pb
@@ -4,7 +4,7 @@
       download_url: "https://android.googlesource.com/platform/dalvik/+archive/090cb5952bab050da27003badb2d27e279e62115.tar.gz"
       version: "13.0.0_r24"
     }
-    patch_version: 'cr0'
+    patch_version: 'cr1'
     subdir: 'lib'
     unpack_archive: true
   }
diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc
index 805c82a..97b0125 100644
--- a/third_party/blink/common/features.cc
+++ b/third_party/blink/common/features.cc
@@ -463,9 +463,12 @@
 
 // Drop touch-end dispatch from `InputHandlerProxy` when all other touch-events
 // in current interaction sequence are dropeed.
+//
+// TODO(https://crbug.com/1417126): This is disabled because of a suspicious
+// flake in AR/XR tests.
 BASE_FEATURE(kDroppedTouchSequenceIncludesTouchEnd,
              "DroppedTouchSequenceIncludesTouchEnd",
-             base::FEATURE_ENABLED_BY_DEFAULT);
+             base::FEATURE_DISABLED_BY_DEFAULT);
 
 // File handling icons. https://crbug.com/1218213
 BASE_FEATURE(kFileHandlingIcons,
diff --git a/third_party/blink/public/common/tokens/multi_token.h b/third_party/blink/public/common/tokens/multi_token.h
index 64e901b..4b324945 100644
--- a/third_party/blink/public/common/tokens/multi_token.h
+++ b/third_party/blink/public/common/tokens/multi_token.h
@@ -15,6 +15,7 @@
 #include <limits>
 #include <type_traits>
 
+#include "base/types/variant_util.h"
 #include "base/unguessable_token.h"
 #include "third_party/abseil-cpp/absl/types/variant.h"
 #include "third_party/blink/public/common/tokens/multi_token_internal.h"
@@ -166,19 +167,13 @@
   // currently held.
   template <typename T, EnableIfIsSupportedToken<T> = 0>
   static constexpr Tag IndexOf() {
-    return static_cast<Tag>(
-        absl::variant<IndexOfHelper<Tokens>...>(IndexOfHelper<T>()).index());
+    return static_cast<Tag>(base::VariantIndexOfType<Storage, T>());
   }
 
   // Equivalent to `value().ToString()`.
   std::string ToString() const;
 
  private:
-  // Helper struct for IndexOf(); a `base::TokenType` is never usable as a
-  // literal type but an IndexOfHelper<base::TokenType> is.
-  template <typename T>
-  struct IndexOfHelper {};
-
   Storage storage_;
 };
 
diff --git a/third_party/blink/public/devtools_protocol/browser_protocol.pdl b/third_party/blink/public/devtools_protocol/browser_protocol.pdl
index f254e070..3e1f216 100644
--- a/third_party/blink/public/devtools_protocol/browser_protocol.pdl
+++ b/third_party/blink/public/devtools_protocol/browser_protocol.pdl
@@ -10900,7 +10900,19 @@
 
 # This domain allows interacting with the FedCM dialog.
 experimental domain FedCm
+  # Corresponds to IdentityRequestAccount
+  type Account extends object
+    properties
+      string accountId
+      string email
+      string name
+      string givenName
+      string pictureUrl
+      string idpConfigUrl
+
   event dialogShown
+    parameters
+      array of Account accounts
 
   command enable
   command disable
diff --git a/third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom b/third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom
index 73450fcd..f12dafb 100644
--- a/third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom
+++ b/third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom
@@ -3828,6 +3828,12 @@
   kPrivateAggregationApiFledgeExtensions = 4487,
   kDeprecatedInterestGroupDailyUpdateUrl = 4488,
   kCSSColorGradientColorSpace = 4489,
+  kDanglingMarkupInWindowName = 4490,
+  kDanglingMarkupInWindowNameNotEndsWithNewLineOrGT = 4491,
+  kDanglingMarkupInWindowNameNotEndsWithGT = 4492,
+  kDanglingMarkupInTarget = 4493,
+  kDanglingMarkupInTargetNotEndsWithGT = 4494,
+  kDanglingMarkupInTargetNotEndsWithNewLineOrGT = 4495,
 
   // Add new features immediately above this line. Don't change assigned
   // numbers of any item, and don't reuse removed slots.
diff --git a/third_party/blink/renderer/README.md b/third_party/blink/renderer/README.md
index 30b91738..1e3494eb 100644
--- a/third_party/blink/renderer/README.md
+++ b/third_party/blink/renderer/README.md
@@ -14,7 +14,7 @@
 and should not be used outside of it. Use [Blink's public API](../public)
 in code outside of Blink.
 
-### `core/`
+### [`core/`](core/README.md)
 
 The `core/` directory implements the essence of the Web Platform defined by specs
 and IDL interfaces. Due to historical reasons, `core/` contains a lot of features with
@@ -35,7 +35,7 @@
 
 For example, `modules/crypto` implements WebCrypto API.
 
-### `platform/`
+### [`platform/`](platform/README.md)
 
 The `platform/` directory is a collection of lower level features of Blink that are factored
 out of a monolithic `core/`. These features follow the same principles as `modules/`,
diff --git a/third_party/blink/renderer/bindings/idl_in_modules.gni b/third_party/blink/renderer/bindings/idl_in_modules.gni
index a34c04e..82036b3 100644
--- a/third_party/blink/renderer/bindings/idl_in_modules.gni
+++ b/third_party/blink/renderer/bindings/idl_in_modules.gni
@@ -651,6 +651,7 @@
           "//third_party/blink/renderer/modules/sanitizer_api/sanitizer.idl",
           "//third_party/blink/renderer/modules/sanitizer_api/sanitizer_config.idl",
           "//third_party/blink/renderer/modules/scheduler/scheduler.idl",
+          "//third_party/blink/renderer/modules/scheduler/window_idle_tasks.idl",
           "//third_party/blink/renderer/modules/scheduler/scheduler_post_task_callback.idl",
           "//third_party/blink/renderer/modules/scheduler/scheduler_post_task_options.idl",
           "//third_party/blink/renderer/modules/scheduler/script_wrappable_task_state.idl",
diff --git a/third_party/blink/renderer/core/css/properties/shorthands/shorthands_custom.cc b/third_party/blink/renderer/core/css/properties/shorthands/shorthands_custom.cc
index 5d00dee..a531c0c 100644
--- a/third_party/blink/renderer/core/css/properties/shorthands/shorthands_custom.cc
+++ b/third_party/blink/renderer/core/css/properties/shorthands/shorthands_custom.cc
@@ -128,14 +128,6 @@
           CSSTimingData::GetRepeated(animation_data->TimingFunctionList(), i)));
       list->Append(*ComputedStyleUtils::ValueForAnimationDelayStart(
           CSSTimingData::GetRepeated(animation_data->DelayStartList(), i)));
-      if (CSSAnimationData::InitialDelayEnd() !=
-          CSSTimingData::GetRepeated(animation_data->DelayEndList(), i)) {
-        DCHECK_EQ(shorthand.length(), 10u);
-        DCHECK_EQ(shorthand.properties()[3]->PropertyID(),
-                  CSSPropertyID::kAnimationDelayEnd);
-        list->Append(*ComputedStyleUtils::ValueForAnimationDelayEnd(
-            CSSTimingData::GetRepeated(animation_data->DelayEndList(), i)));
-      }
       list->Append(*ComputedStyleUtils::ValueForAnimationIterationCount(
           CSSTimingData::GetRepeated(animation_data->IterationCountList(), i)));
       list->Append(*ComputedStyleUtils::ValueForAnimationDirection(
@@ -146,16 +138,16 @@
           CSSTimingData::GetRepeated(animation_data->PlayStateList(), i)));
       list->Append(*MakeGarbageCollected<CSSCustomIdentValue>(
           animation_data->NameList()[i]));
-      // When serializing shorthands, a component value must be omitted
-      // if doesn't change the meaning of the overall value.
-      // https://drafts.csswg.org/cssom/#serializing-css-values
+      // The shorthand can not represent the following properties if they have
+      // non-initial values. This is because they are always reset to their
+      // initial value by the shorthand.
       if (CSSAnimationData::InitialTimeline() !=
           animation_data->GetTimeline(i)) {
-        DCHECK_EQ(shorthand.length(), 10u);
-        DCHECK_EQ(shorthand.properties()[9]->PropertyID(),
-                  CSSPropertyID::kAnimationTimeline);
-        list->Append(*ComputedStyleUtils::ValueForAnimationTimeline(
-            animation_data->GetTimeline(i)));
+        return nullptr;
+      }
+      if (CSSAnimationData::InitialDelayEnd() !=
+          CSSTimingData::GetRepeated(animation_data->DelayEndList(), i)) {
+        return nullptr;
       }
       animations_list->Append(*list);
     }
diff --git a/third_party/blink/renderer/core/css/style_property_serializer.cc b/third_party/blink/renderer/core/css/style_property_serializer.cc
index fd37849..49517a5c 100644
--- a/third_party/blink/renderer/core/css/style_property_serializer.cc
+++ b/third_party/blink/renderer/core/css/style_property_serializer.cc
@@ -1536,22 +1536,24 @@
 
       bool is_initial_value = value->IsInitialValue();
 
-      // When serializing shorthands, a component value must be omitted
-      // if doesn't change the meaning of the overall value.
-      // https://drafts.csswg.org/cssom/#serializing-css-values
+      // The shorthand can not represent the following properties if they have
+      // non-initial values. This is because they are always reset to their
+      // initial value by the shorthand.
       if (property->IDEquals(CSSPropertyID::kAnimationTimeline)) {
-        if (auto* ident = DynamicTo<CSSIdentifierValue>(value)) {
-          if (ident->GetValueID() ==
-              CSSAnimationData::InitialTimeline().GetKeyword()) {
-            DCHECK(RuntimeEnabledFeatures::CSSScrollTimelineEnabled());
-            is_initial_value = true;
-          }
+        auto* ident = DynamicTo<CSSIdentifierValue>(value);
+        if (!ident || (ident->GetValueID() !=
+                       CSSAnimationData::InitialTimeline().GetKeyword())) {
+          DCHECK(RuntimeEnabledFeatures::CSSScrollTimelineEnabled());
+          return g_empty_string;
         }
+        is_initial_value = true;
       }
-
       if (property->IDEquals(CSSPropertyID::kAnimationDelayEnd)) {
-        is_initial_value = CSSToStyleMap::MapAnimationDelayEnd(*value) ==
-                           CSSTimingData::InitialDelayEnd();
+        if (CSSToStyleMap::MapAnimationDelayEnd(*value) !=
+            CSSTimingData::InitialDelayEnd()) {
+          return g_empty_string;
+        }
+        is_initial_value = true;
       }
 
       if (!is_initial_value) {
diff --git a/third_party/blink/renderer/core/display_lock/display_lock_context.cc b/third_party/blink/renderer/core/display_lock/display_lock_context.cc
index 5e20b62d..81a9808 100644
--- a/third_party/blink/renderer/core/display_lock/display_lock_context.cc
+++ b/third_party/blink/renderer/core/display_lock/display_lock_context.cc
@@ -1044,6 +1044,7 @@
     if (!object_element->UseFallbackContent())
       return nullptr;
   } else if (IsA<HTMLImageElement>(*element_) ||
+             IsA<HTMLCanvasElement>(*element_) ||
              (element_->IsFormControlElement() &&
               !element_->IsOutputElement()) ||
              element_->IsMediaElement() || element_->IsFrameOwnerElement() ||
diff --git a/third_party/blink/renderer/core/dom/element.cc b/third_party/blink/renderer/core/dom/element.cc
index c1ee4bcd..8ee4be4 100644
--- a/third_party/blink/renderer/core/dom/element.cc
+++ b/third_party/blink/renderer/core/dom/element.cc
@@ -2836,6 +2836,7 @@
     if (UNLIKELY(HasUndoStack())) {
       frame->GetEditor().GetUndoStack().ElementRemoved(this);
     }
+    frame->GetEditor().ElementRemoved(this);
     frame->GetEventHandler().ElementRemoved(this);
   }
 }
diff --git a/third_party/blink/renderer/core/dom/scripted_idle_task_controller.cc b/third_party/blink/renderer/core/dom/scripted_idle_task_controller.cc
index 2f5f9b57..5925eca 100644
--- a/third_party/blink/renderer/core/dom/scripted_idle_task_controller.cc
+++ b/third_party/blink/renderer/core/dom/scripted_idle_task_controller.cc
@@ -75,17 +75,6 @@
 
 }  // namespace internal
 
-V8IdleTask::V8IdleTask(V8IdleRequestCallback* callback) : callback_(callback) {}
-
-void V8IdleTask::Trace(Visitor* visitor) const {
-  visitor->Trace(callback_);
-  IdleTask::Trace(visitor);
-}
-
-void V8IdleTask::invoke(IdleDeadline* deadline) {
-  callback_->InvokeAndReportException(nullptr, deadline);
-}
-
 ScriptedIdleTaskController::ScriptedIdleTaskController(
     ExecutionContext* context)
     : ExecutionContextLifecycleStateObserver(context),
diff --git a/third_party/blink/renderer/core/dom/scripted_idle_task_controller.h b/third_party/blink/renderer/core/dom/scripted_idle_task_controller.h
index 930b5e2..fe2e38db 100644
--- a/third_party/blink/renderer/core/dom/scripted_idle_task_controller.h
+++ b/third_party/blink/renderer/core/dom/scripted_idle_task_controller.h
@@ -5,7 +5,6 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_DOM_SCRIPTED_IDLE_TASK_CONTROLLER_H_
 #define THIRD_PARTY_BLINK_RENDERER_CORE_DOM_SCRIPTED_IDLE_TASK_CONTROLLER_H_
 
-#include "third_party/blink/renderer/bindings/core/v8/v8_idle_request_callback.h"
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/dom/idle_deadline.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context_lifecycle_state_observer.h"
@@ -25,9 +24,10 @@
 class IdleRequestOptions;
 class ThreadScheduler;
 
-// |IdleTask| is an interface type which generalizes tasks which are invoked
-// on idle. The tasks need to define what to do on idle in |invoke|.
-class IdleTask : public GarbageCollected<IdleTask>, public NameClient {
+// `IdleTask` is an interface type which generalizes tasks which are invoked
+// on idle. The tasks need to define what to do on idle in `invoke`.
+class CORE_EXPORT IdleTask : public GarbageCollected<IdleTask>,
+                             public NameClient {
  public:
   virtual void Trace(Visitor* visitor) const {}
   const char* NameInHeapSnapshot() const override { return "IdleTask"; }
@@ -39,24 +39,10 @@
   probe::AsyncTaskContext async_task_context_;
 };
 
-// |V8IdleTask| is the adapter class for the conversion from
-// |V8IdleRequestCallback| to |IdleTask|.
-class V8IdleTask : public IdleTask {
- public:
-  static V8IdleTask* Create(V8IdleRequestCallback* callback) {
-    return MakeGarbageCollected<V8IdleTask>(callback);
-  }
-
-  explicit V8IdleTask(V8IdleRequestCallback*);
-  ~V8IdleTask() override = default;
-
-  void invoke(IdleDeadline*) override;
-  void Trace(Visitor*) const override;
-
- private:
-  Member<V8IdleRequestCallback> callback_;
-};
-
+// `ScriptedIdleTaskController` manages scheduling and running `IdleTask`s. This
+// provides some higher level functionality on top of the thread scheduler's
+// idle tasks, e.g. timeouts and providing an `IdleDeadline` to callbacks, which
+// is used both by the requestIdleCallback API and internally in blink.
 class CORE_EXPORT ScriptedIdleTaskController
     : public GarbageCollected<ScriptedIdleTaskController>,
       public ExecutionContextLifecycleStateObserver,
diff --git a/third_party/blink/renderer/core/editing/editor.cc b/third_party/blink/renderer/core/editing/editor.cc
index 8be6be2..1648ab9 100644
--- a/third_party/blink/renderer/core/editing/editor.cc
+++ b/third_party/blink/renderer/core/editing/editor.cc
@@ -948,6 +948,16 @@
                            InputEvent::InputType::kInsertReplacementText);
 }
 
+void Editor::ElementRemoved(Element* element) {
+  if (!RuntimeEnabledFeatures::DontLeakDetachedInputEnabled()) {
+    return;
+  }
+  if (last_edit_command_ &&
+      last_edit_command_->EndingSelection().RootEditableElement() == element) {
+    last_edit_command_ = nullptr;
+  }
+}
+
 void Editor::Trace(Visitor* visitor) const {
   visitor->Trace(frame_);
   visitor->Trace(last_edit_command_);
diff --git a/third_party/blink/renderer/core/editing/editor.h b/third_party/blink/renderer/core/editing/editor.h
index e5f9571..20141e8 100644
--- a/third_party/blink/renderer/core/editing/editor.h
+++ b/third_party/blink/renderer/core/editing/editor.h
@@ -147,6 +147,8 @@
 
   void SetStartNewKillRingSequence(bool);
 
+  void ElementRemoved(Element* element);
+
   void Clear();
 
   SelectionInDOMTree SelectionForCommand(Event*);
diff --git a/third_party/blink/renderer/core/frame/local_dom_window.cc b/third_party/blink/renderer/core/frame/local_dom_window.cc
index aa977dbc..fee3b48e 100644
--- a/third_party/blink/renderer/core/frame/local_dom_window.cc
+++ b/third_party/blink/renderer/core/frame/local_dom_window.cc
@@ -1964,18 +1964,6 @@
   return GetAgent()->IsOriginKeyed();
 }
 
-int LocalDOMWindow::requestIdleCallback(V8IdleRequestCallback* callback,
-                                        const IdleRequestOptions* options) {
-  SetCurrentTaskAsCallbackParent(callback);
-  if (!GetFrame())
-    return 0;
-  return document_->RequestIdleCallback(V8IdleTask::Create(callback), options);
-}
-
-void LocalDOMWindow::cancelIdleCallback(int id) {
-  document()->CancelIdleCallback(id);
-}
-
 CustomElementRegistry* LocalDOMWindow::customElements(
     ScriptState* script_state) const {
   if (!script_state->World().IsMainWorld())
diff --git a/third_party/blink/renderer/core/frame/local_dom_window.h b/third_party/blink/renderer/core/frame/local_dom_window.h
index 6d4acf5..03356ba 100644
--- a/third_party/blink/renderer/core/frame/local_dom_window.h
+++ b/third_party/blink/renderer/core/frame/local_dom_window.h
@@ -74,7 +74,6 @@
 class Fence;
 class FrameConsole;
 class History;
-class IdleRequestOptions;
 class InputMethodController;
 class LocalFrame;
 class MediaQueryList;
@@ -93,7 +92,6 @@
 class StyleMedia;
 class TrustedTypePolicyFactory;
 class V8FrameRequestCallback;
-class V8IdleRequestCallback;
 class V8VoidFunction;
 struct WebPictureInPictureWindowOptions;
 class WindowAgent;
@@ -345,10 +343,6 @@
   // https://html.spec.whatwg.org/C/#dom-originagentcluster
   bool originAgentCluster() const;
 
-  // Idle callback extensions
-  int requestIdleCallback(V8IdleRequestCallback*, const IdleRequestOptions*);
-  void cancelIdleCallback(int id);
-
   // Custom elements
   CustomElementRegistry* customElements(ScriptState*) const;
   CustomElementRegistry* customElements() const;
diff --git a/third_party/blink/renderer/core/frame/window.idl b/third_party/blink/renderer/core/frame/window.idl
index cff8b45..4da19b8 100644
--- a/third_party/blink/renderer/core/frame/window.idl
+++ b/third_party/blink/renderer/core/frame/window.idl
@@ -108,11 +108,6 @@
 
     [Replaceable, SameObject] readonly attribute External external;
 
-    // Cooperative Scheduling of Background Tasks
-    // https://www.w3.org/TR/requestidlecallback/#window_extensions
-    [Measure] long requestIdleCallback(IdleRequestCallback callback, optional IdleRequestOptions options = {});
-    void cancelIdleCallback(long handle);
-
     // CSS Object Model (CSSOM)
     // https://drafts.csswg.org/cssom/#extensions-to-the-window-interface
     [Affects=Nothing, NewObject] CSSStyleDeclaration getComputedStyle(Element elt, optional DOMString? pseudoElt);
diff --git a/third_party/blink/renderer/core/html/html_iframe_element.cc b/third_party/blink/renderer/core/html/html_iframe_element.cc
index 93308e2..0075c90 100644
--- a/third_party/blink/renderer/core/html/html_iframe_element.cc
+++ b/third_party/blink/renderer/core/html/html_iframe_element.cc
@@ -190,6 +190,18 @@
       UseCounter::Count(GetDocument(), WebFeature::kFrameNameContainsNewline);
     if (name_.Contains('<'))
       UseCounter::Count(GetDocument(), WebFeature::kFrameNameContainsBrace);
+    if (name_.Contains('\n') && name_.Contains('<')) {
+      UseCounter::Count(GetDocument(), WebFeature::kDanglingMarkupInWindowName);
+      if (!name_.EndsWith('>')) {
+        UseCounter::Count(GetDocument(),
+                          WebFeature::kDanglingMarkupInWindowNameNotEndsWithGT);
+        if (!name_.EndsWith('\n')) {
+          UseCounter::Count(
+              GetDocument(),
+              WebFeature::kDanglingMarkupInWindowNameNotEndsWithNewLineOrGT);
+        }
+      }
+    }
   } else if (name == html_names::kSandboxAttr) {
     sandbox_->DidUpdateAttributeValue(params.old_value, value);
 
diff --git a/third_party/blink/renderer/core/layout/layout_html_canvas.cc b/third_party/blink/renderer/core/layout/layout_html_canvas.cc
index 097f167..9f88dd2 100644
--- a/third_party/blink/renderer/core/layout/layout_html_canvas.cc
+++ b/third_party/blink/renderer/core/layout/layout_html_canvas.cc
@@ -43,6 +43,9 @@
 void LayoutHTMLCanvas::PaintReplaced(const PaintInfo& paint_info,
                                      const PhysicalOffset& paint_offset) const {
   NOT_DESTROYED();
+  if (ChildPaintBlockedByDisplayLock()) {
+    return;
+  }
   HTMLCanvasPainter(*this).PaintReplaced(paint_info, paint_offset);
 }
 
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_line_resolver.cc b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_line_resolver.cc
index 130f767..91aa9f1 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_line_resolver.cc
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_line_resolver.cc
@@ -7,6 +7,7 @@
 #include <algorithm>
 #include "third_party/blink/renderer/core/layout/ng/grid/ng_grid_data.h"
 #include "third_party/blink/renderer/core/layout/ng/grid/ng_grid_named_line_collection.h"
+#include "third_party/blink/renderer/core/style/computed_grid_track_list.h"
 #include "third_party/blink/renderer/core/style/computed_style.h"
 #include "third_party/blink/renderer/core/style/grid_area.h"
 #include "third_party/blink/renderer/core/style/grid_position.h"
@@ -48,13 +49,17 @@
   if (subgrid_area.rows.IsTranslatedDefinite())
     subgridded_row_span_size_ = subgrid_area.SpanSize(kForRows);
 
+  // TODO(kschmi) - use a collector design (similar to
+  // `OrderedNamedLinesCollector`) to collect all of the lines first and then
+  // do a single step of filtering and adding them to the subgrid list. Also
+  // consider moving these to class methods.
   auto MergeNamedGridLinesWithParent =
       [](NamedGridLinesMap& subgrid_map, const NamedGridLinesMap& parent_map,
          GridSpan subgrid_span, bool is_opposite_direction_to_parent) -> void {
     // Update `subgrid_map` to a merged map from a parent grid or subgrid map
     // (`parent_map`). The map is a key-value store with keys as the line name
     // and the value as an array of ascending indices.
-    for (auto& pair : parent_map) {
+    for (const auto& pair : parent_map) {
       // TODO(kschmi) : Benchmark whether this is faster with an std::map, which
       // would eliminate the need for sorting and removing duplicates below.
       // Perf will vary based on the number of named lines defined.
@@ -78,9 +83,12 @@
       // to index 0 and don't need to be offset.
       const auto& existing_entry = subgrid_map.find(pair.key);
       if (existing_entry != subgrid_map.end()) {
-        for (auto& value : existing_entry->value)
+        for (const auto& value : existing_entry->value) {
           merged_list.push_back(value);
+        }
 
+        // TODO(kschmi): Reverse the list if `is_opposite_direction_to_parent`
+        // and there was no existing entry, as it will be sorted backwards.
         std::sort(merged_list.begin(), merged_list.end());
 
         // Remove any duplicated entries in the sorted list. Duplicates can
@@ -97,16 +105,13 @@
                           merged_list.end());
       }
 
-      // Override the existing subgrid's line names map with the new merged list
-      // for this particular line name entry. If `merged_list` list is empty,
-      // (this can happen when all entries for a particular line name are out
-      // of the subgrid range), erase the entry entirely, as
-      // `NGGridNamedLineCollection` doesn't support named line entries without
-      // values.
-      if (merged_list.empty())
-        subgrid_map.erase(pair.key);
-      else
+      // Override the existing subgrid's line names map with the new merged
+      // list for this particular line name entry. `merged_list` list can be
+      // empty if all entries for a particular line name are out of the
+      // subgrid range.
+      if (!merged_list.empty()) {
         subgrid_map.Set(pair.key, merged_list);
+      }
     }
   };
   auto MergeImplicitLinesWithParent = [&](NamedGridLinesMap& subgrid_map,
@@ -116,7 +121,7 @@
     // First, clamp the existing `subgrid_map` to the subgrid range before
     // merging. These are positive and relative to index 0, so we only need to
     // clamp values above `subgrid_span_size`.
-    for (auto& pair : subgrid_map) {
+    for (const auto& pair : subgrid_map) {
       WTF::Vector<wtf_size_t> clamped_list;
       for (const auto& position : pair.value) {
         if (position > subgrid_span_size)
@@ -130,7 +135,7 @@
     // Update `subgrid_map` to a merged map from a parent grid or subgrid map
     // (`parent_map`). The map is a key-value store with keys as the implicit
     // line name and the value as an array of ascending indices.
-    for (auto& pair : parent_map) {
+    for (const auto& pair : parent_map) {
       WTF::Vector<wtf_size_t> merged_list;
       for (const auto& position : pair.value) {
         auto IsGridAreaInSubgridRange = [&]() -> bool {
@@ -163,7 +168,8 @@
               const auto& opposite_line_entry =
                   parent_map.find(opposite_line_name);
               if (opposite_line_entry != parent_map.end()) {
-                for (auto& opposite_position : opposite_line_entry->value) {
+                for (const auto& opposite_position :
+                     opposite_line_entry->value) {
                   if (subgrid_span.Contains(opposite_position))
                     return true;
                 }
@@ -198,21 +204,107 @@
         // index 0 and don't need to be offset.
         const auto& existing_entry = subgrid_map.find(pair.key);
         if (existing_entry != subgrid_map.end()) {
-          for (auto& value : existing_entry->value)
+          for (const auto& value : existing_entry->value) {
             merged_list.push_back(value);
+          }
           std::sort(merged_list.begin(), merged_list.end());
         }
 
         // Override the existing subgrid's line names map with the new merged
-        // list for this particular line name entry. If `merged_list` list is
-        // empty, (this can happen when all entries for a particular line name
-        // are out of the subgrid range), erase the entry entirely, as
-        // `NGGridNamedLineCollection` doesn't support named line entries
-        // without values.
-        if (merged_list.empty())
-          subgrid_map.erase(pair.key);
-        else
+        // list for this particular line name entry. `merged_list` list can be
+        // empty if all entries for a particular line name are out of the
+        // subgrid range.
+        if (!merged_list.empty()) {
           subgrid_map.Set(pair.key, merged_list);
+        }
+      }
+    }
+  };
+  auto ExpandAutoRepeatTracksFromParent =
+      [](NamedGridLinesMap& subgrid_map,
+         const NamedGridLinesMap& parent_auto_repeat_map,
+         const blink::ComputedGridTrackList& track_list, GridSpan subgrid_span,
+         wtf_size_t auto_repetitions,
+         bool is_opposite_direction_to_parent) -> void {
+    const wtf_size_t auto_repeat_track_count =
+        track_list.TrackList().AutoRepeatTrackCount();
+    const wtf_size_t auto_repeat_total_tracks =
+        auto_repeat_track_count * auto_repetitions;
+    if (auto_repeat_total_tracks == 0) {
+      return;
+    }
+
+    // First, we need to offset the existing (non auto repeat) line names that
+    // come after the auto repeater. This is because they were parsed without
+    // knowledge of the number of repeats. Now that we know how many auto
+    // repeats there are, we need to shift the existing entries by the total
+    // number of auto repeat tracks.
+    // TODO(kschmi): Do we also need to do this for implicit lines?
+    const wtf_size_t insertion_point = track_list.auto_repeat_insertion_point;
+    const wtf_size_t last_auto_repeat_index =
+        insertion_point + auto_repeat_total_tracks;
+    for (const auto& pair : subgrid_map) {
+      WTF::Vector<wtf_size_t> shifted_list;
+      for (const auto& position : pair.value) {
+        if (position >= insertion_point) {
+          wtf_size_t expanded_position = position + last_auto_repeat_index;
+          // These have already been offset relative to index 0, so explicitly
+          // do not offset by `subgrid_span` like we do below.
+          if (subgrid_span.Contains(expanded_position)) {
+            shifted_list.push_back(expanded_position);
+          }
+        }
+      }
+      subgrid_map.Set(pair.key, shifted_list);
+    }
+
+    // Now expand the auto repeaters into `subgrid_map`.
+    for (const auto& pair : parent_auto_repeat_map) {
+      Vector<wtf_size_t, 16> merged_list;
+      for (const auto& position : pair.value) {
+        // The outer loop is the number of repeats.
+        for (wtf_size_t i = 0; i < auto_repetitions; ++i) {
+          // The inner loop expands out a single repeater.
+          for (wtf_size_t j = 0; j < auto_repeat_track_count; ++j) {
+            // The expanded position always starts at the insertion point, then
+            // factors in the line name index, incremented by both auto repeat
+            // loops.
+            wtf_size_t expanded_position = insertion_point + position + i + j;
+
+            // Filter out parent named lines that are out of the subgrid range.
+            // Also offset entries by `subgrid_start_line` before inserting them
+            // into the merged map so they are all relative to offset 0. These
+            // are already in ascending order so there's no need to sort.
+            if (subgrid_span.Contains(expanded_position)) {
+              if (is_opposite_direction_to_parent) {
+                merged_list.push_back(subgrid_span.EndLine() -
+                                      expanded_position);
+              } else {
+                merged_list.push_back(expanded_position -
+                                      subgrid_span.StartLine());
+              }
+            }
+          }
+        }
+
+        // If there's a name collision, merge the values and sort. These are
+        // from the subgrid and not the parent, so they are already relative to
+        // index 0 and don't need to be offset.
+        const auto& existing_entry = subgrid_map.find(pair.key);
+        if (existing_entry != subgrid_map.end()) {
+          for (const auto& value : existing_entry->value) {
+            merged_list.push_back(value);
+          }
+          // TODO(kschmi): Reverse the list if `is_opposite_direction_to_parent`
+          // and there was no existing entry, as it will be sorted backwards.
+          std::sort(merged_list.begin(), merged_list.end());
+        }
+
+        // If the merged list is empty, it means that all of the entries from
+        // the parent were out of the subgrid range.
+        if (!merged_list.empty()) {
+          subgrid_map.Set(pair.key, merged_list);
+        }
       }
     }
   };
@@ -230,6 +322,13 @@
         *subgridded_columns_merged_implicit_grid_line_names_,
         parent_line_resolver.ImplicitNamedLinesMap(kForColumns),
         subgrid_area.columns);
+    // Expand auto repeaters from the parent into the named line map.
+    ExpandAutoRepeatTracksFromParent(
+        *subgridded_columns_merged_explicit_grid_line_names_,
+        parent_line_resolver.AutoRepeatLineNamesMap(kForColumns),
+        parent_line_resolver.ComputedGridTrackList(kForColumns),
+        subgrid_area.columns, parent_line_resolver.AutoRepetitions(kForColumns),
+        is_opposite_direction_to_parent);
   }
   if (subgrid_area.rows.IsTranslatedDefinite()) {
     MergeNamedGridLinesWithParent(
@@ -240,6 +339,13 @@
         *subgridded_rows_merged_implicit_grid_line_names_,
         parent_line_resolver.ImplicitNamedLinesMap(kForRows),
         subgrid_area.rows);
+    // Expand auto repeaters from the parent into the named line map.
+    ExpandAutoRepeatTracksFromParent(
+        *subgridded_rows_merged_explicit_grid_line_names_,
+        parent_line_resolver.AutoRepeatLineNamesMap(kForRows),
+        parent_line_resolver.ComputedGridTrackList(kForRows), subgrid_area.rows,
+        parent_line_resolver.AutoRepetitions(kForRows),
+        is_opposite_direction_to_parent);
   }
 }
 
@@ -497,6 +603,13 @@
              : ComputedGridTrackList(track_direction).named_grid_lines;
 }
 
+const NamedGridLinesMap& NGGridLineResolver::AutoRepeatLineNamesMap(
+    GridTrackSizingDirection track_direction) const {
+  // Auto repeat line names always come from the style object, as they get
+  // merged into the explicit line names map for subgrids.
+  return ComputedGridTrackList(track_direction).auto_repeat_named_grid_lines;
+}
+
 const blink::ComputedGridTrackList& NGGridLineResolver::ComputedGridTrackList(
     GridTrackSizingDirection track_direction) const {
   // TODO(kschmi): Refactor so this isn't necessary and handle auto-repeats
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_line_resolver.h b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_line_resolver.h
index b07aaa9..1970a8d 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_line_resolver.h
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_line_resolver.h
@@ -76,6 +76,9 @@
   const NamedGridLinesMap& ExplicitNamedLinesMap(
       GridTrackSizingDirection track_direction) const;
 
+  const NamedGridLinesMap& AutoRepeatLineNamesMap(
+      GridTrackSizingDirection track_direction) const;
+
   const blink::ComputedGridTrackList& ComputedGridTrackList(
       GridTrackSizingDirection track_direction) const;
 
diff --git a/third_party/blink/renderer/core/page/frame_tree.cc b/third_party/blink/renderer/core/page/frame_tree.cc
index 7c0ce10..198c449 100644
--- a/third_party/blink/renderer/core/page/frame_tree.cc
+++ b/third_party/blink/renderer/core/page/frame_tree.cc
@@ -42,6 +42,29 @@
 
 const unsigned kInvalidChildCount = ~0U;
 
+void LogDanglingMarkupHistogram(Document* document, const AtomicString& name) {
+  document->CountUse(WebFeature::kDanglingMarkupInTarget);
+  if (!name.EndsWith('>')) {
+    document->CountUse(WebFeature::kDanglingMarkupInTargetNotEndsWithGT);
+    if (!name.EndsWith('\n')) {
+      document->CountUse(
+          WebFeature::kDanglingMarkupInTargetNotEndsWithNewLineOrGT);
+    }
+  }
+}
+
+bool ContainsNewLineAndLessThan(const AtomicString& name) {
+  return name.Contains('\n') && name.Contains('<');
+}
+
+bool IsRequestFromHtml(FrameLoadRequest& request) {
+  return request.ClientRedirectReason() ==
+             ClientNavigationReason::kFormSubmissionGet ||
+         request.ClientRedirectReason() ==
+             ClientNavigationReason::kFormSubmissionPost ||
+         request.ClientRedirectReason() == ClientNavigationReason::kAnchorClick;
+}
+
 }  // namespace
 
 FrameTree::FrameTree(Frame* this_frame)
@@ -210,6 +233,12 @@
   if (request.GetNavigationPolicy() != kNavigationPolicyCurrentTab)
     return FindResult(current_frame, false);
 
+  // Log use counters if the name contains both '\n' and '<'.
+  if (ContainsNewLineAndLessThan(name) && IsRequestFromHtml(request) &&
+      current_frame->GetDocument()) {
+    LogDanglingMarkupHistogram(current_frame->GetDocument(), name);
+  }
+
   const KURL& url = request.GetResourceRequest().Url();
   Frame* frame = FindFrameForNavigationInternal(name, url, &request);
   bool new_window = false;
diff --git a/third_party/blink/renderer/core/page/scrolling/scrolling_test.cc b/third_party/blink/renderer/core/page/scrolling/scrolling_test.cc
index c0b4b5bf..359a7dc3 100644
--- a/third_party/blink/renderer/core/page/scrolling/scrolling_test.cc
+++ b/third_party/blink/renderer/core/page/scrolling/scrolling_test.cc
@@ -2496,9 +2496,8 @@
   EXPECT_EQ(nullptr, ScrollableAreaByDOMElementId("displaynone"));
 }
 
-// Tests that the compositor gets a scroll node for scrollable input boxes,
-// which are unique as they are not a composited scroller but also do not have
-// NonCompositedMainThreadScrollingReasons.
+// Tests that the compositor gets a scroll node for a non-composited (due to
+// non-opaque background) scrollable input box.
 TEST_P(UnifiedScrollingSimTest, ScrollNodeForInputBox) {
   // This test requires scroll unification.
   if (!base::FeatureList::IsEnabled(::features::kScrollUnification))
@@ -2518,7 +2517,8 @@
   Compositor().BeginFrame();
 
   auto* scrollable_area = ScrollableAreaByDOMElementId("textinput");
-  ASSERT_EQ(0u, scrollable_area->GetNonCompositedMainThreadScrollingReasons());
+  ASSERT_EQ(cc::MainThreadScrollingReason::kNotOpaqueForTextAndLCDText,
+            scrollable_area->GetNonCompositedMainThreadScrollingReasons());
 
   const auto* scroll_node = ScrollNodeForScrollableArea(scrollable_area);
   ASSERT_TRUE(scroll_node);
diff --git a/third_party/blink/renderer/core/paint/paint_layer_scrollable_area.cc b/third_party/blink/renderer/core/paint/paint_layer_scrollable_area.cc
index 57f7b86..f44f9df 100644
--- a/third_party/blink/renderer/core/paint/paint_layer_scrollable_area.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer_scrollable_area.cc
@@ -2456,20 +2456,6 @@
   return !properties->ScrollTranslation()->HasDirectCompositingReasons();
 }
 
-static bool LayerNodeMayNeedCompositedScrolling(const PaintLayer* layer) {
-  // Don't force composite scroll for select or text input elements.
-  if (Node* node = layer->GetLayoutObject().GetNode()) {
-    if (IsA<HTMLSelectElement>(node))
-      return false;
-    if (TextControlElement* text_control = EnclosingTextControl(node)) {
-      if (IsA<HTMLInputElement>(text_control)) {
-        return false;
-      }
-    }
-  }
-  return true;
-}
-
 bool PaintLayerScrollableArea::ComputeNeedsCompositedScrolling(
     bool force_prefer_compositing_to_lcd_text) {
   const auto* box = GetLayoutBox();
@@ -2502,8 +2488,7 @@
   }
 
   if (!force_prefer_compositing_to_lcd_text &&
-      (RuntimeEnabledFeatures::PreferNonCompositedScrollingEnabled() ||
-       !LayerNodeMayNeedCompositedScrolling(layer_))) {
+      RuntimeEnabledFeatures::PreferNonCompositedScrollingEnabled()) {
     return false;
   }
 
diff --git a/third_party/blink/renderer/core/speculation_rules/build.gni b/third_party/blink/renderer/core/speculation_rules/build.gni
index af1eb80..2386349 100644
--- a/third_party/blink/renderer/core/speculation_rules/build.gni
+++ b/third_party/blink/renderer/core/speculation_rules/build.gni
@@ -13,6 +13,8 @@
   "speculation_rule.h",
   "speculation_rule_set.cc",
   "speculation_rule_set.h",
+  "speculation_rules_features.cc",
+  "speculation_rules_features.h",
   "speculation_rules_header.cc",
   "speculation_rules_header.h",
   "speculation_rules_metrics.cc",
diff --git a/third_party/blink/renderer/core/speculation_rules/document_rule_predicate.cc b/third_party/blink/renderer/core/speculation_rules/document_rule_predicate.cc
index 95d8bf7..6c4cd39 100644
--- a/third_party/blink/renderer/core/speculation_rules/document_rule_predicate.cc
+++ b/third_party/blink/renderer/core/speculation_rules/document_rule_predicate.cc
@@ -14,6 +14,7 @@
 #include "third_party/blink/renderer/core/dom/node_computed_style.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
 #include "third_party/blink/renderer/core/html/html_anchor_element.h"
+#include "third_party/blink/renderer/core/speculation_rules/speculation_rules_features.h"
 #include "third_party/blink/renderer/core/url_pattern/url_pattern.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h"
 #include "third_party/blink/renderer/platform/heap/member.h"
@@ -569,8 +570,8 @@
 
   // If predicateType is "selector_matches"
   if (predicate_type == "selector_matches" && input->size() == 1) {
-    const bool selector_matches_enabled = RuntimeEnabledFeatures::
-        SpeculationRulesDocumentRulesSelectorMatchesEnabled(execution_context);
+    const bool selector_matches_enabled =
+        speculation_rules::SelectorMatchesEnabled(execution_context);
     if (!selector_matches_enabled) {
       SetParseErrorMessage(out_error,
                            "\"selector_matches\" is currently unsupported.");
diff --git a/third_party/blink/renderer/core/speculation_rules/document_speculation_rules.cc b/third_party/blink/renderer/core/speculation_rules/document_speculation_rules.cc
index 602cc37..d7fa7d77 100644
--- a/third_party/blink/renderer/core/speculation_rules/document_speculation_rules.cc
+++ b/third_party/blink/renderer/core/speculation_rules/document_speculation_rules.cc
@@ -24,6 +24,7 @@
 #include "third_party/blink/renderer/core/probe/core_probes.h"
 #include "third_party/blink/renderer/core/speculation_rules/document_rule_predicate.h"
 #include "third_party/blink/renderer/core/speculation_rules/speculation_candidate.h"
+#include "third_party/blink/renderer/core/speculation_rules/speculation_rules_features.h"
 #include "third_party/blink/renderer/core/speculation_rules/speculation_rules_metrics.h"
 #include "third_party/blink/renderer/platform/scheduler/public/event_loop.h"
 #include "third_party/blink/renderer/platform/weborigin/referrer.h"
@@ -762,9 +763,8 @@
   if (was_selector_matches_enabled_) {
     return true;
   }
-  was_selector_matches_enabled_ = RuntimeEnabledFeatures::
-      SpeculationRulesDocumentRulesSelectorMatchesEnabled(
-          GetSupplementable()->GetExecutionContext());
+  was_selector_matches_enabled_ = speculation_rules::SelectorMatchesEnabled(
+      GetSupplementable()->GetExecutionContext());
   return was_selector_matches_enabled_;
 }
 
diff --git a/third_party/blink/renderer/core/speculation_rules/speculation_rule_set.cc b/third_party/blink/renderer/core/speculation_rules/speculation_rule_set.cc
index dd25121f..aff135260 100644
--- a/third_party/blink/renderer/core/speculation_rules/speculation_rule_set.cc
+++ b/third_party/blink/renderer/core/speculation_rules/speculation_rule_set.cc
@@ -13,6 +13,7 @@
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
 #include "third_party/blink/renderer/core/inspector/identifiers_factory.h"
 #include "third_party/blink/renderer/core/speculation_rules/document_rule_predicate.h"
+#include "third_party/blink/renderer/core/speculation_rules/speculation_rules_features.h"
 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
 #include "third_party/blink/renderer/platform/json/json_parser.h"
 #include "third_party/blink/renderer/platform/json/json_values.h"
@@ -76,7 +77,7 @@
       "referrer_policy", "relative_to"};
   const auto kConditionalKnownKeys = [context]() {
     Vector<const char*, 4> conditional_known_keys;
-    if (RuntimeEnabledFeatures::SpeculationRulesEagernessEnabled(context)) {
+    if (speculation_rules::EagernessEnabled(context)) {
       conditional_known_keys.push_back("eagerness");
     }
     if (RuntimeEnabledFeatures::SpeculationRulesNoVarySearchHintEnabled(
@@ -303,7 +304,7 @@
   absl::optional<mojom::blink::SpeculationEagerness> eagerness;
   if (JSONValue* eagerness_value = input->Get("eagerness")) {
     // Feature gated due to known keys check above.
-    DCHECK(RuntimeEnabledFeatures::SpeculationRulesEagernessEnabled(context));
+    DCHECK(speculation_rules::EagernessEnabled(context));
 
     String eagerness_str;
     if (!eagerness_value->AsString(&eagerness_str)) {
diff --git a/third_party/blink/renderer/core/speculation_rules/speculation_rules_features.cc b/third_party/blink/renderer/core/speculation_rules/speculation_rules_features.cc
new file mode 100644
index 0000000..8009d7c
--- /dev/null
+++ b/third_party/blink/renderer/core/speculation_rules/speculation_rules_features.cc
@@ -0,0 +1,26 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/speculation_rules/speculation_rules_features.h"
+
+#include "base/feature_list.h"
+#include "third_party/blink/public/common/features.h"
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
+
+namespace blink::speculation_rules {
+
+bool EagernessEnabled(const FeatureContext* context) {
+  return RuntimeEnabledFeatures::SpeculationRulesEagernessEnabled(context) &&
+         base::FeatureList::IsEnabled(
+             blink::features::kSpeculationRulesEagerness);
+}
+
+bool SelectorMatchesEnabled(const FeatureContext* context) {
+  return RuntimeEnabledFeatures::
+             SpeculationRulesDocumentRulesSelectorMatchesEnabled(context) &&
+         base::FeatureList::IsEnabled(
+             blink::features::kSpeculationRulesDocumentRulesSelectorMatches);
+}
+
+}  // namespace blink::speculation_rules
diff --git a/third_party/blink/renderer/core/speculation_rules/speculation_rules_features.h b/third_party/blink/renderer/core/speculation_rules/speculation_rules_features.h
new file mode 100644
index 0000000..b6965e5
--- /dev/null
+++ b/third_party/blink/renderer/core/speculation_rules/speculation_rules_features.h
@@ -0,0 +1,28 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_SPECULATION_RULES_SPECULATION_RULES_FEATURES_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_SPECULATION_RULES_SPECULATION_RULES_FEATURES_H_
+
+namespace blink {
+
+class FeatureContext;
+
+namespace speculation_rules {
+
+// You might be asking, why not just check RuntimeEnabledFeatures?
+//
+// The answer is that the REF is turned on by an origin trial (namely,
+// SpeculationRulesPrefetchFuture), and that works even if the associated
+// base::Feature is disabled. The simplest solution seems to be this little
+// hack -- just check that the base::Feature is actually on before actually
+// using the feature. This suffices because these are not used to control WebIDL
+// exposure.
+bool EagernessEnabled(const FeatureContext*);
+bool SelectorMatchesEnabled(const FeatureContext*);
+
+}  // namespace speculation_rules
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_CORE_SPECULATION_RULES_SPECULATION_RULES_FEATURES_H_
diff --git a/third_party/blink/renderer/core/svg/svg_a_element.cc b/third_party/blink/renderer/core/svg/svg_a_element.cc
index ad27ac7..a7aab3bc 100644
--- a/third_party/blink/renderer/core/svg/svg_a_element.cc
+++ b/third_party/blink/renderer/core/svg/svg_a_element.cc
@@ -140,6 +140,8 @@
           GetDocument().domWindow(),
           ResourceRequest(GetDocument().CompleteURL(url)));
       frame_request.SetNavigationPolicy(NavigationPolicyFromEvent(&event));
+      frame_request.SetClientRedirectReason(
+          ClientNavigationReason::kAnchorClick);
       frame_request.SetTriggeringEventInfo(
           event.isTrusted()
               ? mojom::blink::TriggeringEventInfo::kFromTrustedEvent
diff --git a/third_party/blink/renderer/core/testing/internals.cc b/third_party/blink/renderer/core/testing/internals.cc
index 727c8e6f..456dc77 100644
--- a/third_party/blink/renderer/core/testing/internals.cc
+++ b/third_party/blink/renderer/core/testing/internals.cc
@@ -2133,6 +2133,18 @@
       .ForceInvocationForTesting();
 }
 
+bool Internals::hasLastEditCommand(Document* document,
+                                   ExceptionState& exception_state) {
+  if (!document || !document->GetFrame()) {
+    exception_state.ThrowDOMException(
+        DOMExceptionCode::kInvalidAccessError,
+        "No frame can be obtained from the provided document.");
+    return false;
+  }
+
+  return document->GetFrame()->GetEditor().LastEditCommand();
+}
+
 Vector<AtomicString> Internals::userPreferredLanguages() const {
   return blink::UserPreferredLanguages();
 }
diff --git a/third_party/blink/renderer/core/testing/internals.h b/third_party/blink/renderer/core/testing/internals.h
index 75d2205..d72743d 100644
--- a/third_party/blink/renderer/core/testing/internals.h
+++ b/third_party/blink/renderer/core/testing/internals.h
@@ -292,6 +292,8 @@
   String idleTimeSpellCheckerState(Document*, ExceptionState&);
   void runIdleTimeSpellChecker(Document*, ExceptionState&);
 
+  bool hasLastEditCommand(Document*, ExceptionState&);
+
   Vector<AtomicString> userPreferredLanguages() const;
   void setUserPreferredLanguages(const Vector<String>&);
   void setSystemTimeZone(const String&);
diff --git a/third_party/blink/renderer/core/testing/internals.idl b/third_party/blink/renderer/core/testing/internals.idl
index 2b21f09..e607475 100644
--- a/third_party/blink/renderer/core/testing/internals.idl
+++ b/third_party/blink/renderer/core/testing/internals.idl
@@ -148,6 +148,8 @@
     [RaisesException] DOMString idleTimeSpellCheckerState(Document document);
     [RaisesException] void runIdleTimeSpellChecker(Document document);
 
+    [RaisesException] boolean hasLastEditCommand(Document document);
+
     sequence<DOMString> userPreferredLanguages();
     void setUserPreferredLanguages(sequence<DOMString> languages);
     void setSystemTimeZone(DOMString timezone);
diff --git a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_manager_test.cc b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_manager_test.cc
index 531cc5b..65c63e8 100644
--- a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_manager_test.cc
+++ b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_manager_test.cc
@@ -721,7 +721,7 @@
   EXPECT_EQ(output_sample_rate,
             mixer->get_output_params_for_testing().sample_rate());
 
-#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_MAC) || \
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_APPLE) || \
     BUILDFLAG(IS_FUCHSIA)
   // Use 10 ms buffer (441 frames per buffer).
   EXPECT_EQ(output_sample_rate / 100,
diff --git a/third_party/blink/renderer/modules/scheduler/BUILD.gn b/third_party/blink/renderer/modules/scheduler/BUILD.gn
index 0d799dd..47c380d6 100644
--- a/third_party/blink/renderer/modules/scheduler/BUILD.gn
+++ b/third_party/blink/renderer/modules/scheduler/BUILD.gn
@@ -20,5 +20,7 @@
     "task_attribution_tracker_impl.h",
     "task_priority_change_event.cc",
     "task_priority_change_event.h",
+    "window_idle_tasks.cc",
+    "window_idle_tasks.h",
   ]
 }
diff --git a/third_party/blink/renderer/modules/scheduler/window_idle_tasks.cc b/third_party/blink/renderer/modules/scheduler/window_idle_tasks.cc
new file mode 100644
index 0000000..a08764b
--- /dev/null
+++ b/third_party/blink/renderer/modules/scheduler/window_idle_tasks.cc
@@ -0,0 +1,66 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/modules/scheduler/window_idle_tasks.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/v8_idle_request_callback.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_idle_request_options.h"
+#include "third_party/blink/renderer/core/dom/scripted_idle_task_controller.h"
+#include "third_party/blink/renderer/core/frame/local_dom_window.h"
+#include "third_party/blink/renderer/platform/bindings/script_state.h"
+#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
+#include "third_party/blink/renderer/platform/scheduler/public/task_attribution_tracker.h"
+#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
+
+namespace blink {
+
+namespace {
+
+// `V8IdleTask` is the adapter class for the conversion from
+// `V8IdleRequestCallback` to `IdleTask`.
+class V8IdleTask : public IdleTask {
+ public:
+  static V8IdleTask* Create(V8IdleRequestCallback* callback) {
+    return MakeGarbageCollected<V8IdleTask>(callback);
+  }
+
+  explicit V8IdleTask(V8IdleRequestCallback* callback) : callback_(callback) {}
+
+  ~V8IdleTask() override = default;
+
+  void invoke(IdleDeadline* deadline) override {
+    callback_->InvokeAndReportException(nullptr, deadline);
+  }
+
+  void Trace(Visitor* visitor) const override {
+    visitor->Trace(callback_);
+    IdleTask::Trace(visitor);
+  }
+
+ private:
+  Member<V8IdleRequestCallback> callback_;
+};
+
+}  // namespace
+
+int WindowIdleTasks::requestIdleCallback(LocalDOMWindow& window,
+                                         V8IdleRequestCallback* callback,
+                                         const IdleRequestOptions* options) {
+  if (!window.GetFrame()) {
+    return 0;
+  }
+  ScriptState* script_state = callback->CallbackRelevantScriptState();
+  auto* tracker = ThreadScheduler::Current()->GetTaskAttributionTracker();
+  if (tracker && script_state->World().IsMainWorld()) {
+    callback->SetParentTaskId(tracker->RunningTaskAttributionId(script_state));
+  }
+  return window.document()->RequestIdleCallback(V8IdleTask::Create(callback),
+                                                options);
+}
+
+void WindowIdleTasks::cancelIdleCallback(LocalDOMWindow& window, int id) {
+  window.document()->CancelIdleCallback(id);
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/modules/scheduler/window_idle_tasks.h b/third_party/blink/renderer/modules/scheduler/window_idle_tasks.h
new file mode 100644
index 0000000..271c36d
--- /dev/null
+++ b/third_party/blink/renderer/modules/scheduler/window_idle_tasks.h
@@ -0,0 +1,27 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_SCHEDULER_WINDOW_IDLE_TASKS_H_
+#define THIRD_PARTY_BLINK_RENDERER_MODULES_SCHEDULER_WINDOW_IDLE_TASKS_H_
+
+#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
+
+namespace blink {
+class IdleRequestOptions;
+class LocalDOMWindow;
+class V8IdleRequestCallback;
+
+class WindowIdleTasks {
+  STATIC_ONLY(WindowIdleTasks);
+
+ public:
+  static int requestIdleCallback(LocalDOMWindow&,
+                                 V8IdleRequestCallback*,
+                                 const IdleRequestOptions*);
+  static void cancelIdleCallback(LocalDOMWindow&, int id);
+};
+
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_MODULES_SCHEDULER_WINDOW_IDLE_TASKS_H_
diff --git a/third_party/blink/renderer/modules/scheduler/window_idle_tasks.idl b/third_party/blink/renderer/modules/scheduler/window_idle_tasks.idl
new file mode 100644
index 0000000..cc3c089
--- /dev/null
+++ b/third_party/blink/renderer/modules/scheduler/window_idle_tasks.idl
@@ -0,0 +1,12 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Cooperative Scheduling of Background Tasks
+// https://www.w3.org/TR/requestidlecallback/#window_extensions
+[
+  ImplementedAs=WindowIdleTasks
+] partial interface Window {
+  [Measure] long requestIdleCallback(IdleRequestCallback callback, optional IdleRequestOptions options = {});
+  void cancelIdleCallback(long handle);
+};
diff --git a/third_party/blink/renderer/platform/README.md b/third_party/blink/renderer/platform/README.md
new file mode 100644
index 0000000..1605814e
--- /dev/null
+++ b/third_party/blink/renderer/platform/README.md
@@ -0,0 +1,32 @@
+# Blink Renderer Platform
+
+The `renderer/platform/` directory contains lower-level, self-contained
+abstractions that `core/` and `modules/` can depend on.
+
+See [renderer/README.md](../README.md) for more about the relationship of
+`platform/` to `core/` and `modules/`.
+
+Here is a non-exhaustive list of some major things in `renderer/platform/`:
+
+* [Runtime Enabled Features](RuntimeEnabledFeatures.md) are runtime flags for
+  new web-exposed features
+* [`bindings/`](bindings/README.md) contains reusable components for the V8-DOM
+  bindings layer
+* `exported/` implements some classes in the [Blink Public
+  API](../../public/README.md) which are declared in
+  [`public/platform/`](../../public/platform/), including
+  [blink::Platform](../../public/platform/platform.h) which initializes Blink
+* [`fonts/`](fonts/README.md) and `text/` contain Blink's font and text stack
+* [`graphics/`](graphics/README.md) contains graphics support code including
+  the [Blink compositing algorithm](graphics/compositing/README.md)
+* [`heap/`](heap/README.md) contains the Blink GC system (a.k.a. Oilpan)
+* [`loader/`](loader/README.md) contains functionality for loading resources
+  from the network
+* [`scheduler/`](scheduler/README.md) contains the Blink Scheduler which
+  coordinates task execution in renderer processes
+* [`widget/`](widget/) handles input and compositing;
+  [WidgetBase](widget/widget_base.h) owns
+  [LayerTreeView](widget/compositing/layer_tree_view.h) which wraps
+  [`cc/`](../../../../cc/README.md) (the renderer compositor)
+* [`wtf/`](wtf/README.md) (Web Template Framework) is a library of containers
+  and other basic functionalities
diff --git a/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer_test.cc b/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer_test.cc
index 4a9e2b6..17e9100c 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer_test.cc
+++ b/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer_test.cc
@@ -565,7 +565,13 @@
       }});
 }
 
-TEST_F(ShapeResultBloberizerTest, SupplementaryMultiRunNG) {
+// TODO(crbug.com/1422946): not yet supported on iOS.
+#if BUILDFLAG(IS_IOS)
+#define MAYBE_SupplementaryMultiRunNG DISABLED_SupplementaryMultiRunNG
+#else
+#define MAYBE_SupplementaryMultiRunNG SupplementaryMultiRunNG
+#endif  // BUILDFLAG(IS_IOS)
+TEST_F(ShapeResultBloberizerTest, MAYBE_SupplementaryMultiRunNG) {
   TextDirection direction = TextDirection::kLtr;
   // 𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕
   const UChar kStrSupp[] = {0xD841, 0xDF0E, 0xD841, 0xDF31, 0xD841, 0xDF79,
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 028ab25..5db71617 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -839,7 +839,7 @@
       // image-set values without the `-webkit-` prefix
       // will be parsed.
       name: "CSSImageSet",
-      status: "experimental",
+      status: "stable",
     },
     {
       name: "CSSIndependentTransformProperties",
@@ -1362,6 +1362,12 @@
       base_feature: "none",
     },
     {
+      // Kill-switch for fixes to a bug that detached input elements are retained.
+      // See crbug.com/1413100
+      name: "DontLeakDetachedInput",
+      status: "stable",
+    },
+    {
       name: "EarlyHintsPreloadForNavigationOptIn",
       origin_trial_feature_name: "EarlyHintsPreloadForNavigation",
       status: "stable",
@@ -3111,11 +3117,17 @@
     },
     {
       name: "SpeculationRulesDocumentRulesSelectorMatches",
-      base_feature: "none",
+      base_feature_status: "enabled",
+      copied_from_base_feature_if: "overridden",
+      origin_trial_feature_name: "SpeculationRulesPrefetchFuture",
+      origin_trial_allows_third_party: true,
     },
     {
       name: "SpeculationRulesEagerness",
-      base_feature: "none",
+      base_feature_status: "enabled",
+      copied_from_base_feature_if: "overridden",
+      origin_trial_feature_name: "SpeculationRulesPrefetchFuture",
+      origin_trial_allows_third_party: true,
     },
     {
       name: "SpeculationRulesFetchFromHeader",
@@ -3133,6 +3145,7 @@
     },
     {
       name: "SpeculationRulesPointerHoverHeuristics",
+      base_feature_status: "enabled",
     },
     // This feature exists solely to be the target of implied_by for features
     // that are part of this trial but which may also be enabled another way.
diff --git a/third_party/blink/tools/blinkpy/tool/blink_tool.py b/third_party/blink/tools/blinkpy/tool/blink_tool.py
index c19043e..aa2bcfa 100644
--- a/third_party/blink/tools/blinkpy/tool/blink_tool.py
+++ b/third_party/blink/tools/blinkpy/tool/blink_tool.py
@@ -74,7 +74,7 @@
         self._path = path
         self.commands = [
             AnalyzeBaselines(),
-            CopyExistingBaselines(),
+            CopyExistingBaselines(self),
             CrashLog(),
             FlakyTests(),
             OptimizeBaselines(),
diff --git a/third_party/blink/tools/blinkpy/tool/blink_tool_unittest.py b/third_party/blink/tools/blinkpy/tool/blink_tool_unittest.py
index 77529f86..ed1c900 100644
--- a/third_party/blink/tools/blinkpy/tool/blink_tool_unittest.py
+++ b/third_party/blink/tools/blinkpy/tool/blink_tool_unittest.py
@@ -10,6 +10,8 @@
 from blinkpy.tool.blink_tool import BlinkTool
 
 
+@unittest.skip('Temporarily disabled while crbug.com/1324638 is being fixed; '
+               'reenable once `copy-existing-baselines` is removed')
 # Avoid creating a real `Git` object, since it runs a command in its
 # constructor.
 @patch('blinkpy.tool.blink_tool.BlinkTool.git',
diff --git a/third_party/blink/tools/blinkpy/tool/commands/copy_existing_baselines.py b/third_party/blink/tools/blinkpy/tool/commands/copy_existing_baselines.py
index 9c881ba..d0aa675 100644
--- a/third_party/blink/tools/blinkpy/tool/commands/copy_existing_baselines.py
+++ b/third_party/blink/tools/blinkpy/tool/commands/copy_existing_baselines.py
@@ -6,7 +6,6 @@
 
 from blinkpy.common.checkout.baseline_copier import BaselineCopier
 from blinkpy.tool.commands.rebaseline import AbstractRebaseliningCommand
-from blinkpy.web_tests.models.test_expectations import TestExpectationsCache
 
 _log = logging.getLogger(__name__)
 
@@ -17,24 +16,24 @@
                  'order to ensure new baselines don\'t break existing passing '
                  'platforms.')
 
-    def __init__(self):
-        super(CopyExistingBaselines, self).__init__(options=[
+    def __init__(self, tool):
+        super().__init__(options=[
             self.test_option,
             self.suffixes_option,
             self.port_name_option,
             self.flag_specific_option,
             self.results_directory_option,
         ])
-        self._exp_cache = TestExpectationsCache()
+        self._copier = BaselineCopier(tool)
 
     def execute(self, options, args, tool):
         # TODO(crbug.com/1324638): Remove this command. Have `rebaseline-cl`
         # call `find_baselines_to_copy(...)` directly to find implied all-pass
         # baselines, then copy all ports/tests together if OK.
-        copier = BaselineCopier(tool)
         port = tool.port_factory.get(options.port_name)
         if options.flag_specific:
             port.set_option_default('flag_specific', options.flag_specific)
         for suffix in options.suffixes.split(','):
-            copier.write_copies(
-                copier.find_baselines_to_copy(options.test, suffix, [port]))
+            self._copier.write_copies(
+                self._copier.find_baselines_to_copy(options.test, suffix,
+                                                    [port]))
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 2d51c0e..e6f21211 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -1118,7 +1118,6 @@
 crbug.com/882385 external/wpt/css/css-contain/quote-scoping-002.html [ Failure ]
 crbug.com/882385 external/wpt/css/css-contain/quote-scoping-003.html [ Failure ]
 crbug.com/882385 external/wpt/css/css-contain/quote-scoping-004.html [ Failure ]
-crbug.com/1315275 external/wpt/css/css-contain/content-visibility/content-visibility-canvas.html [ Failure ]
 crbug.com/1315275 external/wpt/css/css-contain/content-visibility/content-visibility-video.html [ Failure ]
 crbug.com/1400979 external/wpt/css/css-contain/container-queries/nested-query-containers.html [ Crash Failure Pass Timeout ]
 
@@ -3486,11 +3485,7 @@
 crbug.com/618969 external/wpt/css/css-grid/subgrid/orthogonal-writing-mode-002.html [ Failure ]
 crbug.com/618969 external/wpt/css/css-grid/subgrid/orthogonal-writing-mode-003.html [ Failure ]
 crbug.com/618969 external/wpt/css/css-grid/subgrid/orthogonal-writing-mode-004.html [ Failure ]
-crbug.com/618969 external/wpt/css/css-grid/subgrid/parent-repeat-auto-fit-001.html [ Failure ]
-crbug.com/618969 external/wpt/css/css-grid/subgrid/parent-repeat-auto-fit-002.html [ Failure ]
-crbug.com/618969 external/wpt/css/css-grid/subgrid/repeat-auto-fill-002.html [ Failure ]
 crbug.com/618969 external/wpt/css/css-grid/subgrid/repeat-auto-fill-003.html [ Failure ]
-crbug.com/618969 external/wpt/css/css-grid/subgrid/repeat-auto-fill-004.html [ Failure ]
 crbug.com/618969 external/wpt/css/css-grid/subgrid/subgrid-baseline-001.html [ Failure ]
 crbug.com/618969 external/wpt/css/css-grid/subgrid/subgrid-baseline-002.html [ Crash Failure Timeout ]
 
diff --git a/third_party/blink/web_tests/compositing/dont-composite-select-elements.html b/third_party/blink/web_tests/compositing/dont-composite-select-elements.html
deleted file mode 100644
index 24bc0bf..0000000
--- a/third_party/blink/web_tests/compositing/dont-composite-select-elements.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!doctype HTML>
-<script src="../resources/testharness.js"></script>
-<script src="../resources/testharnessreport.js"></script>
-<select size="2">
-  <option> value 1</option>
-  <option> value 2</option>
-  <option> value 3</option>
-  <option> value 4</option>
-</select>
-<script>
-if (window.internals)
-  internals.settings.setPreferCompositingToLCDTextEnabled(true);
-test(function() {
-    var json = JSON.parse(internals.layerTreeAsText(document));
-    // The <select> element's scroller should be painted into the root layer.
-    assert_equals(json["layers"].length, 1);
-}, "test");
-</script>
diff --git a/third_party/blink/web_tests/compositing/dont-composite-text-input-elements.html b/third_party/blink/web_tests/compositing/dont-composite-text-input-elements.html
deleted file mode 100644
index f5a2f74..0000000
--- a/third_party/blink/web_tests/compositing/dont-composite-text-input-elements.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!doctype HTML>
-<script src="../resources/testharness.js"></script>
-<script src="../resources/testharnessreport.js"></script>
-<input width=10 value="This is a long truncated text entry" style="font-size:40pt;"/>
-<script>
-if (window.internals)
-  internals.settings.setPreferCompositingToLCDTextEnabled(true);
-test(function() {
-    var json = JSON.parse(internals.layerTreeAsText(document));
-    // The <input> element's scroller should be painted into the root layer.
-    assert_equals(json["layers"].length, 1);
-}, "test");
-</script>
diff --git a/third_party/blink/web_tests/compositing/select-element.html b/third_party/blink/web_tests/compositing/select-element.html
new file mode 100644
index 0000000..6e9aa2e
--- /dev/null
+++ b/third_party/blink/web_tests/compositing/select-element.html
@@ -0,0 +1,25 @@
+<!doctype HTML>
+<script src="../resources/testharness.js"></script>
+<script src="../resources/testharnessreport.js"></script>
+<select id="target" size="2">
+  <option> value 1</option>
+  <option> value 2</option>
+  <option> value 3</option>
+  <option> value 4</option>
+</select>
+<script>
+if (window.internals) {
+  internals.settings.setPreferCompositingToLCDTextEnabled(true);
+}
+function assertLayers(expected_count) {
+  var json = JSON.parse(internals.layerTreeAsText(document));
+  assert_equals(json.layers.length, expected_count, json);
+}
+test(() => {
+  assertLayers(4);
+}, "scrollable");
+test(() => {
+  target.size = 4;
+  assertLayers(1);
+}, "not scrollable");
+</script>
diff --git a/third_party/blink/web_tests/compositing/text-input-element.html b/third_party/blink/web_tests/compositing/text-input-element.html
new file mode 100644
index 0000000..75ffaf5
--- /dev/null
+++ b/third_party/blink/web_tests/compositing/text-input-element.html
@@ -0,0 +1,20 @@
+<!doctype HTML>
+<script src="../resources/testharness.js"></script>
+<script src="../resources/testharnessreport.js"></script>
+<input id="target" width=10 value="This is a long truncated text entry" style="font-size:40pt;"/>
+<script>
+if (window.internals) {
+  internals.settings.setPreferCompositingToLCDTextEnabled(true);
+}
+function assertLayers(expected_count) {
+  var json = JSON.parse(internals.layerTreeAsText(document));
+  assert_equals(json.layers.length, expected_count, json);
+}
+test(() => {
+  assertLayers(3);
+}, "scrollable");
+test(() => {
+  target.value = "short";
+  assertLayers(1);
+}, "not scrollable");
+</script>
diff --git a/third_party/blink/web_tests/editing/inserting/typing-leaks-detached-input.html b/third_party/blink/web_tests/editing/inserting/typing-leaks-detached-input.html
new file mode 100644
index 0000000..304f6efc
--- /dev/null
+++ b/third_party/blink/web_tests/editing/inserting/typing-leaks-detached-input.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<title>Tests that typing should not retain detached subtree</title>
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+
+<div id="root"></div>
+
+<script>
+const n = 100;
+
+function createSubtree() {
+  let last = document.createElement('div');
+  last.spellcheck = false;
+  last.contentEditable = true;
+  last.id = 'target';
+  for (let i = 0; i < n; ++i) {
+    let div = document.createElement('div');
+    div.appendChild(last);
+    last = div;
+  }
+  root.appendChild(last);
+}
+
+function raf() {
+  return new Promise(resolve => requestAnimationFrame(resolve));
+}
+
+promise_setup(async () => {
+  assert_own_property(window, 'internals', 'This test requires internals')
+})
+
+promise_test(async () => {
+  gc();
+  let nodesBefore = internals.numberOfLiveNodes();
+
+  createSubtree();
+  document.getElementById('target').focus();
+  document.execCommand('insertText', false, 'foo');
+  assert_true(internals.hasLastEditCommand(document));
+
+  root.firstChild.remove();
+
+  await raf();
+  await raf();
+
+  gc();
+  let nodesAfter = internals.numberOfLiveNodes();
+
+  // Give the test some leeway, which should be safe since the size of the
+  // detached subtree is much larger than this.
+  assert_less_than_equal(nodesAfter, nodesBefore + 20);
+  assert_false(internals.hasLastEditCommand(document));
+}, 'Typing should not retain detached subtree');
+
+promise_test(async () => {
+  createSubtree();
+  document.getElementById('target').focus();
+  document.execCommand('insertText', false, 'foo');
+  document.getElementById('target').blur();
+  assert_true(internals.hasLastEditCommand(document));
+
+  await raf();
+  await raf();
+
+  gc();
+  assert_true(internals.hasLastEditCommand(document));
+}, 'Last edit command should be retained if node is still connected');
+</script>
+
+</body>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-contain/content-visibility/content-visibility-canvas.html.ini b/third_party/blink/web_tests/external/wpt/css/css-contain/content-visibility/content-visibility-canvas.html.ini
deleted file mode 100644
index d1d71c8..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-contain/content-visibility/content-visibility-canvas.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[content-visibility-canvas.html]
-  expected: FAIL
diff --git a/third_party/blink/web_tests/external/wpt/css/css-sizing/contain-intrinsic-size/auto-007-expected.txt b/third_party/blink/web_tests/external/wpt/css/css-sizing/contain-intrinsic-size/auto-007-expected.txt
deleted file mode 100644
index 3cba385e..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-sizing/contain-intrinsic-size/auto-007-expected.txt
+++ /dev/null
@@ -1,51 +0,0 @@
-This is a testharness.js-based test.
-PASS .test 1
-PASS .test 2
-PASS .test 3
-PASS .test 4
-PASS .test 5
-PASS .test 6
-PASS .test 7
-PASS .test 8
-PASS .test 9
-PASS .test 10
-PASS .test 11
-PASS .test 12
-PASS .test 13
-PASS .test 14
-PASS .test 15
-PASS .test 16
-PASS .test 17
-PASS .test 18
-PASS .test 19
-PASS .test 20
-PASS .test 21
-PASS .test 22
-PASS .test 23
-PASS .test 24
-FAIL .test 25 assert_equals: 
-<canvas class="test auto-width" data-expected-client-width="300" data-expected-client-height="0"></canvas>
-clientWidth expected 300 but got 1
-FAIL .test 26 assert_equals: 
-<canvas class="test auto-height" data-expected-client-width="0" data-expected-client-height="150"></canvas>
-clientHeight expected 150 but got 2
-FAIL .test 27 assert_equals: 
-<canvas class="test auto-both" data-expected-client-width="300" data-expected-client-height="150"></canvas>
-clientWidth expected 300 but got 3
-PASS .test 28
-PASS .test 29
-PASS .test 30
-PASS .test 31
-PASS .test 32
-PASS .test 33
-PASS .test 34
-PASS .test 35
-PASS .test 36
-PASS .test 37
-PASS .test 38
-PASS .test 39
-PASS .test 40
-PASS .test 41
-PASS .test 42
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/external/wpt/css/css-sizing/contain-intrinsic-size/auto-007.html.ini b/third_party/blink/web_tests/external/wpt/css/css-sizing/contain-intrinsic-size/auto-007.html.ini
deleted file mode 100644
index 9182f6c..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-sizing/contain-intrinsic-size/auto-007.html.ini
+++ /dev/null
@@ -1,15 +0,0 @@
-[auto-007.html]
-  [.test 7]
-    expected: FAIL
-
-  [.test 9]
-    expected: FAIL
-
-  [.test 25]
-    expected: FAIL
-
-  [.test 26]
-    expected: FAIL
-
-  [.test 27]
-    expected: FAIL
diff --git a/third_party/blink/web_tests/external/wpt/scroll-animations/css/animation-shorthand.html b/third_party/blink/web_tests/external/wpt/scroll-animations/css/animation-shorthand.html
index 60d6c76..87e66d0 100644
--- a/third_party/blink/web_tests/external/wpt/scroll-animations/css/animation-shorthand.html
+++ b/third_party/blink/web_tests/external/wpt/scroll-animations/css/animation-shorthand.html
@@ -40,4 +40,53 @@
   'animation-name': 'anim1, anim2, anim3',
   'animation-timeline': 'auto, auto, auto'
 });
+
+test((t) => {
+  t.add_cleanup(() => {
+    target.style = '';
+  });
+
+  target.style.animation = 'anim 1s';
+  target.style.animationTimeline = 'timeline';
+  assert_equals(target.style.animation, '');
+  assert_equals(target.style.animationName, 'anim');
+  assert_equals(target.style.animationDuration, '1s');
+}, 'Animation shorthand can not represent non-initial timelines (specified)');
+
+test((t) => {
+  t.add_cleanup(() => {
+    target.style = '';
+  });
+
+  target.style.animation = 'anim 1s';
+  target.style.animationTimeline = 'timeline';
+  assert_equals(getComputedStyle(target).animation, '');
+  assert_equals(getComputedStyle(target).animationName, 'anim');
+  assert_equals(getComputedStyle(target).animationDuration, '1s');
+}, 'Animation shorthand can not represent non-initial timelines (computed)');
+
+test((t) => {
+  t.add_cleanup(() => {
+    target.style = '';
+  });
+
+  target.style.animation = 'anim 1s';
+  target.style.animationDelayEnd = '42s';
+  assert_equals(target.style.animation, '');
+  assert_equals(target.style.animationName, 'anim');
+  assert_equals(target.style.animationDuration, '1s');
+}, 'Animation shorthand can not represent non-initial animation-delay-end (specified)');
+
+test((t) => {
+  t.add_cleanup(() => {
+    target.style = '';
+  });
+
+  target.style.animation = 'anim 1s';
+  target.style.animationDelayEnd = '42s';
+  assert_equals(getComputedStyle(target).animation, '');
+  assert_equals(getComputedStyle(target).animationName, 'anim');
+  assert_equals(getComputedStyle(target).animationDuration, '1s');
+}, 'Animation shorthand can not represent non-initial animation-delay-end (computed)');
+
 </script>
diff --git a/third_party/blink/web_tests/flag-specific/highdpi/fast/forms/form-element-geometry-expected.png b/third_party/blink/web_tests/flag-specific/highdpi/fast/forms/form-element-geometry-expected.png
index 05249f1e..2583671 100644
--- a/third_party/blink/web_tests/flag-specific/highdpi/fast/forms/form-element-geometry-expected.png
+++ b/third_party/blink/web_tests/flag-specific/highdpi/fast/forms/form-element-geometry-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/flag-specific/highdpi/fragmentation/outline-crossing-columns-expected.png b/third_party/blink/web_tests/flag-specific/highdpi/fragmentation/outline-crossing-columns-expected.png
index 906ae22..06acb4e 100644
--- a/third_party/blink/web_tests/flag-specific/highdpi/fragmentation/outline-crossing-columns-expected.png
+++ b/third_party/blink/web_tests/flag-specific/highdpi/fragmentation/outline-crossing-columns-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/fedcm/fedcm-dialog-event-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/fedcm/fedcm-dialog-event-expected.txt
index c69fae62..fc2bf6a 100644
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/fedcm/fedcm-dialog-event-expected.txt
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/fedcm/fedcm-dialog-event-expected.txt
@@ -1,3 +1,13 @@
 Check that the dialogShown event works after enabling the FedCm domain
 OK
+accounts: [
+    [0] : {
+        accountId : 1234
+        email : john_doe@idp.example
+        givenName : John
+        idpConfigUrl : https://devtools.test:8443/resources/fedcm/fedcm.json
+        name : John Doe
+        pictureUrl : https://idp.example/profile/123
+    }
+]
 
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/fedcm/fedcm-dialog-event.js b/third_party/blink/web_tests/http/tests/inspector-protocol/fedcm/fedcm-dialog-event.js
index 16d66e14..8fb0327 100644
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/fedcm/fedcm-dialog-event.js
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/fedcm/fedcm-dialog-event.js
@@ -11,6 +11,11 @@
 
   const result = await session.evaluateAsync("triggerDialog()");
   testRunner.log(result);
-  await dp.FedCm.onceDialogShown();
+  let msg = await dp.FedCm.onceDialogShown();
+  if (msg.error) {
+    testRunner.log(msg.error);
+  } else {
+    testRunner.log(msg.params.accounts, "accounts: ");
+  }
   testRunner.completeTest();
 })
diff --git a/third_party/blink/web_tests/platform/linux/paint/invalidation/scroll/invalidate-caret-in-composited-scrolling-container-expected.txt b/third_party/blink/web_tests/platform/linux/paint/invalidation/scroll/invalidate-caret-in-composited-scrolling-container-expected.txt
index cb66a88a..8e7f1da 100644
--- a/third_party/blink/web_tests/platform/linux/paint/invalidation/scroll/invalidate-caret-in-composited-scrolling-container-expected.txt
+++ b/third_party/blink/web_tests/platform/linux/paint/invalidation/scroll/invalidate-caret-in-composited-scrolling-container-expected.txt
@@ -10,7 +10,6 @@
       "name": "LayoutNGTextControlSingleLine INPUT id='root'",
       "position": [-1, -1],
       "bounds": [67, 24],
-      "contentsOpaqueForText": true,
       "backgroundColor": "#FFFFFF",
       "invalidations": [
         [0, 0, 67, 24]
@@ -18,6 +17,20 @@
       "transform": 1
     },
     {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [57, 16],
+      "drawsContent": false,
+      "transform": 2
+    },
+    {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [74, 16],
+      "invalidations": [
+        [0, 0, 74, 16]
+      ],
+      "transform": 3
+    },
+    {
       "name": "Caret",
       "position": [56, 0],
       "bounds": [1, 16],
@@ -56,6 +69,16 @@
         [0, 0, 1, 0],
         [4, 3, 0, 1]
       ]
+    },
+    {
+      "id": 3,
+      "parent": 2,
+      "transform": [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 1, 0],
+        [-17, 0, 0, 1]
+      ]
     }
   ]
 }
diff --git a/third_party/blink/web_tests/platform/linux/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.png b/third_party/blink/web_tests/platform/linux/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.png
index b9066c4..1127fe6 100644
--- a/third_party/blink/web_tests/platform/linux/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.png
+++ b/third_party/blink/web_tests/platform/linux/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/linux/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.txt b/third_party/blink/web_tests/platform/linux/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.txt
index 099340a..465c136 100644
--- a/third_party/blink/web_tests/platform/linux/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.txt
+++ b/third_party/blink/web_tests/platform/linux/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.txt
@@ -1,13 +1,56 @@
 {
   "layers": [
     {
-      "name": "Scrolling background of LayoutView #document",
+      "name": "Scrolling background of LayoutNGView #document",
       "bounds": [800, 600],
       "contentsOpaque": true,
       "backgroundColor": "#FFFFFF",
       "invalidations": [
         [7, 7, 67, 24]
       ]
+    },
+    {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [57, 16],
+      "drawsContent": false,
+      "transform": 1
+    },
+    {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [74, 16],
+      "invalidations": [
+        [0, 0, 74, 16]
+      ],
+      "transform": 2
+    },
+    {
+      "name": "LayoutNGTextControlSingleLine INPUT id='target'",
+      "position": [7, 7],
+      "bounds": [67, 24],
+      "invalidations": [
+        [0, 0, 67, 24]
+      ]
+    }
+  ],
+  "transforms": [
+    {
+      "id": 1,
+      "transform": [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 1, 0],
+        [12, 11, 0, 1]
+      ]
+    },
+    {
+      "id": 2,
+      "parent": 1,
+      "transform": [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 1, 0],
+        [-17, 0, 0, 1]
+      ]
     }
   ]
 }
diff --git a/third_party/blink/web_tests/platform/linux/virtual/prefer_compositing_to_lcd_text/scrollbars/listbox-scrollbar-combinations-expected.png b/third_party/blink/web_tests/platform/linux/virtual/prefer_compositing_to_lcd_text/scrollbars/listbox-scrollbar-combinations-expected.png
new file mode 100644
index 0000000..0ccecb0
--- /dev/null
+++ b/third_party/blink/web_tests/platform/linux/virtual/prefer_compositing_to_lcd_text/scrollbars/listbox-scrollbar-combinations-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac-mac10.13/fast/forms/form-element-geometry-expected.png b/third_party/blink/web_tests/platform/mac-mac10.13/fast/forms/form-element-geometry-expected.png
new file mode 100644
index 0000000..3b2cbcd
--- /dev/null
+++ b/third_party/blink/web_tests/platform/mac-mac10.13/fast/forms/form-element-geometry-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac-mac11-arm64/fast/forms/basic-inputs-expected.png b/third_party/blink/web_tests/platform/mac-mac11-arm64/fast/forms/basic-inputs-expected.png
index f5cf310..ecd1c69 100644
--- a/third_party/blink/web_tests/platform/mac-mac11-arm64/fast/forms/basic-inputs-expected.png
+++ b/third_party/blink/web_tests/platform/mac-mac11-arm64/fast/forms/basic-inputs-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac-mac11-arm64/scrollbars/listbox-scrollbar-combinations-expected.png b/third_party/blink/web_tests/platform/mac-mac11-arm64/scrollbars/listbox-scrollbar-combinations-expected.png
index 5366fc5..692175e 100644
--- a/third_party/blink/web_tests/platform/mac-mac11-arm64/scrollbars/listbox-scrollbar-combinations-expected.png
+++ b/third_party/blink/web_tests/platform/mac-mac11-arm64/scrollbars/listbox-scrollbar-combinations-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac-mac12-arm64/fast/forms/basic-inputs-expected.png b/third_party/blink/web_tests/platform/mac-mac12-arm64/fast/forms/basic-inputs-expected.png
index f5cf310..ecd1c69 100644
--- a/third_party/blink/web_tests/platform/mac-mac12-arm64/fast/forms/basic-inputs-expected.png
+++ b/third_party/blink/web_tests/platform/mac-mac12-arm64/fast/forms/basic-inputs-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac-mac12-arm64/scrollbars/listbox-scrollbar-combinations-expected.png b/third_party/blink/web_tests/platform/mac-mac12-arm64/scrollbars/listbox-scrollbar-combinations-expected.png
index 5366fc5..692175e 100644
--- a/third_party/blink/web_tests/platform/mac-mac12-arm64/scrollbars/listbox-scrollbar-combinations-expected.png
+++ b/third_party/blink/web_tests/platform/mac-mac12-arm64/scrollbars/listbox-scrollbar-combinations-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac-mac13-arm64/fast/forms/basic-inputs-expected.png b/third_party/blink/web_tests/platform/mac-mac13-arm64/fast/forms/basic-inputs-expected.png
index f5cf310..ecd1c69 100644
--- a/third_party/blink/web_tests/platform/mac-mac13-arm64/fast/forms/basic-inputs-expected.png
+++ b/third_party/blink/web_tests/platform/mac-mac13-arm64/fast/forms/basic-inputs-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac-mac13-arm64/scrollbars/listbox-scrollbar-combinations-expected.png b/third_party/blink/web_tests/platform/mac-mac13-arm64/scrollbars/listbox-scrollbar-combinations-expected.png
index 5366fc5..692175e 100644
--- a/third_party/blink/web_tests/platform/mac-mac13-arm64/scrollbars/listbox-scrollbar-combinations-expected.png
+++ b/third_party/blink/web_tests/platform/mac-mac13-arm64/scrollbars/listbox-scrollbar-combinations-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/fast/forms/basic-inputs-expected.png b/third_party/blink/web_tests/platform/mac/fast/forms/basic-inputs-expected.png
index cd7ebc1..68c1ee31 100644
--- a/third_party/blink/web_tests/platform/mac/fast/forms/basic-inputs-expected.png
+++ b/third_party/blink/web_tests/platform/mac/fast/forms/basic-inputs-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/fast/forms/color-scheme/select/select-multiple-appearance-basic-expected.png b/third_party/blink/web_tests/platform/mac/fast/forms/color-scheme/select/select-multiple-appearance-basic-expected.png
index a6372d3..5ad0792 100644
--- a/third_party/blink/web_tests/platform/mac/fast/forms/color-scheme/select/select-multiple-appearance-basic-expected.png
+++ b/third_party/blink/web_tests/platform/mac/fast/forms/color-scheme/select/select-multiple-appearance-basic-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/fast/forms/form-element-geometry-expected.png b/third_party/blink/web_tests/platform/mac/fast/forms/form-element-geometry-expected.png
index 3b2cbcd..ee3ff37 100644
--- a/third_party/blink/web_tests/platform/mac/fast/forms/form-element-geometry-expected.png
+++ b/third_party/blink/web_tests/platform/mac/fast/forms/form-element-geometry-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/fast/forms/select/listbox-appearance-basic-expected.png b/third_party/blink/web_tests/platform/mac/fast/forms/select/listbox-appearance-basic-expected.png
index 27bb60b..eb4bb3c9 100644
--- a/third_party/blink/web_tests/platform/mac/fast/forms/select/listbox-appearance-basic-expected.png
+++ b/third_party/blink/web_tests/platform/mac/fast/forms/select/listbox-appearance-basic-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/fast/forms/select/listbox-clip-expected.png b/third_party/blink/web_tests/platform/mac/fast/forms/select/listbox-clip-expected.png
index 753b8dc..33893c95 100644
--- a/third_party/blink/web_tests/platform/mac/fast/forms/select/listbox-clip-expected.png
+++ b/third_party/blink/web_tests/platform/mac/fast/forms/select/listbox-clip-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/fast/forms/select/listbox-scrollbar-incremental-load-expected.png b/third_party/blink/web_tests/platform/mac/fast/forms/select/listbox-scrollbar-incremental-load-expected.png
index e79b3e3..683e496 100644
--- a/third_party/blink/web_tests/platform/mac/fast/forms/select/listbox-scrollbar-incremental-load-expected.png
+++ b/third_party/blink/web_tests/platform/mac/fast/forms/select/listbox-scrollbar-incremental-load-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/fast/forms/select/menulist-appearance-basic-expected.png b/third_party/blink/web_tests/platform/mac/fast/forms/select/menulist-appearance-basic-expected.png
index 51422d6..44d9b79 100644
--- a/third_party/blink/web_tests/platform/mac/fast/forms/select/menulist-appearance-basic-expected.png
+++ b/third_party/blink/web_tests/platform/mac/fast/forms/select/menulist-appearance-basic-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/fast/forms/select/select-initial-position-expected.png b/third_party/blink/web_tests/platform/mac/fast/forms/select/select-initial-position-expected.png
index bbc04aa..4699800 100644
--- a/third_party/blink/web_tests/platform/mac/fast/forms/select/select-initial-position-expected.png
+++ b/third_party/blink/web_tests/platform/mac/fast/forms/select/select-initial-position-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/fast/forms/select/select-overflow-scroll-expected.png b/third_party/blink/web_tests/platform/mac/fast/forms/select/select-overflow-scroll-expected.png
index dafa474..ac74ed9c 100644
--- a/third_party/blink/web_tests/platform/mac/fast/forms/select/select-overflow-scroll-expected.png
+++ b/third_party/blink/web_tests/platform/mac/fast/forms/select/select-overflow-scroll-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/fast/forms/select/select-overflow-scroll-inherited-expected.png b/third_party/blink/web_tests/platform/mac/fast/forms/select/select-overflow-scroll-inherited-expected.png
index 9d850842..bbf2cae5 100644
--- a/third_party/blink/web_tests/platform/mac/fast/forms/select/select-overflow-scroll-inherited-expected.png
+++ b/third_party/blink/web_tests/platform/mac/fast/forms/select/select-overflow-scroll-inherited-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/fragmentation/outline-crossing-columns-expected.png b/third_party/blink/web_tests/platform/mac/fragmentation/outline-crossing-columns-expected.png
index 55da340..da974862 100644
--- a/third_party/blink/web_tests/platform/mac/fragmentation/outline-crossing-columns-expected.png
+++ b/third_party/blink/web_tests/platform/mac/fragmentation/outline-crossing-columns-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/paint/invalidation/scroll/caret-invalidation-in-overflow-scroll-expected.txt b/third_party/blink/web_tests/platform/mac/paint/invalidation/scroll/caret-invalidation-in-overflow-scroll-expected.txt
index c6d15230..800b3a4b 100644
--- a/third_party/blink/web_tests/platform/mac/paint/invalidation/scroll/caret-invalidation-in-overflow-scroll-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/paint/invalidation/scroll/caret-invalidation-in-overflow-scroll-expected.txt
@@ -7,6 +7,17 @@
       "backgroundColor": "#FFFFFF"
     },
     {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [20, 15],
+      "drawsContent": false,
+      "transform": 1
+    },
+    {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [168, 15],
+      "transform": 2
+    },
+    {
       "name": "Caret",
       "position": [16, 0],
       "bounds": [1, 15],
@@ -31,6 +42,16 @@
         [0, 0, 1, 0],
         [391, 11, 0, 1]
       ]
+    },
+    {
+      "id": 2,
+      "parent": 1,
+      "transform": [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 1, 0],
+        [-148, 0, 0, 1]
+      ]
     }
   ]
 }
diff --git a/third_party/blink/web_tests/platform/mac/paint/invalidation/scroll/invalidate-caret-in-composited-scrolling-container-expected.txt b/third_party/blink/web_tests/platform/mac/paint/invalidation/scroll/invalidate-caret-in-composited-scrolling-container-expected.txt
index 56fc61f..89c1343 100644
--- a/third_party/blink/web_tests/platform/mac/paint/invalidation/scroll/invalidate-caret-in-composited-scrolling-container-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/paint/invalidation/scroll/invalidate-caret-in-composited-scrolling-container-expected.txt
@@ -10,7 +10,6 @@
       "name": "LayoutNGTextControlSingleLine INPUT id='root'",
       "position": [-1, -1],
       "bounds": [50, 23],
-      "contentsOpaqueForText": true,
       "backgroundColor": "#FFFFFF",
       "invalidations": [
         [0, 0, 50, 23]
@@ -18,6 +17,20 @@
       "transform": 1
     },
     {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [40, 15],
+      "drawsContent": false,
+      "transform": 2
+    },
+    {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [72, 15],
+      "invalidations": [
+        [0, 0, 72, 15]
+      ],
+      "transform": 3
+    },
+    {
       "name": "Caret",
       "position": [38, 0],
       "bounds": [1, 15],
@@ -56,6 +69,16 @@
         [0, 0, 1, 0],
         [4, 3, 0, 1]
       ]
+    },
+    {
+      "id": 3,
+      "parent": 2,
+      "transform": [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 1, 0],
+        [-32, 0, 0, 1]
+      ]
     }
   ]
 }
diff --git a/third_party/blink/web_tests/platform/mac/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.txt b/third_party/blink/web_tests/platform/mac/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.txt
index 38798783..367b49a 100644
--- a/third_party/blink/web_tests/platform/mac/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.txt
@@ -1,13 +1,56 @@
 {
   "layers": [
     {
-      "name": "Scrolling background of LayoutView #document",
+      "name": "Scrolling background of LayoutNGView #document",
       "bounds": [800, 600],
       "contentsOpaque": true,
       "backgroundColor": "#FFFFFF",
       "invalidations": [
         [7, 7, 50, 23]
       ]
+    },
+    {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [40, 15],
+      "drawsContent": false,
+      "transform": 1
+    },
+    {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [72, 15],
+      "invalidations": [
+        [0, 0, 72, 15]
+      ],
+      "transform": 2
+    },
+    {
+      "name": "LayoutNGTextControlSingleLine INPUT id='target'",
+      "position": [7, 7],
+      "bounds": [50, 23],
+      "invalidations": [
+        [0, 0, 50, 23]
+      ]
+    }
+  ],
+  "transforms": [
+    {
+      "id": 1,
+      "transform": [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 1, 0],
+        [12, 11, 0, 1]
+      ]
+    },
+    {
+      "id": 2,
+      "parent": 1,
+      "transform": [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 1, 0],
+        [-32, 0, 0, 1]
+      ]
     }
   ]
 }
diff --git a/third_party/blink/web_tests/platform/mac/scrollbars/listbox-scrollbar-combinations-expected.png b/third_party/blink/web_tests/platform/mac/scrollbars/listbox-scrollbar-combinations-expected.png
index 29392b2..e09af38 100644
--- a/third_party/blink/web_tests/platform/mac/scrollbars/listbox-scrollbar-combinations-expected.png
+++ b/third_party/blink/web_tests/platform/mac/scrollbars/listbox-scrollbar-combinations-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/virtual/controls-refresh-hc/fast/forms/color-scheme/select/select-multiple-appearance-basic-expected.png b/third_party/blink/web_tests/platform/mac/virtual/controls-refresh-hc/fast/forms/color-scheme/select/select-multiple-appearance-basic-expected.png
index b602db9..8ab891b 100644
--- a/third_party/blink/web_tests/platform/mac/virtual/controls-refresh-hc/fast/forms/color-scheme/select/select-multiple-appearance-basic-expected.png
+++ b/third_party/blink/web_tests/platform/mac/virtual/controls-refresh-hc/fast/forms/color-scheme/select/select-multiple-appearance-basic-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/virtual/dark-color-scheme/fast/forms/color-scheme/select/select-multiple-appearance-basic-expected.png b/third_party/blink/web_tests/platform/mac/virtual/dark-color-scheme/fast/forms/color-scheme/select/select-multiple-appearance-basic-expected.png
index 59581e1..db35eba 100644
--- a/third_party/blink/web_tests/platform/mac/virtual/dark-color-scheme/fast/forms/color-scheme/select/select-multiple-appearance-basic-expected.png
+++ b/third_party/blink/web_tests/platform/mac/virtual/dark-color-scheme/fast/forms/color-scheme/select/select-multiple-appearance-basic-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/win/paint/invalidation/scroll/invalidate-caret-in-composited-scrolling-container-expected.txt b/third_party/blink/web_tests/platform/win/paint/invalidation/scroll/invalidate-caret-in-composited-scrolling-container-expected.txt
index 18826eb5..c08c049 100644
--- a/third_party/blink/web_tests/platform/win/paint/invalidation/scroll/invalidate-caret-in-composited-scrolling-container-expected.txt
+++ b/third_party/blink/web_tests/platform/win/paint/invalidation/scroll/invalidate-caret-in-composited-scrolling-container-expected.txt
@@ -10,7 +10,6 @@
       "name": "LayoutNGTextControlSingleLine INPUT id='root'",
       "position": [-1, -1],
       "bounds": [74, 24],
-      "contentsOpaqueForText": true,
       "backgroundColor": "#FFFFFF",
       "invalidations": [
         [0, 0, 74, 24]
@@ -18,6 +17,20 @@
       "transform": 1
     },
     {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [64, 16],
+      "drawsContent": false,
+      "transform": 2
+    },
+    {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [74, 16],
+      "invalidations": [
+        [0, 0, 74, 16]
+      ],
+      "transform": 3
+    },
+    {
       "name": "Caret",
       "position": [63, 0],
       "bounds": [1, 16],
@@ -56,6 +69,16 @@
         [0, 0, 1, 0],
         [4, 3, 0, 1]
       ]
+    },
+    {
+      "id": 3,
+      "parent": 2,
+      "transform": [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 1, 0],
+        [-10, 0, 0, 1]
+      ]
     }
   ]
 }
diff --git a/third_party/blink/web_tests/platform/win/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.txt b/third_party/blink/web_tests/platform/win/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.txt
index 701632d..2b4686a0 100644
--- a/third_party/blink/web_tests/platform/win/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.txt
+++ b/third_party/blink/web_tests/platform/win/paint/invalidation/selection/selection-in-composited-scrolling-container-expected.txt
@@ -1,13 +1,56 @@
 {
   "layers": [
     {
-      "name": "Scrolling background of LayoutView #document",
+      "name": "Scrolling background of LayoutNGView #document",
       "bounds": [800, 600],
       "contentsOpaque": true,
       "backgroundColor": "#FFFFFF",
       "invalidations": [
         [7, 7, 74, 24]
       ]
+    },
+    {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [64, 16],
+      "drawsContent": false,
+      "transform": 1
+    },
+    {
+      "name": "LayoutNGTextControlInnerEditor DIV",
+      "bounds": [74, 16],
+      "invalidations": [
+        [0, 0, 74, 16]
+      ],
+      "transform": 2
+    },
+    {
+      "name": "LayoutNGTextControlSingleLine INPUT id='target'",
+      "position": [7, 7],
+      "bounds": [74, 24],
+      "invalidations": [
+        [0, 0, 74, 24]
+      ]
+    }
+  ],
+  "transforms": [
+    {
+      "id": 1,
+      "transform": [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 1, 0],
+        [12, 11, 0, 1]
+      ]
+    },
+    {
+      "id": 2,
+      "parent": 1,
+      "transform": [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 1, 0],
+        [-10, 0, 0, 1]
+      ]
     }
   ]
 }
diff --git a/third_party/wlcs/LICENSE b/third_party/wlcs/LICENSE
new file mode 100644
index 0000000..44325404
--- /dev/null
+++ b/third_party/wlcs/LICENSE
@@ -0,0 +1,676 @@
+
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+ 
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+  
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+		     END OF TERMS AND CONDITIONS
+
+	    How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+
diff --git a/third_party/wlcs/OWNERS b/third_party/wlcs/OWNERS
new file mode 100644
index 0000000..e2a876d
--- /dev/null
+++ b/third_party/wlcs/OWNERS
@@ -0,0 +1,2 @@
+file://chrome/browser/ash/guest_os/OWNERS
+file://components/exo/OWNERS
diff --git a/third_party/wlcs/README.chromium b/third_party/wlcs/README.chromium
new file mode 100644
index 0000000..f333888
--- /dev/null
+++ b/third_party/wlcs/README.chromium
@@ -0,0 +1,24 @@
+Name: Wayland Conformance Test Suite
+Short Name: wlcs
+URL: https://github.com/MirServer/wlcs
+Version: 0
+Date: 2023-02-24
+Revision: 2930ad4b5ca602446ad211b49fb1827303ce9f4b
+License: GPL2 and GPL3
+License File: NOT_SHIPPED
+Security Critical: no
+
+Description:
+WLCS is a test suite which verifies protocol-level conformance for
+wayland compositors. This code will be used in two ways:
+1) A subset of header files (distributed under GPL3) will be #included
+   in chromium code to allow the test harness to interface with our
+   code.
+2) Test binaries can be built by automated builders or local developers
+   in order to gauge the conformance of chromium's wayland compositor
+   against the suite.
+This code is licensed under the GPL2 and GPL3, see src/COPYING.* for
+details.
+
+Local Modifications:
+<none>
diff --git a/third_party/zlib/BUILD.gn b/third_party/zlib/BUILD.gn
index b85067a1..d20c468 100644
--- a/third_party/zlib/BUILD.gn
+++ b/third_party/zlib/BUILD.gn
@@ -4,6 +4,12 @@
 
 import("//build/config/compiler/compiler.gni")
 
+declare_args() {
+  # Expose zlib's symbols, used by Node.js to provide zlib APIs for its native
+  # modules.
+  zlib_symbols_visible = false
+}
+
 if (build_with_chromium) {
   import("//testing/test.gni")
 }
@@ -14,6 +20,10 @@
 
 config("zlib_config") {
   include_dirs = [ "." ]
+
+  if (zlib_symbols_visible) {
+    defines = [ "ZLIB_DLL" ]
+  }
 }
 
 config("zlib_internal_config") {
@@ -358,6 +368,11 @@
   configs -= [ "//build/config/compiler:chromium_code" ]
   configs += [ "//build/config/compiler:no_chromium_code" ]
 
+  if (zlib_symbols_visible) {
+    configs -= [ "//build/config/gcc:symbol_visibility_hidden" ]
+    configs += [ "//build/config/gcc:symbol_visibility_default" ]
+  }
+
   public_configs = [ ":zlib_config" ]
 
   configs += [
diff --git a/tools/mb/mb_config.pyl b/tools/mb/mb_config.pyl
index 8460cae05..ae87da0 100644
--- a/tools/mb/mb_config.pyl
+++ b/tools/mb/mb_config.pyl
@@ -382,7 +382,7 @@
         'build2': 'ios_simulator_debug_static_bot_xctest_reclient',
       },
       'Linux Builder (j-500) (reclient)': 'gpu_tests_release_bot_reclient',
-      'Linux Builder (reclient compare)': 'gpu_tests_release_bot_reclient',
+      'Linux Builder (reclient compare)': 'gpu_tests_release_bot_remote_links_small_reclient',
       'Linux Viz': 'release_trybot_minimal_symbols_reclient',
       # TODO(crbug.com/1260232): remove this after the migration.
       'Mac Builder (reclient compare)': 'gpu_tests_release_bot_minimal_symbols_reclient',
diff --git a/tools/mb/mb_config_expectations/chromium.fyi.json b/tools/mb/mb_config_expectations/chromium.fyi.json
index c27e36f..71293b9 100644
--- a/tools/mb/mb_config_expectations/chromium.fyi.json
+++ b/tools/mb/mb_config_expectations/chromium.fyi.json
@@ -243,12 +243,16 @@
   },
   "Linux Builder (reclient compare)": {
     "gn_args": {
+      "concurrent_links": 50,
       "dcheck_always_on": false,
+      "enable_nacl": false,
       "ffmpeg_branding": "Chrome",
       "is_component_build": false,
       "is_debug": false,
       "proprietary_codecs": true,
-      "use_remoteexec": true
+      "rbe_link_cfg_file": "../../buildtools/reclient_cfgs/chromium-browser-clang/rewrapper_linux_link_small.cfg",
+      "use_remoteexec": true,
+      "use_remoteexec_links": true
     }
   },
   "Linux Viz": {
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 267fe24..475ac395 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -4585,18 +4585,6 @@
   <int value="7" label="Decorated Media Custom View Style"/>
 </enum>
 
-<enum name="ArcNoWindowReason">
-  <int value="0" label="NoHandler"/>
-  <int value="1" label="NoHandler (from crash)"/>
-  <int value="2" label="NoRootBounds"/>
-  <int value="3" label="NoRootBounds (from crash)"/>
-  <int value="4" label="NoScreenBounds"/>
-  <int value="5" label="NoScreenBounds (from crash)"/>
-  <int value="6" label="Flag disabled"/>
-  <int value="7" label="Not ARCVM"/>
-  <int value="8" label="No exo helper"/>
-</enum>
-
 <enum name="ArcOptInAction">
   <summary>Defines Arc OptIn actions</summary>
   <int value="0" label="DEPRECATED: Opted Out"/>
@@ -25495,6 +25483,11 @@
   <int value="50" label="Calculator"/>
   <int value="51" label="FirmwareUpdateApp"/>
   <int value="52" label="Google TV"/>
+  <int value="53" label="Google Calendar"/>
+  <int value="54" label="Google Chat"/>
+  <int value="55" label="Google Meet"/>
+  <int value="56" label="Google Maps"/>
+  <int value="57" label="Google Messages"/>
 </enum>
 
 <enum name="DefaultBrowserAsyncAttemptResult">
@@ -36624,7 +36617,7 @@
   <int value="63" label="kDeprecated_ExternallyConnectableAllUrls"/>
   <int value="64" label="kFeedbackPrivate"/>
   <int value="65" label="kFileBrowserHandler"/>
-  <int value="66" label="kFileBrowserHandlerInternal"/>
+  <int value="66" label="kDeleted_FileBrowserHandlerInternal"/>
   <int value="67" label="kFileManagerPrivate"/>
   <int value="68" label="kFileSystem"/>
   <int value="69" label="kFileSystemDirectory"/>
@@ -42386,6 +42379,12 @@
   <int value="4487" label="PrivateAggregationApiFledgeExtensions"/>
   <int value="4488" label="DeprecatedInterestGroupDailyUpdateUrl"/>
   <int value="4489" label="CSSColorGradientColorSpace"/>
+  <int value="4490" label="DanglingMarkupInWindowName"/>
+  <int value="4491" label="DanglingMarkupInWindowNameNotEndsWithNewLineOrGT"/>
+  <int value="4492" label="DanglingMarkupInWindowNameNotEndsWithGT"/>
+  <int value="4493" label="DanglingMarkupInTarget"/>
+  <int value="4494" label="DanglingMarkupInTargetNotEndsWithGT"/>
+  <int value="4495" label="DanglingMarkupInTargetNotEndsWithNewLineOrGT"/>
 </enum>
 
 <enum name="FeaturePolicyAllowlistType">
@@ -49378,9 +49377,23 @@
       label="Search canceled because the search backend isn't available"/>
 </enum>
 
+<enum name="HelpAppSearchConceptReadStatus">
+  <int value="0" label="Ok"/>
+  <int value="1" label="File not exist"/>
+  <int value="2" label="Read error"/>
+  <int value="3" label="Parse error"/>
+</enum>
+
+<enum name="HelpAppSearchConceptWriteStatus">
+  <int value="0" label="Ok"/>
+  <int value="1" label="Write error"/>
+  <int value="2" label="Serialization error"/>
+  <int value="3" label="Replace error"/>
+</enum>
+
 <enum name="HelpAppSearchHandlerSearchResultStatus">
   <int value="0" label="Not ready, empty index"/>
-  <int value="1" label="Not ready, other status (not empty index)"/>
+  <int value="1" label="Not ready, updating"/>
   <int value="2" label="Ready and success"/>
   <int value="3" label="Ready, empty index (not success)"/>
   <int value="4" label="Ready, other status (not success, not empty index)"/>
@@ -60195,6 +60208,7 @@
   <int value="-990704221" label="TerminalSSH:enabled"/>
   <int value="-990187062" label="SendTabToSelfShowSendingUI:enabled"/>
   <int value="-990031172" label="FedCmIframeSupport:disabled"/>
+  <int value="-990002281" label="OmniboxAdaptNarrowTabletWindows:disabled"/>
   <int value="-989671895" label="OfflineIndicatorAlwaysHttpProbe:enabled"/>
   <int value="-989140298" label="UseSyncInvalidations:disabled"/>
   <int value="-989050085" label="AppStoreBillingDebug:enabled"/>
@@ -62101,7 +62115,6 @@
   <int value="86233339" label="MessagesForAndroidOfferNotification:disabled"/>
   <int value="86900696" label="SanitizerAPIv0:enabled"/>
   <int value="87306743" label="VariationsFakeCrashAfterStartup:disabled"/>
-  <int value="88249612" label="CalendarView:enabled"/>
   <int value="88437020" label="FeaturePolicy:enabled"/>
   <int value="88863813" label="DesktopPWAsDetailedInstallDialog:enabled"/>
   <int value="89357752" label="PdfXfaSupport:enabled"/>
@@ -63410,7 +63423,6 @@
       label="AutofillRationalizeStreetAddressAndAddressLine:disabled"/>
   <int value="836406476" label="EnableTouchableAppContextMenu:enabled"/>
   <int value="837350465" label="SystemProxyForSystemServices:enabled"/>
-  <int value="838300788" label="CalendarView:disabled"/>
   <int value="838887742" label="manual-enhanced-bookmarks"/>
   <int value="839230937" label="AutofillEnableCardNicknameUpstream:disabled"/>
   <int value="839798268" label="SafeBrowsingTelemetryForApkDownloads:disabled"/>
@@ -64795,6 +64807,7 @@
   <int value="1621298798" label="VrBrowserKeyboard:enabled"/>
   <int value="1622131033" label="ozone-test-single-overlay-support"/>
   <int value="1622672308" label="ReaderMode:enabled"/>
+  <int value="1623345369" label="OmniboxAdaptNarrowTabletWindows:enabled"/>
   <int value="1625988511" label="DurableClientHintsCache:enabled"/>
   <int value="1626824478" label="ExperimentalAppBanners:disabled"/>
   <int value="1627416551" label="ImeInputLogicMozc:disabled"/>
@@ -81219,6 +81232,18 @@
   <int value="3" label="Reset from allowed state"/>
 </enum>
 
+<enum name="PermissionChangeInfo">
+  <int value="0" label="Infobar shown, page reloaded, permission used"/>
+  <int value="1" label="Infobar shown, page reloaded, permission not used"/>
+  <int value="2" label="Infobar shown, no page reloaded, permission used"/>
+  <int value="3" label="Infobar shown, no page reloaded, permission not used"/>
+  <int value="4" label="Infobar not shown, page reloaded, permission used"/>
+  <int value="5" label="Infobar not shown, page reloaded, permission not used"/>
+  <int value="6" label="Infobar not shown, no page reloaded, permission used"/>
+  <int value="7"
+      label="Infobar not shown, no page reloaded, permission not used"/>
+</enum>
+
 <enum name="PermissionEmbargoStatus">
   <int value="0" label="NOT_EMBARGOED"/>
   <int value="1" label="BLACKLISTED"/>
@@ -90485,6 +90510,7 @@
   <int value="3" label="SUCCESS_LANDING_REFERRER"/>
   <int value="4" label="INVALID_URL"/>
   <int value="5" label="NAVIGATION_EVENT_NOT_FOUND"/>
+  <int value="6" label="SUCCESS_REFERRER"/>
 </enum>
 
 <enum name="SafeBrowsingBinaryUploadResult">
@@ -112668,6 +112694,7 @@
   <int value="54" label="Show Chrome What's New"/>
   <int value="55" label="Trigger Lacros data migration"/>
   <int value="56" label="Menu opened"/>
+  <int value="57" label="Visited Chrome Web Store via extensions sub menu."/>
 </enum>
 
 <enum name="WrongConfigurationMetric">
diff --git a/tools/metrics/histograms/metadata/accessibility/histograms.xml b/tools/metrics/histograms/metadata/accessibility/histograms.xml
index 4b22051..4c8b135 100644
--- a/tools/metrics/histograms/metadata/accessibility/histograms.xml
+++ b/tools/metrics/histograms/metadata/accessibility/histograms.xml
@@ -1463,6 +1463,20 @@
   </summary>
 </histogram>
 
+<histogram
+    name="Accessibility.LiveCaption.{SodaLanguageCode}.SessionContainsRecognizedSpeech"
+    enum="BooleanEnabled" expires_after="2023-06-01">
+  <owner>evliu@google.com</owner>
+  <owner>chrome-media-ux@google.com</owner>
+  <owner>chrome-a11y-core@google.com</owner>
+  <summary>
+    Whether the speech recognition session contains any recognized speech. This
+    is logged once per media stream upon the destruction of the
+    SpeechRecognitionRecognizerImpl.
+  </summary>
+  <token key="SodaLanguageCode" variants="SodaLanguageCode"/>
+</histogram>
+
 <histogram name="Accessibility.LiveCaption2" enum="BooleanEnabled"
     expires_after="2023-11-30">
   <owner>katie@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/help_app/histograms.xml b/tools/metrics/histograms/metadata/help_app/histograms.xml
index 29c30e9..395007b1 100644
--- a/tools/metrics/histograms/metadata/help_app/histograms.xml
+++ b/tools/metrics/histograms/metadata/help_app/histograms.xml
@@ -34,16 +34,57 @@
   </summary>
 </histogram>
 
+<histogram name="Discover.SearchConcept.ReadStatus"
+    enum="HelpAppSearchConceptReadStatus" expires_after="2023-08-08">
+  <owner>chenjih@google.com</owner>
+  <owner>tby@chromium.org</owner>
+  <owner>showoff-eng@google.com</owner>
+  <summary>
+    Various status codes on reading persistence file from disk, in the search
+    handler used by the ChromeOS launcher. Emitted once when a read happens to
+    the persistence file, and this happens once per login. Some number of file
+    not exist values is expected, because the persistence file does not exist
+    before it is written for the first time.
+  </summary>
+</histogram>
+
+<histogram name="Discover.SearchConcept.WriteStatus"
+    enum="HelpAppSearchConceptWriteStatus" expires_after="2023-08-08">
+  <owner>chenjih@google.com</owner>
+  <owner>tby@chromium.org</owner>
+  <owner>showoff-eng@google.com</owner>
+  <summary>
+    Various status codes on writing persistence file back to disk, in the search
+    handler used by the ChromeOS launcher. Emitted once when a write happens to
+    the persistence file, and this happens once per new web contents arrives,
+    which is expected to be approx. once per login.
+  </summary>
+</histogram>
+
+<histogram name="Discover.SearchHandler.SearchAvailableTime" units="ms"
+    expires_after="2023-08-08">
+  <owner>chenjih@google.com</owner>
+  <owner>tby@chromium.org</owner>
+  <owner>showoff-eng@google.com</owner>
+  <summary>
+    The latency of the help search availability after the help app search
+    handler constructed. Recorded each time a help app search handler is
+    constructed.
+  </summary>
+</histogram>
+
 <histogram name="Discover.SearchHandler.SearchResultStatus"
     enum="HelpAppSearchHandlerSearchResultStatus" expires_after="2023-08-08">
+  <owner>chenjih@google.com</owner>
+  <owner>tby@chromium.org</owner>
   <owner>zufeng@google.com</owner>
   <owner>showoff-eng@google.com</owner>
   <summary>
     The end result of a search using the help app search handler. Logged once
     per time a search finishes. Not logged if the search is canceled by a new
     search starting. Use this to calculate the proportion of searches where the
-    search handler is ready vs not ready. &quot;Ready&quot; means the first
-    update finished and the search handler is ready to handle searches.
+    search handler is ready vs not ready. &quot;Ready&quot; means the update
+    finished and the search handler is ready to handle searches.
   </summary>
 </histogram>
 
diff --git a/tools/metrics/histograms/metadata/histogram_suffixes_list.xml b/tools/metrics/histograms/metadata/histogram_suffixes_list.xml
index 270c1a8..78c8ef0 100644
--- a/tools/metrics/histograms/metadata/histogram_suffixes_list.xml
+++ b/tools/metrics/histograms/metadata/histogram_suffixes_list.xml
@@ -6265,6 +6265,10 @@
   <affected-histogram name="Media.VideoCapture.Windows.ImageCaptureOutcome"/>
 </histogram_suffixes>
 
+<!-- TODO(crbug.com/401026): Convert to Patterned Histograms:
+  chromium.googlesource.com/chromium/src/+/HEAD/tools/metrics/histograms/README.md#patterned-histograms
+-->
+
 <histogram_suffixes name="WrenchMenuActionTimings" separator=".">
   <suffix name="About" label=""/>
   <suffix name="AppInfo" label=""/>
@@ -6313,6 +6317,7 @@
   <suffix name="SiteSettings" label=""/>
   <suffix name="TaskManager" label=""/>
   <suffix name="ViewSource" label=""/>
+  <suffix name="VisitChromeWebStore" label=""/>
   <suffix name="Win8MetroRestart" label=""/>
   <suffix name="WinDesktopRestart" label=""/>
   <suffix name="ZoomMinus" label=""/>
diff --git a/tools/metrics/histograms/metadata/permissions/histograms.xml b/tools/metrics/histograms/metadata/permissions/histograms.xml
index af528b0..c0980a20 100644
--- a/tools/metrics/histograms/metadata/permissions/histograms.xml
+++ b/tools/metrics/histograms/metadata/permissions/histograms.xml
@@ -654,6 +654,20 @@
   <token key="PermissionType" variants="Top2PermissionTypes"/>
 </histogram>
 
+<histogram
+    name="Permissions.PageInfo.Changed.{PermissionType}.Reallowed.Outcome"
+    enum="PermissionChangeInfo" expires_after="2023-10-31">
+  <owner>elklm@chromium.org</owner>
+  <owner>src/components/permissions/PERMISSIONS_OWNERS</owner>
+  <summary>
+    Records if a user manually reallowed permission state via PageInfo. Recorded
+    when the user closes a tab, makes cross-origin navigation, or if permission
+    is used by an origin. Grouped by whether permission was used, the page was
+    reloaded and a &quot;Reload this page&quot; infobar was shown.
+  </summary>
+  <token key="PermissionType" variants="AllPermissionTypes"/>
+</histogram>
+
 <histogram name="Permissions.PageInfo.Changed.{PermissionType}.{ReloadInfoBar}"
     enum="PermissionChangeAction" expires_after="2023-10-31">
   <owner>elklm@chromium.org</owner>
diff --git a/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc b/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc
index d2fb716..7dcf7090 100644
--- a/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc
+++ b/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc
@@ -5353,66 +5353,16 @@
 
 TEST_F(AXPlatformNodeTextRangeProviderTest,
        FindTextWithEmbeddedObjectCharacter) {
-  // ++1 kRootWebArea
-  // ++++2 kList
-  // ++++++3 kListItem
-  // ++++++++4 kStaticText
-  // ++++++++++5 kInlineTextBox
-  // ++++++6 kListItem
-  // ++++++++7 kStaticText
-  // ++++++++++8 kInlineTextBox
-  ui::AXNodeData root_1;
-  ui::AXNodeData list_2;
-  ui::AXNodeData list_item_3;
-  ui::AXNodeData static_text_4;
-  ui::AXNodeData inline_box_5;
-  ui::AXNodeData list_item_6;
-  ui::AXNodeData static_text_7;
-  ui::AXNodeData inline_box_8;
-
-  root_1.id = 1;
-  list_2.id = 2;
-  list_item_3.id = 3;
-  static_text_4.id = 4;
-  inline_box_5.id = 5;
-  list_item_6.id = 6;
-  static_text_7.id = 7;
-  inline_box_8.id = 8;
-
-  root_1.role = ax::mojom::Role::kRootWebArea;
-  root_1.child_ids = {list_2.id};
-
-  list_2.role = ax::mojom::Role::kList;
-  list_2.child_ids = {list_item_3.id, list_item_6.id};
-
-  list_item_3.role = ax::mojom::Role::kListItem;
-  list_item_3.child_ids = {static_text_4.id};
-
-  static_text_4.role = ax::mojom::Role::kStaticText;
-  static_text_4.SetName("foo");
-  static_text_4.child_ids = {inline_box_5.id};
-
-  inline_box_5.role = ax::mojom::Role::kInlineTextBox;
-  inline_box_5.SetName("foo");
-
-  list_item_6.role = ax::mojom::Role::kListItem;
-  list_item_6.child_ids = {static_text_7.id};
-
-  static_text_7.role = ax::mojom::Role::kStaticText;
-  static_text_7.child_ids = {inline_box_8.id};
-  static_text_7.SetName("bar");
-
-  inline_box_8.role = ax::mojom::Role::kInlineTextBox;
-  inline_box_8.SetName("bar");
-
-  ui::AXTreeUpdate update;
-  ui::AXTreeData tree_data;
-  tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID();
-  update.tree_data = tree_data;
-  update.has_tree_data = true;
-  update.root_id = root_1.id;
-  update.nodes = {root_1, list_2, list_item_3, static_text_4,
-                  inline_box_5, list_item_6, static_text_7, inline_box_8};
+  TestAXTreeUpdate update(std::string(R"HTML(
+    ++1 kRootWebArea
+    ++++2 kList
+    ++++++3 kListItem
+    ++++++++4 kStaticText name="foo"
+    ++++++++++5 kInlineTextBox name="foo"
+    ++++++6 kListItem
+    ++++++++7 kStaticText name="bar"
+    ++++++++++8 kInlineTextBox name="bar"
+  )HTML"));
 
   Init(update);
 
@@ -6573,75 +6523,25 @@
 
 TEST_F(AXPlatformNodeTextRangeProviderTest,
        TestNormalizeTextRangeForceSameAnchorOnDegenerateRange) {
-  // ++1 kRootWebArea
-  // ++++2 kGenericContainer
-  // ++++++3 kImage
-  // ++++4 kTextField
-  // ++++++5 kGenericContainer
-  // ++++++++6 kStaticText
-  // ++++++++++7 kInlineTextBox
-  ui::AXNodeData root_1;
-  ui::AXNodeData generic_container_2;
-  ui::AXNodeData line_break_3;
-  ui::AXNodeData text_field_4;
-  ui::AXNodeData generic_container_5;
-  ui::AXNodeData static_text_6;
-  ui::AXNodeData inline_box_7;
+  TestAXTreeUpdate update(std::string(R"HTML(
+    ++1 kRootWebArea
+    ++++2 kGenericContainer boolAttribute=kIsLineBreakingObject,true
+    ++++++3 kImage
+    ++++4 kTextField state=kEditable
+    ++++++5 kGenericContainer
+    ++++++++6 kStaticText name="3.14"
+    ++++++++++7 kInlineTextBox name="3.14"
+  )HTML"));
 
-  root_1.id = 1;
-  generic_container_2.id = 2;
-  line_break_3.id = 3;
-  text_field_4.id = 4;
-  generic_container_5.id = 5;
-  static_text_6.id = 6;
-  inline_box_7.id = 7;
-
-  root_1.role = ax::mojom::Role::kRootWebArea;
-  root_1.child_ids = {generic_container_2.id, text_field_4.id};
-
-  generic_container_2.role = ax::mojom::Role::kGenericContainer;
-  generic_container_2.AddBoolAttribute(
-      ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
-  generic_container_2.child_ids = {line_break_3.id};
-
-  line_break_3.role = ax::mojom::Role::kLineBreak;
-
-  text_field_4.role = ax::mojom::Role::kTextField;
-  text_field_4.AddState(ax::mojom::State::kEditable);
-  text_field_4.child_ids = {generic_container_5.id};
-  text_field_4.SetValue("3.14");
-
-  generic_container_5.role = ax::mojom::Role::kGenericContainer;
-  generic_container_5.child_ids = {static_text_6.id};
-
-  static_text_6.role = ax::mojom::Role::kStaticText;
-  static_text_6.child_ids = {inline_box_7.id};
-  static_text_6.SetName("3.14");
-
-  inline_box_7.role = ax::mojom::Role::kInlineTextBox;
-  inline_box_7.SetName("3.14");
-
-  ui::AXTreeUpdate update;
-  ui::AXTreeData tree_data;
-  tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID();
-  update.tree_data = tree_data;
-  update.has_tree_data = true;
-  update.root_id = root_1.id;
-  update.nodes.push_back(root_1);
-  update.nodes.push_back(generic_container_2);
-  update.nodes.push_back(line_break_3);
-  update.nodes.push_back(text_field_4);
-  update.nodes.push_back(generic_container_5);
-  update.nodes.push_back(static_text_6);
-  update.nodes.push_back(inline_box_7);
+  update.nodes[3].SetValue("3.14");
 
   const AXTree* tree = Init(update);
 
-  const AXNode* line_break_3_node = tree->GetFromId(line_break_3.id);
-  const AXNode* inline_box_7_node = tree->GetFromId(inline_box_7.id);
+  const AXNode* line_break_3_node = tree->GetFromId(3);
+  const AXNode* inline_box_7_node = tree->GetFromId(7);
 
   AXPlatformNodeWin* owner = static_cast<AXPlatformNodeWin*>(
-      AXPlatformNodeFromNode(GetNodeFromTree(tree_data.tree_id, 1)));
+      AXPlatformNodeFromNode(GetNodeFromTree(tree->GetAXTreeID(), 1)));
 
   // start: TextPosition, anchor_id=3, text_offset=1, annotated_text=/xFFFC<>
   // end  : TextPosition, anchor_id=7, text_offset=0, annotated_text=<p>i
@@ -7517,75 +7417,22 @@
   // normalize to other positions, so we should expect the
   // 'UIA_IsReadOnlyAttributeId' attribute queried at this position to return
   // false.
-  // ++1 kRootWebArea
-  // ++++2 kTextField editable value="hello"
-  // ++++++3 kGenericContainer editable isLineBreakingObject=true
-  // ++++++++4 kStaticText editable name="hello"
-  // ++++++++++5 kInlineTextBox editable name="hello"
-  // ++++6 kStaticText name="abc"
-  // ++++++7 kInlineTextBox name="abc"
-  AXNodeData root_1;
-  AXNodeData text_field_2;
-  AXNodeData generic_container_3;
-  AXNodeData static_text_4;
-  AXNodeData inline_text_5;
-  AXNodeData static_text_6;
-  AXNodeData inline_text_7;
-
-  root_1.id = 1;
-  text_field_2.id = 2;
-  generic_container_3.id = 3;
-  static_text_4.id = 4;
-  inline_text_5.id = 5;
-  static_text_6.id = 6;
-  inline_text_7.id = 7;
-
-  root_1.role = ax::mojom::Role::kRootWebArea;
-  root_1.child_ids = {text_field_2.id, static_text_6.id};
-
-  text_field_2.role = ax::mojom::Role::kTextField;
-  text_field_2.AddState(ax::mojom::State::kEditable);
-  text_field_2.SetValue("hello");
-  text_field_2.child_ids = {generic_container_3.id};
-
-  generic_container_3.role = ax::mojom::Role::kGenericContainer;
-  generic_container_3.AddState(ax::mojom::State::kEditable);
-  generic_container_3.AddBoolAttribute(
-      ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
-  generic_container_3.child_ids = {static_text_4.id};
-
-  static_text_4.role = ax::mojom::Role::kStaticText;
-  static_text_4.SetName("hello");
-  static_text_4.AddState(ax::mojom::State::kEditable);
-  static_text_4.child_ids = {inline_text_5.id};
-
-  inline_text_5.role = ax::mojom::Role::kInlineTextBox;
-  inline_text_5.SetName("hello");
-  inline_text_5.AddState(ax::mojom::State::kEditable);
-
-  static_text_6.role = ax::mojom::Role::kStaticText;
-  static_text_6.SetName("abc");
-  static_text_6.child_ids = {inline_text_7.id};
-
-  inline_text_7.role = ax::mojom::Role::kInlineTextBox;
-  inline_text_7.SetName("abc");
-
-  ui::AXTreeUpdate update;
-  ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID();
-  update.root_id = root_1.id;
-  update.tree_data.tree_id = tree_id;
-  update.has_tree_data = true;
-  update.nodes = {root_1,        text_field_2,  generic_container_3,
-                  static_text_4, inline_text_5, static_text_6,
-                  inline_text_7};
+  TestAXTreeUpdate update(std::string(R"HTML(
+    ++1 kRootWebArea
+    ++++2 kTextField state=kEditable
+    ++++++3 kGenericContainer state=kEditable boolAttribute=kIsLineBreakingObject,true
+    ++++++++4 kStaticText state=kEditable name="hello"
+    ++++++++++5 kInlineTextBox state=kEditable name="hello"
+    ++++6 kStaticText name="abc"
+  )HTML"));
 
   const AXTree* tree = Init(update);
-  const AXNode* inline_text_5_node = tree->GetFromId(inline_text_5.id);
+  const AXNode* inline_text_5_node = tree->GetFromId(5);
 
   // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers|
   // will build the entire tree.
   AXPlatformNodeWin* owner = static_cast<AXPlatformNodeWin*>(
-      AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1)));
+      AXPlatformNodeFromNode(GetNodeFromTree(tree->GetAXTreeID(), 1)));
 
   ComPtr<AXPlatformNodeTextRangeProviderWin> range;
   base::win::ScopedVariant expected_variant;
@@ -7637,98 +7484,32 @@
   // ends at the beginning of the next paragraph. The range only contains the
   // generated newline character. The readonly attribute value returned should
   // be the one of the common anchor of the start and end endpoint.
-
-  // ++1 kRootWebArea
-  // ++++2 kGenericContainer
-  // ++++++3 kImage
-  // ++++++4 kTextField editable
-  // ++++5 kGenericContainer editable
-  // ++++++6 kImage
-  // ++++++7 kTextField editable
-  // ++++8 kGenericContainer
-  // ++++++9 kTextField editable
-  // ++++++10 kTextField editable
-  AXNodeData root_1;
-  AXNodeData generic_container_2;
-  AXNodeData image_3;
-  AXNodeData text_field_4;
-  AXNodeData generic_container_5;
-  AXNodeData image_6;
-  AXNodeData text_field_7;
-  AXNodeData generic_container_8;
-  AXNodeData text_field_9;
-  AXNodeData text_field_10;
-
-  root_1.id = 1;
-  generic_container_2.id = 2;
-  image_3.id = 3;
-  text_field_4.id = 4;
-  generic_container_5.id = 5;
-  image_6.id = 6;
-  text_field_7.id = 7;
-  generic_container_8.id = 8;
-  text_field_9.id = 9;
-  text_field_10.id = 10;
-
-  root_1.role = ax::mojom::Role::kRootWebArea;
-  root_1.child_ids = {generic_container_2.id, generic_container_5.id,
-                      generic_container_8.id};
-
-  generic_container_2.role = ax::mojom::Role::kGenericContainer;
-  generic_container_2.child_ids = {image_3.id, text_field_4.id};
-
-  image_3.role = ax::mojom::Role::kImage;
-  image_3.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
-                           true);
-
-  text_field_4.role = ax::mojom::Role::kTextField;
-  text_field_4.AddState(ax::mojom::State::kEditable);
-
-  generic_container_5.role = ax::mojom::Role::kGenericContainer;
-  generic_container_5.AddState(ax::mojom::State::kEditable);
-  generic_container_5.child_ids = {image_6.id, text_field_7.id};
-
-  image_6.role = ax::mojom::Role::kImage;
-  image_6.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
-                           true);
-
-  text_field_7.role = ax::mojom::Role::kTextField;
-  text_field_7.AddState(ax::mojom::State::kEditable);
-
-  generic_container_8.role = ax::mojom::Role::kGenericContainer;
-  generic_container_8.child_ids = {text_field_9.id, text_field_10.id};
-
-  text_field_9.role = ax::mojom::Role::kTextField;
-  text_field_9.AddState(ax::mojom::State::kEditable);
-  text_field_9.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
-                                true);
-
-  text_field_10.role = ax::mojom::Role::kTextField;
-  text_field_10.AddState(ax::mojom::State::kEditable);
-
-  ui::AXTreeUpdate update;
-  ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID();
-  update.root_id = root_1.id;
-  update.tree_data.tree_id = tree_id;
-  update.has_tree_data = true;
-  update.nodes = {root_1,       generic_container_2, image_3,
-                  text_field_4, generic_container_5, image_6,
-                  text_field_7, generic_container_8, text_field_9,
-                  text_field_10};
+  TestAXTreeUpdate update(std::string(R"HTML(
+    ++1 kRootWebArea
+    ++++2 kGenericContainer
+    ++++++3 kImage boolAttribute=kIsLineBreakingObject,true
+    ++++++4 kTextField state=kEditable
+    ++++5 kGenericContainer state=kEditable
+    ++++++6 kImage boolAttribute=kIsLineBreakingObject,true
+    ++++++7 kTextField state=kEditable
+    ++++8 kGenericContainer
+    ++++++9 kTextField state=kEditable boolAttribute=kIsLineBreakingObject,true
+    ++++++10 kTextField state=kEditable
+  )HTML"));
 
   const AXTree* tree = Init(update);
 
-  const AXNode* image_3_node = tree->GetFromId(image_3.id);
-  const AXNode* image_6_node = tree->GetFromId(image_6.id);
-  const AXNode* text_field_4_node = tree->GetFromId(text_field_4.id);
-  const AXNode* text_field_7_node = tree->GetFromId(text_field_7.id);
-  const AXNode* text_field_9_node = tree->GetFromId(text_field_9.id);
-  const AXNode* text_field_10_node = tree->GetFromId(text_field_10.id);
+  const AXNode* image_3_node = tree->GetFromId(3);
+  const AXNode* image_6_node = tree->GetFromId(6);
+  const AXNode* text_field_4_node = tree->GetFromId(4);
+  const AXNode* text_field_7_node = tree->GetFromId(7);
+  const AXNode* text_field_9_node = tree->GetFromId(9);
+  const AXNode* text_field_10_node = tree->GetFromId(10);
 
   // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers|
   // will build the entire tree.
   AXPlatformNodeWin* owner = static_cast<AXPlatformNodeWin*>(
-      AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1)));
+      AXPlatformNodeFromNode(GetNodeFromTree(tree->GetAXTreeID(), 1)));
 
   base::win::ScopedVariant expected_variant;
 
diff --git a/ui/aura/window_event_dispatcher.cc b/ui/aura/window_event_dispatcher.cc
index 3820ceb2..720f9cc7 100644
--- a/ui/aura/window_event_dispatcher.cc
+++ b/ui/aura/window_event_dispatcher.cc
@@ -505,10 +505,7 @@
   observer_notifiers_.push(std::make_unique<ObserverNotifier>(this, *event));
 }
 
-void WindowEventDispatcher::OnEventProcessingFinished(
-    ui::Event* event,
-    ui::EventTarget* target,
-    const ui::EventDispatchDetails& details) {
+void WindowEventDispatcher::OnEventProcessingFinished(ui::Event* event) {
   if (in_shutdown_)
     return;
 
diff --git a/ui/aura/window_event_dispatcher.h b/ui/aura/window_event_dispatcher.h
index 511fdb1..3afd4ed 100644
--- a/ui/aura/window_event_dispatcher.h
+++ b/ui/aura/window_event_dispatcher.h
@@ -222,10 +222,7 @@
   // Overridden from ui::EventProcessor:
   ui::EventTarget* GetRootForEvent(ui::Event* event) override;
   void OnEventProcessingStarted(ui::Event* event) override;
-  void OnEventProcessingFinished(
-      ui::Event* event,
-      ui::EventTarget* target,
-      const ui::EventDispatchDetails& details) override;
+  void OnEventProcessingFinished(ui::Event* event) override;
 
   // Overridden from ui::EventDispatcherDelegate.
   bool CanDispatchToTarget(ui::EventTarget* target) override;
diff --git a/ui/chromeos/events/event_rewriter_chromeos.cc b/ui/chromeos/events/event_rewriter_chromeos.cc
index cbe8925..56b3a17 100644
--- a/ui/chromeos/events/event_rewriter_chromeos.cc
+++ b/ui/chromeos/events/event_rewriter_chromeos.cc
@@ -1860,7 +1860,8 @@
     //  No      System   No                 Fn -> System
     //  Yes     Fn       No                 Unchanged
     //  Yes     System   No                 Unchanged
-    if (ForceTopRowAsFunctionKeys() == flip_remapping) {
+    if (ForceTopRowAsFunctionKeys(key_event.source_device_id()) ==
+        flip_remapping) {
       // Rewrite the F1-F12 keys on a Chromebook keyboard to system keys.
       // This is the original Chrome OS layout.
       static const KeyboardRemapping kFkeysToSystemKeys1[] = {
@@ -2256,7 +2257,8 @@
   // If the scan code appears in the top row mapping it is an action key.
   const bool is_action_key = (key_iter != scan_code_map.end());
   if (is_action_key) {
-    if (flip_remapping != ForceTopRowAsFunctionKeys()) {
+    if (flip_remapping !=
+        ForceTopRowAsFunctionKeys(key_event.source_device_id())) {
       ApplyRemapping(key_iter->second, state);
     }
 
@@ -2405,7 +2407,8 @@
                                  std::size(kActionToFnKeys))) {
     // Incoming key code is an action key. Check if it needs to be mapped back
     // to its corresponding function key.
-    if (flip_remapping != ForceTopRowAsFunctionKeys()) {
+    if (flip_remapping !=
+        ForceTopRowAsFunctionKeys(key_event.source_device_id())) {
       // On Drallion, mirror mode toggle is on its own key so don't remap it.
       if (layout == KeyboardCapability::KeyboardTopRowLayout::
                         kKbdTopRowLayoutDrallion &&
@@ -2431,20 +2434,19 @@
       state->code = DomCode::F12;
       state->key = DomKey::F12;
     }
-    // At this point, the search modifier flag should be cleared if the
-    // remapping was supposed to be flipped.
+    // If the mapping should be flipped when command is down, the flag needs to
+    // be cleared.
     if (flip_remapping) {
       state->flags &= ~EF_COMMAND_DOWN;
     }
-
     return true;
   }
 
   return false;
 }
 
-bool EventRewriterChromeOS::ForceTopRowAsFunctionKeys() const {
-  return delegate_ && delegate_->TopRowKeysAreFunctionKeys();
+bool EventRewriterChromeOS::ForceTopRowAsFunctionKeys(int device_id) const {
+  return delegate_ && delegate_->TopRowKeysAreFunctionKeys(device_id);
 }
 
 KeyboardCapability::DeviceType EventRewriterChromeOS::KeyboardDeviceAdded(
diff --git a/ui/chromeos/events/event_rewriter_chromeos.h b/ui/chromeos/events/event_rewriter_chromeos.h
index e5d1810..82a9854 100644
--- a/ui/chromeos/events/event_rewriter_chromeos.h
+++ b/ui/chromeos/events/event_rewriter_chromeos.h
@@ -103,7 +103,7 @@
     // function keys instead of having them rewritten into back, forward,
     // brightness, volume, etc. or if the user has specified that they desire
     // top-row keys to be treated as function keys globally.
-    virtual bool TopRowKeysAreFunctionKeys() const = 0;
+    virtual bool TopRowKeysAreFunctionKeys(int device_id) const = 0;
 
     // Returns true if the |key_code| and |flags| have been resgistered for
     // extensions and EventRewriterChromeOS will not rewrite the event.
@@ -246,7 +246,7 @@
   // By default the top row (F1-F12) keys are system keys for back, forward,
   // brightness, volume, etc. However, windows for v2 apps can optionally
   // request raw function keys for these keys.
-  bool ForceTopRowAsFunctionKeys() const;
+  bool ForceTopRowAsFunctionKeys(int device_id) const;
 
   // Adds a device to |device_id_to_info_| only if no failure occurs in
   // identifying the keyboard, and returns the device type of this keyboard
diff --git a/ui/events/event_processor.cc b/ui/events/event_processor.cc
index 4937e23..2bf8068d 100644
--- a/ui/events/event_processor.cc
+++ b/ui/events/event_processor.cc
@@ -77,16 +77,13 @@
       target = targeter->FindNextBestTarget(target, event_to_dispatch);
     }
   }
-  OnEventProcessingFinished(event, target, details);
+  OnEventProcessingFinished(event);
   return details;
 }
 
 void EventProcessor::OnEventProcessingStarted(Event* event) {
 }
 
-void EventProcessor::OnEventProcessingFinished(
-    Event* event,
-    EventTarget* target,
-    const EventDispatchDetails& details) {}
+void EventProcessor::OnEventProcessingFinished(Event* event) {}
 
 }  // namespace ui
diff --git a/ui/events/event_processor.h b/ui/events/event_processor.h
index 10cdc5ad..b65b0ce9 100644
--- a/ui/events/event_processor.h
+++ b/ui/events/event_processor.h
@@ -50,9 +50,7 @@
   // dispatching of |event| will be performed by this EventProcessor). Note
   // that the last target to which |event| was dispatched may have been
   // destroyed.
-  virtual void OnEventProcessingFinished(Event* event,
-                                         EventTarget* target,
-                                         const EventDispatchDetails& details);
+  virtual void OnEventProcessingFinished(Event* event);
 
  private:
   base::WeakPtrFactory<EventProcessor> weak_ptr_factory_{this};
diff --git a/ui/events/test/test_event_processor.cc b/ui/events/test/test_event_processor.cc
index 1ab483d9..c36d3da 100644
--- a/ui/events/test/test_event_processor.cc
+++ b/ui/events/test/test_event_processor.cc
@@ -55,10 +55,7 @@
     event->SetHandled();
 }
 
-void TestEventProcessor::OnEventProcessingFinished(
-    Event* event,
-    EventTarget* target,
-    const EventDispatchDetails& details) {
+void TestEventProcessor::OnEventProcessingFinished(Event* event) {
   num_times_processing_finished_++;
 }
 
diff --git a/ui/events/test/test_event_processor.h b/ui/events/test/test_event_processor.h
index 86e42dc..4b36763 100644
--- a/ui/events/test/test_event_processor.h
+++ b/ui/events/test/test_event_processor.h
@@ -43,9 +43,7 @@
   EventTargeter* GetDefaultEventTargeter() override;
   EventDispatchDetails OnEventFromSource(Event* event) override;
   void OnEventProcessingStarted(Event* event) override;
-  void OnEventProcessingFinished(Event* event,
-                                 EventTarget* target,
-                                 const EventDispatchDetails& details) override;
+  void OnEventProcessingFinished(Event* event) override;
 
  private:
   std::unique_ptr<EventTarget> root_;
diff --git a/ui/file_manager/file_manager/background/js/mock_volume_manager.js b/ui/file_manager/file_manager/background/js/mock_volume_manager.js
index eb352f5..01c4a47 100644
--- a/ui/file_manager/file_manager/background/js/mock_volume_manager.js
+++ b/ui/file_manager/file_manager/background/js/mock_volume_manager.js
@@ -120,7 +120,7 @@
    * Current implementation can handle only fake entries.
    *
    * @param {!Entry|!FilesAppEntry} entry A fake entry.
-   * @return {!EntryLocation} Location information.
+   * @return {!EntryLocation|null} Location information.
    */
   getLocationInfo(entry) {
     if (util.isFakeEntry(entry)) {
@@ -160,6 +160,11 @@
     }
 
     const volumeInfo = this.getVolumeInfo(entry);
+    // For filtered out volumes, its volume info won't exist in the volume info
+    // list.
+    if (!volumeInfo) {
+      return null;
+    }
     const rootType = VolumeManagerCommon.getRootTypeFromVolumeType(
         assert(volumeInfo.volumeType));
     const isRootEntry = util.isSameEntry(entry, volumeInfo.fileSystem.root);
diff --git a/ui/file_manager/file_manager/common/js/files_app_entry_types.js b/ui/file_manager/file_manager/common/js/files_app_entry_types.js
index c3c1450..a14e7de 100644
--- a/ui/file_manager/file_manager/common/js/files_app_entry_types.js
+++ b/ui/file_manager/file_manager/common/js/files_app_entry_types.js
@@ -355,7 +355,9 @@
     }
     this.type_name = 'VolumeEntry';
 
-    // TODO(lucmult): consider deriving this from volumeInfo.
+    // TODO(b/271485133): consider deriving this from volumeInfo. Setting
+    // rootType here breaks some integration tests, e.g.
+    // saveAsDlpRestrictedAndroid.
     this.rootType = null;
 
     this.disabled_ = false;
@@ -756,6 +758,21 @@
   createReader() {
     return new StaticReader([]);
   }
+
+  /**
+   * FakeEntry can be a placeholder for the real volume, if so this field will
+   * be the volume type of the volume it represents.
+   * @return {VolumeManagerCommon.VolumeType|null}
+   */
+  get volumeType() {
+    // Recent rootType has no corresponding volume type, and it will throw error
+    // in the below getVolumeTypeFromRootType() call, we need to return null
+    // here.
+    if (this.rootType === VolumeManagerCommon.RootType.RECENT) {
+      return null;
+    }
+    return VolumeManagerCommon.getVolumeTypeFromRootType(this.rootType);
+  }
 }
 
 /**
@@ -802,4 +819,12 @@
   toURL() {
     return `fake-entry://guest-os/${this.guest_id}`;
   }
+
+  /** @override */
+  get volumeType() {
+    if (this.vm_type === chrome.fileManagerPrivate.VmType.ARCVM) {
+      return VolumeManagerCommon.VolumeType.ANDROID_FILES;
+    }
+    return VolumeManagerCommon.VolumeType.GUEST_OS;
+  }
 }
diff --git a/ui/file_manager/file_manager/externs/files_app_entry_interfaces.js b/ui/file_manager/file_manager/externs/files_app_entry_interfaces.js
index d570f86..00d9d02 100644
--- a/ui/file_manager/file_manager/externs/files_app_entry_interfaces.js
+++ b/ui/file_manager/file_manager/externs/files_app_entry_interfaces.js
@@ -207,4 +207,11 @@
    * @return {string}
    */
   get iconName() {}
+
+  /**
+   * FakeEntry can be a placeholder for the real volume, if so this field will
+   * be the volume type of the volume it represents.
+   * @return {VolumeManagerCommon.VolumeType|null}
+   */
+  get volumeType() {}
 }
diff --git a/ui/file_manager/file_manager/state/for_tests.ts b/ui/file_manager/file_manager/state/for_tests.ts
index bcca805..da10e47 100644
--- a/ui/file_manager/file_manager/state/for_tests.ts
+++ b/ui/file_manager/file_manager/state/for_tests.ts
@@ -6,13 +6,11 @@
 
 import {MockVolumeManager} from '../background/js/mock_volume_manager.js';
 import {DialogType} from '../common/js/dialog_type.js';
-import {VolumeManagerCommon} from '../common/js/volume_manager_types.js';
 import {Crostini} from '../externs/background/crostini.js';
 import {FilesAppDirEntry} from '../externs/files_app_entry_interfaces.js';
-import {FileData, FileKey, PropStatus, State, Volume} from '../externs/ts/state.js';
-import {constants} from '../foreground/js/constants.js';
+import {FileKey, PropStatus, State} from '../externs/ts/state.js';
+import {VolumeInfo} from '../externs/volume_info.js';
 import {FileSelectionHandler} from '../foreground/js/file_selection.js';
-import {MetadataItem} from '../foreground/js/metadata/metadata_item.js';
 import {MetadataModel} from '../foreground/js/metadata/metadata_model.js';
 import {MockMetadataModel} from '../foreground/js/metadata/mock_metadata.js';
 import {createFakeDirectoryModel} from '../foreground/js/mock_directory_model.js';
@@ -77,6 +75,21 @@
 }
 
 /**
+ * Store state might include objects (e.g. Entry type) which can not stringified
+ * by JSON, here we implement a custom "replacer" to handle that.
+ */
+function jsonStringifyStoreState(state: any): string {
+  return JSON.stringify(state, (key, value) => {
+    // Currently only the key with "entry" (inside `FileData`) can't be
+    // stringified, we just return its URL.
+    if (key === 'entry') {
+      return value.toURL();
+    }
+    return value;
+  }, 2);
+}
+
+/**
  * Waits for a part of the Store to be in the expected state.
  *
  * Waits a maximum of 10 seconds, since in the unittest the Store manipulation
@@ -92,7 +105,9 @@
   let got: any;
   const timeout = new Promise((_, reject) => {
     setTimeout(() => {
-      reject(new Error(`waitDeepEquals timed out waiting for \n${want}`));
+      reject(new Error(`waitDeepEquals timed out.\nWANT:\n${
+          jsonStringifyStoreState(
+              want)}\nGOT:\n${jsonStringifyStoreState(got)}`));
     }, 10000);
   });
 
@@ -105,8 +120,6 @@
       if (error.constructor?.name === 'AssertionError') {
         return false;
       }
-      console.log(error.stack);
-      console.error(error);
       throw error;
     }
   });
@@ -115,9 +128,9 @@
 }
 
 /** Setup store and initialize it with empty state. */
-export function setupStore(): Store {
+export function setupStore(initialState: State = getEmptyState()): Store {
   const store = getStore();
-  store.init(getEmptyState());
+  store.init(initialState);
   return store;
 }
 
@@ -139,98 +152,39 @@
 }
 
 /**
- * Create a fake FileData with partial information. Only the fields listed are
- * required, other fields are optional.
+ * Create a fake VolumeMetadata with VolumeInfo, VolumeInfo can be created by
+ * MockVolumeManager.createMockVolumeInfo.
  */
-export function createFakeFileData(
-    partialFileData: Pick<FileData, 'entry'|'label'|'type'>&
-    Partial<Omit<FileData, 'entry'|'label'|'type'>>,
-    ): FileData {
-  const defaultFileData = {
-    icon: constants.ICON_TYPES.FOLDER,
-    volumeType: null,
-    isDirectory: true,
-    metadata: {} as MetadataItem,
-    isRootEntry: false,
-    isEjectable: false,
-    shouldDelayLoadingChildren: false,
-    children: [],
-    expanded: false,
-  };
-  return {
-    ...defaultFileData,
-    ...partialFileData,
-  };
-}
-
-/** Create a fake VolumeMetadata. */
 export function createFakeVolumeMetadata(
-    partialMetadata:
-        Pick<chrome.fileManagerPrivate.VolumeMetadata, 'volumeId'|'volumeType'>&
-    Partial<Omit<
-        chrome.fileManagerPrivate.VolumeMetadata, 'volumeId'|'volumeType'>>,
+    volumeInfo: VolumeInfo,
     ): chrome.fileManagerPrivate.VolumeMetadata {
-  const defaultMetadata = {
+  return {
+    volumeId: volumeInfo.volumeId,
+    volumeType: volumeInfo.volumeType,
     profile: {
-      displayName: 'foobar@chromium.org',
-      isCurrentProfile: true,
+      ...volumeInfo.profile,
       profileId: '',
     },
-    configurable: false,
-    watchable: true,
-    source: VolumeManagerCommon.Source.SYSTEM,
-    volumeLabel: undefined,
+    configurable: volumeInfo.configurable,
+    watchable: volumeInfo.watchable,
+    source: volumeInfo.source,
+    volumeLabel: volumeInfo.label,
     fileSystemId: undefined,
-    providerId: undefined,
+    providerId: volumeInfo.providerId,
     sourcePath: undefined,
-    deviceType: undefined,
-    devicePath: undefined,
+    deviceType: volumeInfo.deviceType,
+    devicePath: volumeInfo.devicePath,
     isParentDevice: undefined,
-    isReadOnly: false,
-    isReadOnlyRemovableDevice: false,
-    hasMedia: false,
+    isReadOnly: volumeInfo.isReadOnly,
+    isReadOnlyRemovableDevice: volumeInfo.isReadOnlyRemovableDevice,
+    hasMedia: volumeInfo.hasMedia,
     mountCondition: undefined,
     mountContext: undefined,
-    diskFileSystemType: undefined,
-    iconSet: {icon16x16Url: '', icon32x32Url: ''},
-    driveLabel: '',
-    remoteMountPath: undefined,
+    diskFileSystemType: volumeInfo.diskFileSystemType,
+    iconSet: volumeInfo.iconSet,
+    driveLabel: volumeInfo.driveLabel,
+    remoteMountPath: volumeInfo.remoteMountPath,
     hidden: false,
-    vmType: undefined,
-  };
-  return {
-    ...defaultMetadata,
-    ...partialMetadata,
-  };
-}
-
-/**
- * Create a fake Volume. Only the fields listed are required, other fields are
- * optional.
- */
-export function createFakeVolume(
-    partialVolume: Pick<Volume, 'volumeId'|'volumeType'|'rootKey'|'label'>&
-    Partial<Omit<Volume, 'volumeId'|'volumeType'|'rootKey'|'label'>>): Volume {
-  const defaultVolume = {
-    status: PropStatus.SUCCESS,
-    source: VolumeManagerCommon.Source.SYSTEM,
-    error: undefined,
-    deviceType: undefined,
-    devicePath: undefined,
-    isReadOnly: false,
-    isReadOnlyRemovableDevice: false,
-    providerId: undefined,
-    configurable: false,
-    watchable: true,
-    diskFileSystemType: '',
-    iconSet: {icon16x16Url: '', icon32x32Url: ''},
-    driveLabel: '',
-    vmType: undefined,
-    isDisabled: false,
-    prefixKey: undefined,
-  };
-  return {
-    ...defaultVolume,
-    ...partialVolume,
+    vmType: volumeInfo.vmType,
   };
 }
diff --git a/ui/file_manager/file_manager/state/reducers/all_entries.ts b/ui/file_manager/file_manager/state/reducers/all_entries.ts
index ad578bdc..9602619 100644
--- a/ui/file_manager/file_manager/state/reducers/all_entries.ts
+++ b/ui/file_manager/file_manager/state/reducers/all_entries.ts
@@ -4,7 +4,7 @@
 
 import {isVolumeEntry, sortEntries} from '../../common/js/entry_utils.js';
 import {FileType} from '../../common/js/file_type.js';
-import {EntryList, FakeEntryImpl, GuestOsPlaceholder, VolumeEntry} from '../../common/js/files_app_entry_types.js';
+import {EntryList, VolumeEntry} from '../../common/js/files_app_entry_types.js';
 import {str, util} from '../../common/js/util.js';
 import {VolumeManagerCommon} from '../../common/js/volume_manager_types.js';
 import {EntryLocation} from '../../externs/entry_location.js';
@@ -217,34 +217,22 @@
  */
 export function convertEntryToFileData(entry: Entry|FilesAppEntry): FileData {
   // TODO: get VolumeManager/MetadataModel properly.
-  const volumeManager = window.fileManager?.volumeManager;
-  const metadataModel = window.fileManager?.metadataModel;
-  const volumeInfo = volumeManager?.getVolumeInfo(entry);
-  const locationInfo = volumeManager?.getLocationInfo(entry);
+  const {volumeManager, metadataModel} = window.fileManager;
+  const volumeInfo = volumeManager.getVolumeInfo(entry);
+  const locationInfo = volumeManager.getLocationInfo(entry);
   // getEntryLabel() can accept locationInfo=null, but TS doesn't recognize the
   // type definition in closure, hence the ! here.
   const label = util.getEntryLabel(locationInfo!, entry);
   const volumeType = volumeInfo?.volumeType || null;
   const icon = getEntryIcon(entry, locationInfo, volumeType);
 
-  // TODO(b/271485133): populate rootType for VolumeEntry in its constructor,
-  // add volumeType property to FakeEntry so we don't need the following nested
-  // if logic to check.
-  if (entry instanceof VolumeEntry) {
-    entry.disabled = volumeManager?.isDisabled(volumeType!);
-    entry.rootType = locationInfo?.rootType;
-  } else if (entry instanceof FakeEntryImpl) {
-    if (entry.rootType === VolumeManagerCommon.RootType.CROSTINI) {
-      entry.disabled = volumeManager?.isDisabled(entry.rootType);
-    } else if (entry instanceof GuestOsPlaceholder) {
-      if (entry.vm_type == chrome.fileManagerPrivate.VmType.ARCVM) {
-        entry.disabled = volumeManager.isDisabled(
-            VolumeManagerCommon.VolumeType.ANDROID_FILES);
-      } else {
-        entry.disabled =
-            volumeManager.isDisabled(VolumeManagerCommon.VolumeType.GUEST_OS);
-      }
-    }
+  /**
+   * Update disabled attribute if entry supports disabled attribute and has a
+   * non-null volumeType.
+   */
+  if ('disabled' in entry && 'volumeType' in entry && entry.volumeType) {
+    entry.disabled = volumeManager.isDisabled(
+        entry.volumeType as VolumeManagerCommon.VolumeType);
   }
 
   const metadata = metadataModel ?
@@ -361,6 +349,7 @@
         case VolumeManagerCommon.RootType.DRIVE_FAKE_ROOT:
           return EntryType.ENTRY_LIST;
         case VolumeManagerCommon.RootType.CROSTINI:
+        case VolumeManagerCommon.RootType.ANDROID_FILES:
           return EntryType.PLACEHOLDER;
         case VolumeManagerCommon.RootType.DRIVE_OFFLINE:
         case VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME:
@@ -437,6 +426,7 @@
     myFilesEntryList = new EntryList(
         str('MY_FILES_ROOT_LABEL'), VolumeManagerCommon.RootType.MY_FILES);
     appendEntry(state, myFilesEntryList);
+    state.uiEntries = [...state.uiEntries, myFilesEntryList.toURL()];
   }
 
   return {
@@ -485,6 +475,9 @@
         // Also remove it from the children field.
         myFilesFileData.children = myFilesFileData.children.filter(
             childKey => childKey !== childEntry.toURL());
+        // And remove it from the uiEntries if existed.
+        state.uiEntries = state.uiEntries.filter(
+            uiEntryKey => uiEntryKey !== childEntry.toURL());
       }
     }
     appendChildIfNotExisted(myFilesEntry, newVolumeEntry);
@@ -514,6 +507,9 @@
         appendChildIfNotExisted(myFilesVolumeEntry!, childEntry);
         myFilesEntryList.removeChildEntry(childEntry);
       }
+      // Remove MyFiles entry list from the uiEntries.
+      state.uiEntries = state.uiEntries.filter(
+          uiEntryKey => uiEntryKey !== myFilesEntryListKey);
     }
   }
 
@@ -528,6 +524,7 @@
           str('DRIVE_DIRECTORY_LABEL'),
           VolumeManagerCommon.RootType.DRIVE_FAKE_ROOT);
       appendEntry(state, googleDrive);
+      state.uiEntries = [...state.uiEntries, googleDrive.toURL()];
     }
     appendChildIfNotExisted(googleDrive, myDrive!);
 
@@ -560,7 +557,7 @@
         fakeEntries[VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME];
     if (fakeSharedWithMe) {
       appendEntry(state, fakeSharedWithMe);
-      state.uiEntries.push(fakeSharedWithMe.toURL());
+      state.uiEntries = [...state.uiEntries, fakeSharedWithMe.toURL()];
       appendChildIfNotExisted(googleDrive, fakeSharedWithMe);
     }
 
@@ -568,7 +565,7 @@
     const fakeOffline = fakeEntries[VolumeManagerCommon.RootType.DRIVE_OFFLINE];
     if (fakeOffline) {
       appendEntry(state, fakeOffline);
-      state.uiEntries.push(fakeOffline.toURL());
+      state.uiEntries = [...state.uiEntries, fakeOffline.toURL()];
       appendChildIfNotExisted(googleDrive, fakeOffline);
     }
   }
@@ -592,6 +589,7 @@
             volumeMetadata.driveLabel || '',
             VolumeManagerCommon.RootType.REMOVABLE, volumeMetadata.devicePath);
         appendEntry(state, parentEntry);
+        state.uiEntries = [...state.uiEntries, parentEntry.toURL()];
         // Removable devices with group, its parent should always be ejectable.
         state.allEntries[parentKey].isEjectable = true;
       }
diff --git a/ui/file_manager/file_manager/state/reducers/all_entries_unittest.ts b/ui/file_manager/file_manager/state/reducers/all_entries_unittest.ts
index 861834d..794ef553 100644
--- a/ui/file_manager/file_manager/state/reducers/all_entries_unittest.ts
+++ b/ui/file_manager/file_manager/state/reducers/all_entries_unittest.ts
@@ -2,26 +2,24 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {assertArrayEquals, assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';
+import {assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';
 
 import {MockVolumeManager} from '../../background/js/mock_volume_manager.js';
 import {EntryList, FakeEntryImpl, VolumeEntry} from '../../common/js/files_app_entry_types.js';
 import {MockFileSystem} from '../../common/js/mock_entry.js';
 import {waitUntil} from '../../common/js/test_error_reporting.js';
 import {VolumeManagerCommon} from '../../common/js/volume_manager_types.js';
-import {EntryType, FileData} from '../../externs/ts/state.js';
+import {EntryType, FileData, State} from '../../externs/ts/state.js';
 import {VolumeInfo} from '../../externs/volume_info.js';
-import {constants} from '../../foreground/js/constants.js';
 import {MetadataItem} from '../../foreground/js/metadata/metadata_item.js';
 import {MockMetadataModel} from '../../foreground/js/metadata/mock_metadata.js';
 import {ActionType} from '../actions.js';
-import {addChildEntries as addChildEntriesAction, ClearStaleCachedEntriesAction} from '../actions/all_entries.js';
-import {addVolume} from '../actions/volumes.js';
-import {allEntriesSize, assertAllEntriesEqual, cd, changeSelection, createFakeFileData, createFakeVolume, createFakeVolumeMetadata, setUpFileManagerOnWindow, setupStore, updMetadata} from '../for_tests.js';
+import {addChildEntries, ClearStaleCachedEntriesAction} from '../actions/all_entries.js';
+import {allEntriesSize, assertAllEntriesEqual, cd, changeSelection, createFakeVolumeMetadata, setUpFileManagerOnWindow, setupStore, updMetadata, waitDeepEquals} from '../for_tests.js';
 import {getEmptyState, Store} from '../store.js';
 
-import {addChildEntries, cacheEntries, clearCachedEntries, getMyFiles} from './all_entries.js';
-import {driveRootEntryListKey, makeRemovableParentKey, myFilesEntryListKey} from './volumes.js';
+import {clearCachedEntries, convertEntryToFileData, getMyFiles} from './all_entries.js';
+import {convertVolumeInfoAndMetadataToVolume, myFilesEntryListKey} from './volumes.js';
 
 let store: Store;
 let fileSystem: MockFileSystem;
@@ -47,25 +45,19 @@
 
 /** Generate MyFiles entry with fake entry list. */
 function createMyFilesDataWithEntryList(): FileData {
-  return createFakeFileData({
-    entry: new EntryList('My files', VolumeManagerCommon.RootType.MY_FILES),
-    label: 'My files',
-    type: EntryType.ENTRY_LIST,
-  });
+  const myFilesEntryList =
+      new EntryList('My files', VolumeManagerCommon.RootType.MY_FILES);
+  return convertEntryToFileData(myFilesEntryList);
 }
 
 /** Generate MyFiles entry with real volume entry. */
 function createMyFilesDataWithVolumeEntry():
     {fileData: FileData, volumeInfo: VolumeInfo} {
-  const {volumeManager} = window.fileManager;
+  const volumeManager = new MockVolumeManager();
   const downloadsVolumeInfo = volumeManager.getCurrentProfileVolumeInfo(
       VolumeManagerCommon.VolumeType.DOWNLOADS)!;
-  const fileData = createFakeFileData({
-    entry: new VolumeEntry(downloadsVolumeInfo),
-    volumeType: VolumeManagerCommon.VolumeType.DOWNLOADS,
-    label: 'My files',
-    type: EntryType.VOLUME_ROOT,
-  });
+  const myFilesVolumeEntry = new VolumeEntry(downloadsVolumeInfo);
+  const fileData = convertEntryToFileData(myFilesVolumeEntry);
   return {fileData, volumeInfo: downloadsVolumeInfo};
 }
 
@@ -248,12 +240,9 @@
   const currentState = getEmptyState();
   // Add MyFiles volume to the store.
   const {fileData, volumeInfo} = createMyFilesDataWithVolumeEntry();
-  const volume = createFakeVolume({
-    volumeType: volumeInfo.volumeType,
-    volumeId: volumeInfo.volumeId,
-    label: volumeInfo.label,
-    rootKey: volumeInfo.displayRoot.toURL(),
-  });
+  const volumeMetadata = createFakeVolumeMetadata(volumeInfo);
+  const volume =
+      convertVolumeInfoAndMetadataToVolume(volumeInfo, volumeMetadata);
   currentState.allEntries[fileData.entry.toURL()] = fileData;
   currentState.volumes[volumeInfo.volumeId] = volume;
   const {myFilesEntry, myFilesVolume} = getMyFiles(currentState);
@@ -280,288 +269,76 @@
   assertEquals(null, myFilesVolume);
 }
 
-/** Tests that MyFiles volume entry can be cached correctly. */
-export function testCacheEntriesForMyFilesVolume() {
-  const currentState = getEmptyState();
-  const myFilesFileData = createMyFilesDataWithEntryList();
-  const myFilesEntryList = myFilesFileData.entry as EntryList;
-  // Put MyFiles entry in the store and add ui entries as its children.
-  currentState.allEntries[myFilesEntryList.toURL()] = myFilesFileData;
-  const playFilesEntry = new FakeEntryImpl(
-      'Play files', VolumeManagerCommon.RootType.ANDROID_FILES);
-  myFilesEntryList.addEntry(playFilesEntry);
-  const linuxFilesEntry =
-      new FakeEntryImpl('Linux files', VolumeManagerCommon.RootType.CROSTINI);
-  myFilesEntryList.addEntry(linuxFilesEntry);
-
-  const {volumeManager} = window.fileManager;
-  const myFilesVolumeInfo = volumeManager.getCurrentProfileVolumeInfo(
-      VolumeManagerCommon.VolumeType.DOWNLOADS)!;
-  const myFilesVolumeMetadata = createFakeVolumeMetadata({
-    volumeId: myFilesVolumeInfo.volumeId,
-    volumeType: myFilesVolumeInfo.volumeType,
-  });
-  cacheEntries(currentState, addVolume({
-                 volumeInfo: myFilesVolumeInfo,
-                 volumeMetadata: myFilesVolumeMetadata,
-               }));
-
-  // cacheEntries() updates state in place.
-  const newState = currentState;
-  // Expect all existing ui children will be added to the real MyFiles entry.
-  const myFilesVolumeEntry: VolumeEntry =
-      newState.allEntries[myFilesVolumeInfo.displayRoot!.toURL()].entry;
-  const uiChildren = myFilesVolumeEntry.getUIChildren();
-  assertEquals(2, uiChildren.length);
-  assertEquals(playFilesEntry, uiChildren[0]);
-  assertEquals(linuxFilesEntry, uiChildren[1]);
-  assertEquals(0, myFilesEntryList.getUIChildren().length);
-}
-
-/** Tests that volume nested in MyFiles volume can be cached correctly. */
-export function testCacheEntriesForNestedVolumeInMyFilesVolume() {
-  const currentState = getEmptyState();
-  // Put MyFiles and play files ui entry in the store.
-  const {fileData, volumeInfo} = createMyFilesDataWithVolumeEntry();
-  const myFilesVolumeEntry = fileData.entry as VolumeEntry;
-  const myFilesVolume = createFakeVolume({
-    volumeType: volumeInfo.volumeType,
-    volumeId: volumeInfo.volumeId,
-    label: volumeInfo.label,
-    rootKey: volumeInfo.displayRoot.toURL(),
-  });
-  currentState.allEntries[fileData.entry.toURL()] = fileData;
-  currentState.volumes[volumeInfo.volumeId] = myFilesVolume;
-  // Placeholder ui entry and the volume entry it represents have the same
-  // label.
-  const label = 'Play files';
-  const playFilesUiEntry =
-      new FakeEntryImpl(label, VolumeManagerCommon.RootType.ANDROID_FILES);
-  myFilesVolumeEntry.addEntry(playFilesUiEntry);
-  fileData.children.push(playFilesUiEntry.toURL());
-
-  const {volumeManager} = window.fileManager;
-  const playFilesVolumeInfo = MockVolumeManager.createMockVolumeInfo(
-      VolumeManagerCommon.VolumeType.ANDROID_FILES, 'playFilesId', label);
-  volumeManager.volumeInfoList.add(playFilesVolumeInfo);
-  const playFilesVolumeMetadata = createFakeVolumeMetadata({
-    volumeType: playFilesVolumeInfo.volumeType,
-    volumeId: playFilesVolumeInfo.volumeId,
-  });
-  cacheEntries(currentState, addVolume({
-                 volumeInfo: playFilesVolumeInfo,
-                 volumeMetadata: playFilesVolumeMetadata,
-               }));
-  // cacheEntries() updates state in place.
-  const newState = currentState;
-  // Expect the new play file volume will be nested inside MyFiles and the old
-  // placeholder will be removed.
-  const playFilesVolumeEntry =
-      newState.allEntries[playFilesVolumeInfo.displayRoot!.toURL()].entry;
-  const newMyFilesFileData: FileData =
-      newState.allEntries[myFilesVolumeEntry.toURL()];
-  assertEquals(1, myFilesVolumeEntry.getUIChildren().length);
-  assertEquals(playFilesVolumeEntry, myFilesVolumeEntry.getUIChildren()[0]);
-  assertEquals(1, newMyFilesFileData.children.length);
-  assertEquals(playFilesVolumeEntry.toURL(), newMyFilesFileData.children[0]);
-}
-
-/** Tests that drive volume can be cached correctly. */
-export function testAddDriveVolume(done: () => void) {
-  const currentState = getEmptyState();
-
-  const {volumeManager} = window.fileManager;
-  const driveVolumeInfo = volumeManager.getCurrentProfileVolumeInfo(
-      VolumeManagerCommon.VolumeType.DRIVE)!;
-  const driveVolumeMetadata = createFakeVolumeMetadata({
-    volumeType: driveVolumeInfo.volumeType,
-    volumeId: driveVolumeInfo.volumeId,
-  });
-  // DriveFS takes time to resolve.
-  driveVolumeInfo.resolveDisplayRoot(() => {
-    cacheEntries(currentState, addVolume({
-                   volumeInfo: driveVolumeInfo,
-                   volumeMetadata: driveVolumeMetadata,
-                 }));
-    // cacheEntries() updates state in place.
-    const newState = currentState;
-    // Expect all fake entries inside Drive will be added as its children.
-    const driveFakeRootEntry: EntryList =
-        newState.allEntries[driveRootEntryListKey].entry;
-    assertEquals(
-        VolumeManagerCommon.RootType.DRIVE_FAKE_ROOT,
-        driveFakeRootEntry.rootType);
-    const driveChildren = driveFakeRootEntry.getUIChildren();
-    assertEquals(5, driveChildren.length);
-    // My Drive.
-    const myDriveEntry: VolumeEntry =
-        newState.allEntries[driveChildren[0]!.toURL()].entry;
-    assertEquals(VolumeManagerCommon.RootType.DRIVE, myDriveEntry.rootType);
-    assertEquals(myDriveEntry, driveChildren[0]);
-    // Shared drives root.
-    const sharedDrivesRootEntry: DirectoryEntry =
-        newState.allEntries[driveChildren[1]!.toURL()].entry;
-    assertEquals('/team_drives', sharedDrivesRootEntry.fullPath);
-    assertEquals(sharedDrivesRootEntry, driveChildren[1]);
-    // Computers root.
-    const computersRootEntry: DirectoryEntry =
-        newState.allEntries[driveChildren[2]!.toURL()].entry;
-    assertEquals('/Computers', computersRootEntry.fullPath);
-    assertEquals(computersRootEntry, driveChildren[2]);
-    // Shared with me.
-    const sharedWithMeEntry: FakeEntryImpl =
-        newState.allEntries[driveChildren[3]!.toURL()].entry;
-    assertEquals(
-        VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME,
-        sharedWithMeEntry.rootType);
-    assertEquals(sharedWithMeEntry, driveChildren[3]);
-    // Offline.
-    const offlineEntry: FakeEntryImpl =
-        newState.allEntries[driveChildren[4]!.toURL()].entry;
-    assertEquals(offlineEntry, driveChildren[4]);
-    assertEquals(
-        VolumeManagerCommon.RootType.DRIVE_OFFLINE, offlineEntry.rootType);
-    assertArrayEquals(
-        [sharedWithMeEntry.toURL(), offlineEntry.toURL()], newState.uiEntries);
-
-    done();
-  });
-}
-
-/** Tests that multiple partition volumes can be cached correctly. */
-export function testCacheEntriesForMultipleUsbPartitionsGrouping() {
-  const currentState = getEmptyState();
-  // Add partition-1 into the store.
-  const {volumeManager} = window.fileManager;
-  const partition1VolumeInfo = MockVolumeManager.createMockVolumeInfo(
-      VolumeManagerCommon.VolumeType.REMOVABLE, 'removable:partition1',
-      'Partition 1', '/device/path/1');
-  volumeManager.volumeInfoList.add(partition1VolumeInfo);
-  const partition1VolumeEntry = new VolumeEntry(partition1VolumeInfo);
-  const partition1FileData = createFakeFileData({
-    entry: partition1VolumeEntry,
-    label: partition1VolumeInfo.label,
-    type: EntryType.VOLUME_ROOT,
-  });
-  const partition1Volume = createFakeVolume({
-    volumeId: partition1VolumeInfo.volumeId,
-    volumeType: VolumeManagerCommon.VolumeType.REMOVABLE,
-    rootKey: partition1VolumeInfo.displayRoot!.toURL(),
-    label: partition1VolumeInfo.label,
-    devicePath: partition1VolumeInfo.devicePath,
-    driveLabel: 'USB_Drive',
-  });
-  currentState.volumes[partition1Volume.volumeId] = partition1Volume;
-  currentState.allEntries[partition1VolumeEntry.toURL()] = partition1FileData;
-
-  const partition2VolumeInfo = MockVolumeManager.createMockVolumeInfo(
-      VolumeManagerCommon.VolumeType.REMOVABLE, 'removable:partition2',
-      'Partition 2', partition1Volume.devicePath);
-  volumeManager.volumeInfoList.add(partition2VolumeInfo);
-  const partition2VolumeMetadata = createFakeVolumeMetadata({
-    volumeType: partition2VolumeInfo.volumeType,
-    volumeId: partition2VolumeInfo.volumeId,
-    devicePath: partition1Volume.devicePath,
-    driveLabel: partition1Volume.driveLabel,
-  });
-  cacheEntries(currentState, addVolume({
-                 volumeInfo: partition2VolumeInfo,
-                 volumeMetadata: partition2VolumeMetadata,
-               }));
-  // cacheEntries() updates state in place.
-  const newState = currentState;
-  // Expect a fake parent entry list will be created.
-  const parentEntryFileData: FileData =
-      newState.allEntries[makeRemovableParentKey(partition1Volume)];
-  const parentEntry = parentEntryFileData.entry as EntryList;
-  assertEquals('USB_Drive', parentEntry.label);
-  assertEquals(VolumeManagerCommon.RootType.REMOVABLE, parentEntry.rootType);
-  assertTrue(parentEntryFileData.isEjectable);
-  // Expect both partition1 and partition2 will be added as children.
-  const partition2VolumeEntry: VolumeEntry =
-      newState.allEntries[partition2VolumeInfo.displayRoot!.toURL()].entry;
-  assertEquals(2, parentEntry.getUIChildren().length);
-  assertEquals(partition1VolumeEntry, parentEntry.getUIChildren()[0]);
-  assertEquals(partition2VolumeEntry, parentEntry.getUIChildren()[1]);
-  assertEquals(
-      constants.ICON_TYPES.UNKNOWN_REMOVABLE,
-      newState.allEntries[partition1VolumeEntry.toURL()].icon);
-  assertEquals(
-      constants.ICON_TYPES.UNKNOWN_REMOVABLE,
-      newState.allEntries[partition2VolumeEntry.toURL()].icon);
-}
-
 /** Tests that child entries can be added to the store correctly. */
-export function testAddChildEntries() {
-  const currentState = getEmptyState();
+export async function testAddChildEntries(done: () => void) {
+  const initialState = getEmptyState();
 
   // Add parent/children entries to the store.
-  const fakeFs = new MockFileSystem('fake-fs');
-  fakeFs.populate([
-    '/aaa/',
-    '/aaa/1/',
-    '/aaa/2/',
-    '/aaa/2/123/',
+  fileSystem.populate([
+    '/a/',
+    '/a/1/',
+    '/a/2/',
+    '/a/2/b/',
   ]);
-  currentState.allEntries[fakeFs.entries['/aaa'].toURL()] = createFakeFileData({
-    entry: fakeFs.entries['/aaa'],
-    label: 'AAA',
-    type: EntryType.FS_API,
-  });
-  currentState.allEntries[fakeFs.entries['/aaa/1'].toURL()] =
-      createFakeFileData({
-        entry: fakeFs.entries['/aaa/1'],
-        label: 'AAA 1',
-        type: EntryType.FS_API,
-      });
-  currentState.allEntries[fakeFs.entries['/aaa/2'].toURL()] =
-      createFakeFileData({
-        entry: fakeFs.entries['/aaa/2'],
-        label: 'AAA 2',
-        type: EntryType.FS_API,
-        shouldDelayLoadingChildren: true,
-      });
-  currentState.allEntries[fakeFs.entries['/aaa/2/123'].toURL()] =
-      createFakeFileData({
-        entry: fakeFs.entries['/aaa/2/123'],
-        label: 'AAA 123',
-        type: EntryType.FS_API,
-      });
+  const aEntry = fileSystem.entries['/a'];
+  initialState.allEntries[aEntry.toURL()] = convertEntryToFileData(aEntry);
+  // Make sure aEntry won't be cleared.
+  initialState.uiEntries.push(aEntry.toURL());
 
-  // Add child entries for /aaa/.
-  const newState1 = addChildEntries(
-      currentState, addChildEntriesAction({
-        parentKey: fakeFs.entries['/aaa'].toURL(),
-        entries: [fakeFs.entries['/aaa/1'], fakeFs.entries['/aaa/2']],
-      }));
-  // Expect the children filed is updated.
-  const newChildren1 =
-      newState1.allEntries[fakeFs.entries['/aaa'].toURL()].children;
-  assertEquals(2, newChildren1.length);
-  assertEquals(fakeFs.entries['/aaa/1'].toURL(), newChildren1[0]);
-  assertEquals(fakeFs.entries['/aaa/2'].toURL(), newChildren1[1]);
+  const store = setupStore(initialState);
 
-  // Add child entries for /aaa/2 who has shouldDelayLoadingChildren.
-  assertFalse(currentState.allEntries[fakeFs.entries['/aaa/2/123'].toURL()]
-                  .shouldDelayLoadingChildren);
-  const newState2 =
-      addChildEntries(currentState, addChildEntriesAction({
-                        parentKey: fakeFs.entries['/aaa/2'].toURL(),
-                        entries: [fakeFs.entries['/aaa/2/123']],
-                      }));
-  // Expect child entry also has shouldDelayLoadingChildren=true.
-  const newChildren2 =
-      newState2.allEntries[fakeFs.entries['/aaa/2'].toURL()].children;
-  assertEquals(1, newChildren2.length);
-  assertEquals(fakeFs.entries['/aaa/2/123'].toURL(), newChildren2[0]);
-  assertTrue(newState2.allEntries[fakeFs.entries['/aaa/2/123'].toURL()]
-                 .shouldDelayLoadingChildren);
+  // Dispatch an action to add child entries for /aaa/.
+  const a1Entry = fileSystem.entries['/a/1'];
+  const a2Entry = fileSystem.entries['/a/2'];
+  store.dispatch(addChildEntries({
+    parentKey: aEntry.toURL(),
+    entries: [a1Entry, a2Entry],
+  }));
 
-  // Add child entries for non-existed parent.
-  const newState3 = addChildEntries(currentState, addChildEntriesAction({
-                                      parentKey: 'non-exist-key',
-                                      entries: [fakeFs.entries['/aaa/1']],
-                                    }));
-  // Expect nothing happens.
-  assertEquals(currentState, newState3);
+  // Expect the children filed of /a is updated.
+  const want1: State['allEntries'] = {
+    [aEntry.toURL()]: {
+      ...convertEntryToFileData(aEntry),
+      children: [a1Entry.toURL(), a2Entry.toURL()],
+    },
+    [a1Entry.toURL()]: convertEntryToFileData(a1Entry),
+    [a2Entry.toURL()]: convertEntryToFileData(a2Entry),
+  };
+  await waitDeepEquals(store, want1, (state) => state.allEntries);
+
+  // Set shouldDelayLoadingChildren=true for /a/2.
+  store.getState().allEntries[a2Entry.toURL()].shouldDelayLoadingChildren =
+      true;
+  // Dispatch an action to add child entries for /a/2.
+  const bEntry = fileSystem.entries['/a/2/b'];
+  store.dispatch(addChildEntries({
+    parentKey: a2Entry.toURL(),
+    entries: [bEntry],
+  }));
+
+  // Expect child entry /a/2/b also has shouldDelayLoadingChildren=true.
+  const want2: State['allEntries'] = {
+    ...want1,
+    [a2Entry.toURL()]: {
+      ...convertEntryToFileData(a2Entry),
+      shouldDelayLoadingChildren: true,
+      children: [bEntry.toURL()],
+    },
+    [bEntry.toURL()]: {
+      ...convertEntryToFileData(bEntry),
+      shouldDelayLoadingChildren: true,
+    },
+  };
+  await waitDeepEquals(store, want2, (state) => state.allEntries);
+
+  // Dispatch an action to add child entries for non-existed parent entry.
+  store.dispatch(addChildEntries({
+    parentKey: 'non-exist-key',
+    entries: [a1Entry],
+  }));
+
+  // Expect nothing changes in the store.
+  await waitDeepEquals(store, want2, (state) => state.allEntries);
+
+  done();
 }
diff --git a/ui/file_manager/file_manager/state/reducers/android_apps_unittest.ts b/ui/file_manager/file_manager/state/reducers/android_apps_unittest.ts
index 8b44457..923fd4c 100644
--- a/ui/file_manager/file_manager/state/reducers/android_apps_unittest.ts
+++ b/ui/file_manager/file_manager/state/reducers/android_apps_unittest.ts
@@ -2,16 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {assertEquals} from 'chrome://webui-test/chai_assert.js';
-
-import {addAndroidApps as addAndroidAppsAction} from '../actions/android_apps.js';
-import {getEmptyState} from '../store.js';
-
-import {addAndroidApps} from './android_apps.js';
+import {State} from '../../externs/ts/state.js';
+import {addAndroidApps} from '../actions/android_apps.js';
+import {setupStore, waitDeepEquals} from '../for_tests.js';
 
 /** Tests that android apps can be added correctly to the store. */
-export function testAddAndroidApps() {
-  const currentState = getEmptyState();
+export async function testAddAndroidApps(done: () => void) {
   const androidApps: chrome.fileManagerPrivate.AndroidApp[] = [
     {
       name: 'App 1',
@@ -26,15 +22,17 @@
       iconSet: {icon16x16Url: 'url3', icon32x32Url: 'url4'},
     },
   ];
-  const newState =
-      addAndroidApps(currentState, addAndroidAppsAction({apps: androidApps}));
-  const keys = Object.keys(newState.androidApps);
-  assertEquals(2, keys.length);
-  assertEquals('com.test.app1', keys[0]);
-  assertEquals('com.test.app2', keys[1]);
-  assertEquals('App 1', newState.androidApps[keys[0]!].name);
-  assertEquals('App 2', newState.androidApps[keys[1]!].name);
-  assertEquals('Activity1', newState.androidApps[keys[0]!].activityName);
-  assertEquals('url1', newState.androidApps[keys[0]!].iconSet.icon16x16Url);
-  assertEquals('url4', newState.androidApps[keys[1]!].iconSet.icon32x32Url);
+
+  // Dispatch an action to add android apps.
+  const store = setupStore();
+  store.dispatch(addAndroidApps({apps: androidApps}));
+
+  // Expect both android apps are existed in the store.
+  const want: State['androidApps'] = {
+    'com.test.app1': androidApps[0],
+    'com.test.app2': androidApps[1],
+  };
+  await waitDeepEquals(store, want, (state) => state.androidApps);
+
+  done();
 }
diff --git a/ui/file_manager/file_manager/state/reducers/current_directory.ts b/ui/file_manager/file_manager/state/reducers/current_directory.ts
index 6b8a3b78..2e7d9cd8 100644
--- a/ui/file_manager/file_manager/state/reducers/current_directory.ts
+++ b/ui/file_manager/file_manager/state/reducers/current_directory.ts
@@ -67,7 +67,7 @@
   // At the end of the change directory, DirectoryContents will send an Action
   // with the Entry to be cached.
   if (fileData) {
-    const volumeManager = window.fileManager?.volumeManager;
+    const {volumeManager} = window.fileManager;
     if (!volumeManager) {
       console.debug(`VolumeManager not available yet.`);
       currentDirectory = currentState.currentDirectory || currentDirectory;
diff --git a/ui/file_manager/file_manager/state/reducers/folder_shortcuts_unittest.ts b/ui/file_manager/file_manager/state/reducers/folder_shortcuts_unittest.ts
index cc2c652..ef376df 100644
--- a/ui/file_manager/file_manager/state/reducers/folder_shortcuts_unittest.ts
+++ b/ui/file_manager/file_manager/state/reducers/folder_shortcuts_unittest.ts
@@ -2,16 +2,17 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {assertEquals} from 'chrome://webui-test/chai_assert.js';
-
 import {MockFileSystem} from '../../common/js/mock_entry.js';
-import {VolumeManagerCommon} from '../../common/js/volume_manager_types.js';
-import {EntryType} from '../../externs/ts/state.js';
-import {addFolderShortcut as addFolderShortcutAction, refreshFolderShortcut as refreshFolderShortcutAction, removeFolderShortcut as removeFolderShortcutAction} from '../actions/folder_shortcuts.js';
-import {createFakeFileData} from '../for_tests.js';
+import {State} from '../../externs/ts/state.js';
+import {addFolderShortcut, refreshFolderShortcut, removeFolderShortcut} from '../actions/folder_shortcuts.js';
+import {setUpFileManagerOnWindow, setupStore, waitDeepEquals} from '../for_tests.js';
 import {getEmptyState} from '../store.js';
 
-import {addFolderShortcut, refreshFolderShortcut, removeFolderShortcut} from './folder_shortcuts.js';
+import {convertEntryToFileData} from './all_entries.js';
+
+export function setUp() {
+  setUpFileManagerOnWindow();
+}
 
 /** Generate a fake file system with fake file entries. */
 function setupFileSystem(): MockFileSystem {
@@ -25,92 +26,137 @@
   return fileSystem;
 }
 
-
 /** Tests folder shortcuts can be refreshed correctly. */
-export function testRefreshFolderShortcuts() {
-  const currentState = getEmptyState();
+export async function testRefreshFolderShortcuts(done: () => void) {
+  const initialState = getEmptyState();
   // Add shortcut-1 to the store.
   const fileSystem = setupFileSystem();
   const shortcutEntry1: DirectoryEntry = fileSystem.entries['/shortcut-1'];
+  initialState.allEntries[shortcutEntry1.toURL()] =
+      convertEntryToFileData(shortcutEntry1);
+  initialState.folderShortcuts.push(shortcutEntry1.toURL());
+
+  const store = setupStore(initialState);
+
+  // Dispatch a refresh action with shortcut 2 and 3.
   const shortcutEntry2: DirectoryEntry = fileSystem.entries['/shortcut-2'];
   const shortcutEntry3: DirectoryEntry = fileSystem.entries['/shortcut-3'];
-  currentState.allEntries[shortcutEntry1.toURL()] = createFakeFileData({
-    entry: shortcutEntry1,
-    volumeType: VolumeManagerCommon.VolumeType.DRIVE,
-    label: 'shortcut 1',
-    type: EntryType.FS_API,
-  });
-  currentState.folderShortcuts.push(shortcutEntry1.toURL());
+  store.dispatch(
+      refreshFolderShortcut({entries: [shortcutEntry2, shortcutEntry3]}));
 
-  const newState = refreshFolderShortcut(
-      currentState,
-      refreshFolderShortcutAction({entries: [shortcutEntry2, shortcutEntry3]}));
-  assertEquals(2, newState.folderShortcuts.length);
-  assertEquals(shortcutEntry2.toURL(), newState.folderShortcuts[0]);
-  assertEquals(shortcutEntry3.toURL(), newState.folderShortcuts[1]);
+  // Expect all shortcut entries are in allEntries, and only shortcut 2 and 3
+  // are in the folderShortcuts.
+  const want: Partial<State> = {
+    allEntries: {
+      [shortcutEntry2.toURL()]: convertEntryToFileData(shortcutEntry2),
+      [shortcutEntry3.toURL()]: convertEntryToFileData(shortcutEntry3),
+    },
+    folderShortcuts: [shortcutEntry2.toURL(), shortcutEntry3.toURL()],
+  };
+  await waitDeepEquals(store, want, (state) => ({
+                                      allEntries: state.allEntries,
+                                      folderShortcuts: state.folderShortcuts,
+                                    }));
+
+  done();
 }
 
 /** Tests folder shortcut can be added correctly. */
-export function testAddFolderShortcut() {
-  const currentState = getEmptyState();
+export async function testAddFolderShortcut(done: () => void) {
+  const initialState = getEmptyState();
   // Add shortcut-1 and shortcut-3 to the store.
   const fileSystem = setupFileSystem();
   const shortcutEntry1: DirectoryEntry = fileSystem.entries['/shortcut-1'];
-  const shortcutEntry2: DirectoryEntry = fileSystem.entries['/shortcut-2'];
   const shortcutEntry3: DirectoryEntry = fileSystem.entries['/shortcut-3'];
-  const shortcutEntry4: DirectoryEntry = fileSystem.entries['/shortcut-4'];
-  currentState.allEntries[shortcutEntry1.toURL()] = createFakeFileData({
-    entry: shortcutEntry1,
-    volumeType: VolumeManagerCommon.VolumeType.DRIVE,
-    label: 'shortcut 1',
-    type: EntryType.FS_API,
-  });
-  currentState.allEntries[shortcutEntry3.toURL()] = createFakeFileData({
-    entry: shortcutEntry3,
-    volumeType: VolumeManagerCommon.VolumeType.DRIVE,
-    label: 'shortcut 3',
-    type: EntryType.FS_API,
-  });
-  currentState.folderShortcuts.push(shortcutEntry1.toURL());
-  currentState.folderShortcuts.push(shortcutEntry3.toURL());
+  initialState.allEntries[shortcutEntry1.toURL()] =
+      convertEntryToFileData(shortcutEntry1);
+  initialState.allEntries[shortcutEntry3.toURL()] =
+      convertEntryToFileData(shortcutEntry3);
+  initialState.folderShortcuts.push(shortcutEntry1.toURL());
+  initialState.folderShortcuts.push(shortcutEntry3.toURL());
 
-  // Add a new shortcut.
-  const newState1 = addFolderShortcut(
-      currentState, addFolderShortcutAction({entry: shortcutEntry2}));
-  assertEquals(3, newState1.folderShortcuts.length);
-  assertEquals(shortcutEntry2.toURL(), newState1.folderShortcuts[1]);
-  // Add an already existed shortcut.
-  const newState2 = addFolderShortcut(
-      currentState, addFolderShortcutAction({entry: shortcutEntry1}));
-  assertEquals(newState2.folderShortcuts, currentState.folderShortcuts);
-  // Add another entry to check sorting.
-  const newState3 = addFolderShortcut(
-      currentState, addFolderShortcutAction({entry: shortcutEntry4}));
-  assertEquals(3, newState3.folderShortcuts.length);
-  assertEquals(shortcutEntry4.toURL(), newState3.folderShortcuts[2]);
+  const store = setupStore(initialState);
+
+  // Dispatch an action to add shortcut 4.
+  const shortcutEntry4: DirectoryEntry = fileSystem.entries['/shortcut-4'];
+  store.dispatch(addFolderShortcut({entry: shortcutEntry4}));
+
+  // Expect the newly added shortcut 4 is in the store.
+  const want1: Partial<State> = {
+    allEntries: {
+      [shortcutEntry1.toURL()]: convertEntryToFileData(shortcutEntry1),
+      [shortcutEntry3.toURL()]: convertEntryToFileData(shortcutEntry3),
+      [shortcutEntry4.toURL()]: convertEntryToFileData(shortcutEntry4),
+    },
+    folderShortcuts: [
+      shortcutEntry1.toURL(),
+      shortcutEntry3.toURL(),
+      shortcutEntry4.toURL(),
+    ],
+  };
+  await waitDeepEquals(store, want1, (state) => ({
+                                       allEntries: state.allEntries,
+                                       folderShortcuts: state.folderShortcuts,
+                                     }));
+
+  // Dispatch another action to add already existed shortcut 1.
+  store.dispatch(addFolderShortcut({entry: shortcutEntry1}));
+
+  // Expect no changes in the store.
+  await waitDeepEquals(store, want1, (state) => ({
+                                       allEntries: state.allEntries,
+                                       folderShortcuts: state.folderShortcuts,
+                                     }));
+
+  // Dispatch another action to add shortcut 2 to check sorting.
+  const shortcutEntry2: DirectoryEntry = fileSystem.entries['/shortcut-2'];
+  store.dispatch(addFolderShortcut({entry: shortcutEntry2}));
+
+  // Expect shortcut 2 will be inserted in the middle.
+  const want2: Partial<State> = {
+    allEntries: {
+      ...want1.allEntries,
+      [shortcutEntry2.toURL()]: convertEntryToFileData(shortcutEntry2),
+    },
+    folderShortcuts: [
+      shortcutEntry1.toURL(),
+      shortcutEntry2.toURL(),
+      shortcutEntry3.toURL(),
+      shortcutEntry4.toURL(),
+    ],
+  };
+  await waitDeepEquals(store, want2, (state) => ({
+                                       allEntries: state.allEntries,
+                                       folderShortcuts: state.folderShortcuts,
+                                     }));
+
+  done();
 }
 
 /** Tests folder shortcut can be removed correctly. */
-export function testRemoveFolderShortcut() {
-  const currentState = getEmptyState();
+export async function testRemoveFolderShortcut(done: () => void) {
+  const initialState = getEmptyState();
   // Add shortcut-1 to the store.
   const fileSystem = setupFileSystem();
   const shortcutEntry1: DirectoryEntry = fileSystem.entries['/shortcut-1'];
-  const shortcutEntry2: DirectoryEntry = fileSystem.entries['/shortcut-2'];
-  currentState.allEntries[shortcutEntry1.toURL()] = createFakeFileData({
-    entry: shortcutEntry1,
-    volumeType: VolumeManagerCommon.VolumeType.DRIVE,
-    label: 'shortcut 1',
-    type: EntryType.FS_API,
-  });
-  currentState.folderShortcuts.push(shortcutEntry1.toURL());
+  initialState.allEntries[shortcutEntry1.toURL()] =
+      convertEntryToFileData(shortcutEntry1);
+  initialState.folderShortcuts.push(shortcutEntry1.toURL());
 
-  // Remove a shortcut.
-  const newState1 = removeFolderShortcut(
-      currentState, removeFolderShortcutAction({key: shortcutEntry1.toURL()}));
-  assertEquals(0, newState1.folderShortcuts.length);
-  // Remove a non-exist shortcut.
-  const newState2 = removeFolderShortcut(
-      currentState, removeFolderShortcutAction({key: shortcutEntry2.toURL()}));
-  assertEquals(newState2.folderShortcuts, currentState.folderShortcuts);
+  const store = setupStore(initialState);
+
+  // Dispatch an action to remove shortcut 1.
+  store.dispatch(removeFolderShortcut({key: shortcutEntry1.toURL()}));
+
+  // Expect shortcut 1 is removed from the store.
+  await waitDeepEquals(store, [], (state) => state.folderShortcuts);
+
+  // Dispatch another action to remove non-existed shortcut 2.
+  const shortcutEntry2: DirectoryEntry = fileSystem.entries['/shortcut-2'];
+  store.dispatch(removeFolderShortcut({key: shortcutEntry2.toURL()}));
+
+  // Expect no changes in the store.
+  await waitDeepEquals(store, [], (state) => state.folderShortcuts);
+
+  done();
 }
diff --git a/ui/file_manager/file_manager/state/reducers/navigation_unittest.ts b/ui/file_manager/file_manager/state/reducers/navigation_unittest.ts
index 6dbb326..21e32d23 100644
--- a/ui/file_manager/file_manager/state/reducers/navigation_unittest.ts
+++ b/ui/file_manager/file_manager/state/reducers/navigation_unittest.ts
@@ -2,20 +2,18 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
-
 import {MockVolumeManager} from '../../background/js/mock_volume_manager.js';
 import {EntryList, FakeEntryImpl, VolumeEntry} from '../../common/js/files_app_entry_types.js';
 import {MockFileEntry, MockFileSystem} from '../../common/js/mock_entry.js';
 import {TrashRootEntry} from '../../common/js/trash.js';
 import {VolumeManagerCommon} from '../../common/js/volume_manager_types.js';
-import {EntryType, FileData, NavigationSection, NavigationType, Volume} from '../../externs/ts/state.js';
-import {refreshNavigationRoots as refreshNavigationRootsAction, updateNavigationEntry as updateNavigationEntryAction} from '../actions/navigation.js';
-import {createFakeFileData, createFakeVolume, setUpFileManagerOnWindow} from '../for_tests.js';
+import {FileData, NavigationSection, NavigationType, State, Volume} from '../../externs/ts/state.js';
+import {refreshNavigationRoots, updateNavigationEntry} from '../actions/navigation.js';
+import {createFakeVolumeMetadata, setUpFileManagerOnWindow, setupStore, waitDeepEquals} from '../for_tests.js';
 import {getEmptyState} from '../store.js';
 
-import {refreshNavigationRoots, updateNavigationEntry} from './navigation.js';
-import {driveRootEntryListKey, myFilesEntryListKey, recentRootKey, trashRootKey} from './volumes.js';
+import {convertEntryToFileData} from './all_entries.js';
+import {convertVolumeInfoAndMetadataToVolume, driveRootEntryListKey, myFilesEntryListKey, recentRootKey, trashRootKey} from './volumes.js';
 
 export function setUp() {
   setUpFileManagerOnWindow();
@@ -27,11 +25,7 @@
       'Recent', VolumeManagerCommon.RootType.RECENT,
       chrome.fileManagerPrivate.SourceRestriction.ANY_SOURCE,
       chrome.fileManagerPrivate.FileCategory.ALL);
-  return createFakeFileData({
-    entry: recentEntry,
-    label: 'Recent',
-    type: EntryType.RECENT,
-  });
+  return convertEntryToFileData(recentEntry);
 }
 
 /** Create FileData for shortcut entry. */
@@ -39,11 +33,10 @@
     fileSystemName: string, entryName: string, label: string): FileData {
   const fakeFs = new MockFileSystem(fileSystemName);
   const shortcutEntry = MockFileEntry.create(fakeFs, `/root/${entryName}`);
-  return createFakeFileData({
-    entry: shortcutEntry,
+  return {
+    ...convertEntryToFileData(shortcutEntry),
     label,
-    type: EntryType.FS_API,
-  });
+  };
 }
 
 /** Create FileData for MyFiles entry. */
@@ -52,40 +45,23 @@
   const downloadsVolumeInfo = volumeManager.getCurrentProfileVolumeInfo(
       VolumeManagerCommon.VolumeType.DOWNLOADS)!;
   const myFilesEntry = new VolumeEntry(downloadsVolumeInfo);
-  const fileData = createFakeFileData({
-    entry: myFilesEntry,
-    volumeType: VolumeManagerCommon.VolumeType.DOWNLOADS,
-    label: 'My files',
-    type: EntryType.VOLUME_ROOT,
-  });
-  const volume = createFakeVolume({
-    volumeId: downloadsVolumeInfo.volumeId,
-    volumeType: fileData.volumeType!,
-    label: fileData.label,
-    rootKey: fileData.entry.toURL(),
-  });
+  const fileData = convertEntryToFileData(myFilesEntry);
+  const volume = convertVolumeInfoAndMetadataToVolume(
+      downloadsVolumeInfo, createFakeVolumeMetadata(downloadsVolumeInfo));
   return {fileData, volume};
 }
 
 /** Create FileData for drive root entry. */
-function createDriveRootEntryFileData(): FileData {
-  const driveEntry = new EntryList(
+function createDriveRootEntryListFileData(): FileData {
+  const driveRootEntryList = new EntryList(
       'Google Drive', VolumeManagerCommon.RootType.DRIVE_FAKE_ROOT);
-  return createFakeFileData({
-    entry: driveEntry,
-    label: 'Google Drive',
-    type: EntryType.ENTRY_LIST,
-  });
+  return convertEntryToFileData(driveRootEntryList);
 }
 
 /** Create FileData for trash entry. */
 function createTrashEntryFileData(): FileData {
   const trashEntry = new TrashRootEntry();
-  return createFakeFileData({
-    entry: trashEntry,
-    label: 'Bin',
-    type: EntryType.TRASH,
-  });
+  return convertEntryToFileData(trashEntry);
 }
 
 /** Create android apps. */
@@ -118,18 +94,9 @@
   const {volumeManager} = window.fileManager;
   volumeManager.volumeInfoList.add(volumeInfo);
   const volumeEntry = new VolumeEntry(volumeInfo);
-  const fileData = createFakeFileData({
-    entry: volumeEntry,
-    label,
-    type: EntryType.VOLUME_ROOT,
-  });
-  const volume = createFakeVolume({
-    volumeId: volumeInfo.volumeId,
-    volumeType: volumeInfo.volumeType,
-    label,
-    rootKey: volumeEntry.toURL(),
-    devicePath,
-  });
+  const fileData = convertEntryToFileData(volumeEntry);
+  const volume = convertVolumeInfoAndMetadataToVolume(
+      volumeInfo, createFakeVolumeMetadata(volumeInfo));
   return {fileData, volume};
 }
 
@@ -139,44 +106,44 @@
  * 2. manages NavigationSection for the relevant volumes.
  * 3. keeps MTP/Archive/Removable volumes on the original order.
  */
-export function testNavigationRoots() {
-  const currentState = getEmptyState();
+export async function testNavigationRoots(done: () => void) {
+  const initialState = getEmptyState();
   // Put recent entry in the store.
   const recentEntryFileData = createRecentFileData();
-  currentState.allEntries[recentRootKey] = recentEntryFileData;
+  initialState.allEntries[recentRootKey] = recentEntryFileData;
   // Put 2 shortcut entries in the store.
   const shortcutEntryFileData1 =
       createShortcutEntryFileData('drive', 'shortcut1', 'Shortcut 1');
-  currentState.allEntries[shortcutEntryFileData1.entry.toURL()] =
+  initialState.allEntries[shortcutEntryFileData1.entry.toURL()] =
       shortcutEntryFileData1;
-  currentState.folderShortcuts.push(shortcutEntryFileData1.entry.toURL());
+  initialState.folderShortcuts.push(shortcutEntryFileData1.entry.toURL());
   const shortcutEntryFileData2 =
       createShortcutEntryFileData('drive', 'shortcut2', 'Shortcut 2');
-  currentState.allEntries[shortcutEntryFileData2.entry.toURL()] =
+  initialState.allEntries[shortcutEntryFileData2.entry.toURL()] =
       shortcutEntryFileData2;
-  currentState.folderShortcuts.push(shortcutEntryFileData2.entry.toURL());
+  initialState.folderShortcuts.push(shortcutEntryFileData2.entry.toURL());
   // Put MyFiles entry in the store.
   const myFilesVolume = createMyFilesEntryFileData();
-  currentState.allEntries[myFilesVolume.fileData.entry.toURL()] =
+  initialState.allEntries[myFilesVolume.fileData.entry.toURL()] =
       myFilesVolume.fileData;
-  currentState.volumes[myFilesVolume.volume.volumeId] = myFilesVolume.volume;
+  initialState.volumes[myFilesVolume.volume.volumeId] = myFilesVolume.volume;
   // Put drive entry in the store.
-  const driveRootEntryFileData = createDriveRootEntryFileData();
-  currentState.allEntries[driveRootEntryListKey] = driveRootEntryFileData;
+  const driveRootEntryFileData = createDriveRootEntryListFileData();
+  initialState.allEntries[driveRootEntryListKey] = driveRootEntryFileData;
   // Put trash entry in the store.
   const trashEntryFileData = createTrashEntryFileData();
-  currentState.allEntries[trashRootKey] = trashEntryFileData;
+  initialState.allEntries[trashRootKey] = trashEntryFileData;
   // Put the android apps in the store.
   const androidAppsData = createAndroidApps();
-  currentState.androidApps[androidAppsData[0].packageName] = androidAppsData[0];
-  currentState.androidApps[androidAppsData[1].packageName] = androidAppsData[1];
+  initialState.androidApps[androidAppsData[0].packageName] = androidAppsData[0];
+  initialState.androidApps[androidAppsData[1].packageName] = androidAppsData[1];
 
   // Create different volumes.
   const providerVolume1 = createVolumeFileData(
       VolumeManagerCommon.VolumeType.PROVIDED, 'provided:prov1');
-  currentState.allEntries[providerVolume1.fileData.entry.toURL()] =
+  initialState.allEntries[providerVolume1.fileData.entry.toURL()] =
       providerVolume1.fileData;
-  currentState.volumes[providerVolume1.volume.volumeId] =
+  initialState.volumes[providerVolume1.volume.volumeId] =
       providerVolume1.volume;
 
   // Set the device paths of the removable volumes to different strings to
@@ -184,53 +151,56 @@
   const hogeVolume = createVolumeFileData(
       VolumeManagerCommon.VolumeType.REMOVABLE, 'removable:hoge', 'Hoge',
       'device/path/1');
-  currentState.allEntries[hogeVolume.fileData.entry.toURL()] =
+  initialState.allEntries[hogeVolume.fileData.entry.toURL()] =
       hogeVolume.fileData;
-  currentState.volumes[hogeVolume.volume.volumeId] = hogeVolume.volume;
+  initialState.volumes[hogeVolume.volume.volumeId] = hogeVolume.volume;
 
   const fugaVolume = createVolumeFileData(
       VolumeManagerCommon.VolumeType.REMOVABLE, 'removable:fuga', 'Fuga',
       'device/path/2');
-  currentState.allEntries[fugaVolume.fileData.entry.toURL()] =
+  initialState.allEntries[fugaVolume.fileData.entry.toURL()] =
       fugaVolume.fileData;
-  currentState.volumes[fugaVolume.volume.volumeId] = fugaVolume.volume;
+  initialState.volumes[fugaVolume.volume.volumeId] = fugaVolume.volume;
 
   const archiveVolume = createVolumeFileData(
       VolumeManagerCommon.VolumeType.ARCHIVE, 'archive:a-rar');
-  currentState.allEntries[archiveVolume.fileData.entry.toURL()] =
+  initialState.allEntries[archiveVolume.fileData.entry.toURL()] =
       archiveVolume.fileData;
-  currentState.volumes[archiveVolume.volume.volumeId] = archiveVolume.volume;
+  initialState.volumes[archiveVolume.volume.volumeId] = archiveVolume.volume;
 
   const mtpVolume =
       createVolumeFileData(VolumeManagerCommon.VolumeType.MTP, 'mtp:a-phone');
-  currentState.allEntries[mtpVolume.fileData.entry.toURL()] =
+  initialState.allEntries[mtpVolume.fileData.entry.toURL()] =
       mtpVolume.fileData;
-  currentState.volumes[mtpVolume.volume.volumeId] = mtpVolume.volume;
+  initialState.volumes[mtpVolume.volume.volumeId] = mtpVolume.volume;
 
   const providerVolume2 = createVolumeFileData(
       VolumeManagerCommon.VolumeType.PROVIDED, 'provided:prov2');
-  currentState.allEntries[providerVolume2.fileData.entry.toURL()] =
+  initialState.allEntries[providerVolume2.fileData.entry.toURL()] =
       providerVolume2.fileData;
-  currentState.volumes[providerVolume2.volume.volumeId] =
+  initialState.volumes[providerVolume2.volume.volumeId] =
       providerVolume2.volume;
 
   const androidFilesVolume = createVolumeFileData(
       VolumeManagerCommon.VolumeType.ANDROID_FILES, 'android_files:droid');
   androidFilesVolume.volume.prefixKey = myFilesVolume.fileData.entry.toURL();
-  currentState.allEntries[androidFilesVolume.fileData.entry.toURL()] =
+  initialState.allEntries[androidFilesVolume.fileData.entry.toURL()] =
       androidFilesVolume.fileData;
-  currentState.volumes[androidFilesVolume.volume.volumeId] =
+  initialState.volumes[androidFilesVolume.volume.volumeId] =
       androidFilesVolume.volume;
 
   const smbVolume = createVolumeFileData(
       VolumeManagerCommon.VolumeType.SMB, 'smb:file-share');
-  currentState.allEntries[smbVolume.fileData.entry.toURL()] =
+  initialState.allEntries[smbVolume.fileData.entry.toURL()] =
       smbVolume.fileData;
-  currentState.volumes[smbVolume.volume.volumeId] = smbVolume.volume;
+  initialState.volumes[smbVolume.volume.volumeId] = smbVolume.volume;
 
-  const newState =
-      refreshNavigationRoots(currentState, refreshNavigationRootsAction());
-  // Navigation roots built above:
+  const store = setupStore(initialState);
+
+  // Dispatch an action to refresh navigation roots.
+  store.dispatch(refreshNavigationRoots());
+
+  // Expect navigation roots being built in the store:
   //  1.  fake-entry://recent
   //  2.  /root/shortcut1
   //  3.  /root/shortcut2
@@ -253,357 +223,449 @@
 
   // Check items order and that MTP/Archive/Removable respect the original
   // order.
-  const {roots} = newState.navigation;
-  assertEquals(15, roots.length);
+  const want: State['navigation']['roots'] = [
+    // recent.
+    {
+      key: recentEntryFileData.entry.toURL(),
+      section: NavigationSection.TOP,
+      separator: false,
+      type: NavigationType.RECENT,
+    },
+    // shortcut1.
+    {
+      key: shortcutEntryFileData1.entry.toURL(),
+      section: NavigationSection.TOP,
+      separator: false,
+      type: NavigationType.SHORTCUT,
+    },
+    // shortcut2.
+    {
+      key: shortcutEntryFileData2.entry.toURL(),
+      section: NavigationSection.TOP,
+      separator: false,
+      type: NavigationType.SHORTCUT,
+    },
+    // My Files.
+    {
+      key: myFilesVolume.fileData.entry.toURL(),
+      section: NavigationSection.MY_FILES,
+      separator: true,
+      type: NavigationType.VOLUME,
+    },
+    // Drive.
+    {
+      key: driveRootEntryFileData.entry.toURL(),
+      section: NavigationSection.CLOUD,
+      separator: true,
+      type: NavigationType.DRIVE,
+    },
+    // Trash.
+    {
+      key: trashEntryFileData.entry.toURL(),
+      section: NavigationSection.TRASH,
+      separator: true,
+      type: NavigationType.TRASH,
+    },
+    // FSP, and SMB are grouped together.
+    // smb:file-share.
+    {
+      key: smbVolume.fileData.entry.toURL(),
+      section: NavigationSection.CLOUD,
+      separator: true,
+      type: NavigationType.VOLUME,
+    },
+    // provided:prov1.
+    {
+      key: providerVolume1.fileData.entry.toURL(),
+      section: NavigationSection.CLOUD,
+      separator: false,
+      type: NavigationType.VOLUME,
+    },
+    // provided:prov2.
+    {
+      key: providerVolume2.fileData.entry.toURL(),
+      section: NavigationSection.CLOUD,
+      separator: false,
+      type: NavigationType.VOLUME,
+    },
+    // MTP/Archive/Removable are grouped together.
+    // removable:hoge.
+    {
+      key: hogeVolume.fileData.entry.toURL(),
+      section: NavigationSection.REMOVABLE,
+      separator: true,
+      type: NavigationType.VOLUME,
+    },
+    // removable:fuga.
+    {
+      key: fugaVolume.fileData.entry.toURL(),
+      section: NavigationSection.REMOVABLE,
+      separator: false,
+      type: NavigationType.VOLUME,
+    },
+    // archive:a-rar.
+    {
+      key: archiveVolume.fileData.entry.toURL(),
+      section: NavigationSection.REMOVABLE,
+      separator: false,
+      type: NavigationType.VOLUME,
+    },
+    // mtp:a-phone.
+    {
+      key: mtpVolume.fileData.entry.toURL(),
+      section: NavigationSection.REMOVABLE,
+      separator: false,
+      type: NavigationType.VOLUME,
+    },
+    // android:app1.
+    {
+      key: androidAppsData[0].packageName,
+      section: NavigationSection.ANDROID_APPS,
+      separator: true,
+      type: NavigationType.ANDROID_APPS,
+    },
+    // android:app2.
+    {
+      key: androidAppsData[1].packageName,
+      section: NavigationSection.ANDROID_APPS,
+      separator: false,
+      type: NavigationType.ANDROID_APPS,
+    },
+  ];
+  await waitDeepEquals(store, want, (state) => state.navigation.roots);
 
-  // recent.
-  assertEquals(recentEntryFileData.entry.toURL(), roots[0]!.key);
-  assertEquals(NavigationSection.TOP, roots[0]!.section);
-  assertEquals(false, roots[0]!.separator);
-  assertEquals(NavigationType.RECENT, roots[0]!.type);
-  // shortcut1.
-  assertEquals(shortcutEntryFileData1.entry.toURL(), roots[1]!.key);
-  assertEquals(NavigationSection.TOP, roots[1]!.section);
-  assertEquals(false, roots[1]!.separator);
-  assertEquals(NavigationType.SHORTCUT, roots[1]!.type);
-  // shortcut2.
-  assertEquals(shortcutEntryFileData2.entry.toURL(), roots[2]!.key);
-  assertEquals(NavigationSection.TOP, roots[2]!.section);
-  assertEquals(false, roots[2]!.separator);
-  assertEquals(NavigationType.SHORTCUT, roots[2]!.type);
-
-  // My Files.
-  assertEquals(myFilesVolume.fileData.entry.toURL(), roots[3]!.key);
-  assertEquals(NavigationSection.MY_FILES, roots[3]!.section);
-  assertEquals(true, roots[3]!.separator);
-  assertEquals(NavigationType.VOLUME, roots[3]!.type);
-
-  // Drive.
-  assertEquals(driveRootEntryFileData.entry.toURL(), roots[4]!.key);
-  assertEquals(NavigationSection.CLOUD, roots[4]!.section);
-  assertEquals(true, roots[4]!.separator);
-  assertEquals(NavigationType.DRIVE, roots[4]!.type);
-
-  // Trash.
-  assertEquals(trashEntryFileData.entry.toURL(), roots[5]!.key);
-  assertEquals(NavigationSection.TRASH, roots[5]!.section);
-  assertEquals(true, roots[5]!.separator);
-  assertEquals(NavigationType.TRASH, roots[5]!.type);
-
-  // FSP, and SMB are grouped together.
-  // smb:file-share.
-  assertEquals(smbVolume.fileData.entry.toURL(), roots[6]!.key);
-  assertEquals(NavigationSection.CLOUD, roots[6]!.section);
-  assertEquals(true, roots[6]!.separator);
-  assertEquals(NavigationType.VOLUME, roots[6]!.type);
-  // provided:prov1.
-  assertEquals(providerVolume1.fileData.entry.toURL(), roots[7]!.key);
-  assertEquals(NavigationSection.CLOUD, roots[7]!.section);
-  assertEquals(false, roots[7]!.separator);
-  assertEquals(NavigationType.VOLUME, roots[7]!.type);
-  // provided:prov2.
-  assertEquals(providerVolume2.fileData.entry.toURL(), roots[8]!.key);
-  assertEquals(NavigationSection.CLOUD, roots[8]!.section);
-  assertEquals(false, roots[8]!.separator);
-  assertEquals(NavigationType.VOLUME, roots[8]!.type);
-
-  // MTP/Archive/Removable are grouped together.
-  // removable:hoge.
-  assertEquals(hogeVolume.fileData.entry.toURL(), roots[9]!.key);
-  assertEquals(NavigationSection.REMOVABLE, roots[9]!.section);
-  assertEquals(true, roots[9]!.separator);
-  assertEquals(NavigationType.VOLUME, roots[9]!.type);
-  // removable:fuga.
-  assertEquals(fugaVolume.fileData.entry.toURL(), roots[10]!.key);
-  assertEquals(NavigationSection.REMOVABLE, roots[10]!.section);
-  assertEquals(false, roots[10]!.separator);
-  assertEquals(NavigationType.VOLUME, roots[10]!.type);
-  // archive:a-rar.
-  assertEquals(archiveVolume.fileData.entry.toURL(), roots[11]!.key);
-  assertEquals(NavigationSection.REMOVABLE, roots[11]!.section);
-  assertEquals(false, roots[11]!.separator);
-  assertEquals(NavigationType.VOLUME, roots[11]!.type);
-  // mtp:a-phone.
-  assertEquals(mtpVolume.fileData.entry.toURL(), roots[12]!.key);
-  assertEquals(NavigationSection.REMOVABLE, roots[12]!.section);
-  assertEquals(false, roots[12]!.separator);
-  assertEquals(NavigationType.VOLUME, roots[12]!.type);
-
-  // android:app1
-  assertEquals(androidAppsData[0].packageName, roots[13]!.key);
-  assertEquals(NavigationSection.ANDROID_APPS, roots[13]!.section);
-  assertEquals(true, roots[13]!.separator);
-  assertEquals(NavigationType.ANDROID_APPS, roots[13]!.type);
-  // android:app2
-  assertEquals(androidAppsData[1].packageName, roots[14]!.key);
-  assertEquals(NavigationSection.ANDROID_APPS, roots[14]!.section);
-  assertEquals(false, roots[14]!.separator);
-  assertEquals(NavigationType.ANDROID_APPS, roots[14]!.type);
+  done();
 }
 
 /**
  * Tests navigation roots with no Recents.
  */
-export function testNavigationRootsWithoutRecents() {
-  const currentState = getEmptyState();
+export async function testNavigationRootsWithoutRecents(done: () => void) {
+  const initialState = getEmptyState();
   // Put shortcut entry in the store.
   const shortcutEntryFileData =
       createShortcutEntryFileData('drive', 'shortcut', 'Shortcut');
-  currentState.allEntries[shortcutEntryFileData.entry.toURL()] =
+  initialState.allEntries[shortcutEntryFileData.entry.toURL()] =
       shortcutEntryFileData;
-  currentState.folderShortcuts.push(shortcutEntryFileData.entry.toURL());
+  initialState.folderShortcuts.push(shortcutEntryFileData.entry.toURL());
   // Put MyFiles entry in the store.
   const myFilesVolume = createMyFilesEntryFileData();
-  currentState.allEntries[myFilesVolume.fileData.entry.toURL()] =
+  initialState.allEntries[myFilesVolume.fileData.entry.toURL()] =
       myFilesVolume.fileData;
-  currentState.volumes[myFilesVolume.volume.volumeId] = myFilesVolume.volume;
+  initialState.volumes[myFilesVolume.volume.volumeId] = myFilesVolume.volume;
 
-  const newState =
-      refreshNavigationRoots(currentState, refreshNavigationRootsAction());
+  const store = setupStore(initialState);
+
+  // Dispatch an action to refresh navigation roots.
+  store.dispatch(refreshNavigationRoots());
 
   // Expect 2 navigation roots.
-  const {roots} = newState.navigation;
-  assertEquals(2, roots.length);
-  assertDeepEquals(
-      {
-        key: shortcutEntryFileData.entry.toURL(),
-        section: NavigationSection.TOP,
-        separator: false,
-        type: NavigationType.SHORTCUT,
-      },
-      roots[0]);
-  assertEquals(myFilesVolume.fileData.entry.toURL(), roots[1]!.key);
+  const want: State['navigation']['roots'] = [
+    // shortcut.
+    {
+      key: shortcutEntryFileData.entry.toURL(),
+      section: NavigationSection.TOP,
+      separator: false,
+      type: NavigationType.SHORTCUT,
+    },
+    // My Files volume.
+    {
+      key: myFilesVolume.fileData.entry.toURL(),
+      section: NavigationSection.MY_FILES,
+      separator: true,
+      type: NavigationType.VOLUME,
+    },
+  ];
+  await waitDeepEquals(store, want, (state) => state.navigation.roots);
+
+  done();
 }
 
 /**
  * Tests navigation roots with fake MyFiles.
  */
-export function testNavigationRootsWithFakeMyFiles() {
-  const currentState = getEmptyState();
+export async function testNavigationRootsWithFakeMyFiles(done: () => void) {
+  const initialState = getEmptyState();
   // Put recent entry in the store.
   const recentEntryFileData = createRecentFileData();
-  currentState.allEntries[recentRootKey] = recentEntryFileData;
+  initialState.allEntries[recentRootKey] = recentEntryFileData;
   // Put MyFiles entry in the store.
   const myFilesEntryList =
       new EntryList('My files', VolumeManagerCommon.RootType.MY_FILES);
-  currentState.allEntries[myFilesEntryList.toURL()] = createFakeFileData({
-    entry: myFilesEntryList,
-    label: 'My files',
-    type: EntryType.ENTRY_LIST,
-  });
+  initialState.allEntries[myFilesEntryList.toURL()] =
+      convertEntryToFileData(myFilesEntryList);
 
-  const newState =
-      refreshNavigationRoots(currentState, refreshNavigationRootsAction());
+  const store = setupStore(initialState);
+
+  // Dispatch an action to refresh navigation roots.
+  store.dispatch(refreshNavigationRoots());
 
   // Expect 2 navigation roots.
-  const {roots} = newState.navigation;
-  assertEquals(2, roots.length);
-  // The type of MyFiles navigation root should be ENTRY_LIST.
-  assertDeepEquals(
-      {
-        key: myFilesEntryList.toURL(),
-        section: NavigationSection.MY_FILES,
-        separator: true,
-        type: NavigationType.ENTRY_LIST,
-      },
-      roots[1]);
+  const want: State['navigation']['roots'] = [
+    // recent.
+    {
+      key: recentEntryFileData.entry.toURL(),
+      section: NavigationSection.TOP,
+      separator: false,
+      type: NavigationType.RECENT,
+    },
+    // My Files entry list.
+    {
+      key: myFilesEntryList.toURL(),
+      section: NavigationSection.MY_FILES,
+      separator: true,
+      type: NavigationType.ENTRY_LIST,
+    },
+  ];
+  await waitDeepEquals(store, want, (state) => state.navigation.roots);
+
+  done();
 }
 
 /**
  * Tests navigation roots with volumes.
  */
-export function testNavigationRootsWithVolumes() {
-  const currentState = getEmptyState();
+export async function testNavigationRootsWithVolumes(done: () => void) {
+  const initialState = getEmptyState();
   // Put recent entry in the store.
   const recentEntryFileData = createRecentFileData();
-  currentState.allEntries[recentRootKey] = recentEntryFileData;
+  initialState.allEntries[recentRootKey] = recentEntryFileData;
   // Put MyFiles entry in the store.
   const myFilesVolume = createMyFilesEntryFileData();
-  currentState.allEntries[myFilesVolume.fileData.entry.toURL()] =
+  initialState.allEntries[myFilesVolume.fileData.entry.toURL()] =
       myFilesVolume.fileData;
-  currentState.volumes[myFilesVolume.volume.volumeId] = myFilesVolume.volume;
+  initialState.volumes[myFilesVolume.volume.volumeId] = myFilesVolume.volume;
   // Put drive entry in the store.
-  const driveRootEntryFileData = createDriveRootEntryFileData();
-  currentState.allEntries[driveRootEntryListKey] = driveRootEntryFileData;
+  const driveRootEntryFileData = createDriveRootEntryListFileData();
+  initialState.allEntries[driveRootEntryListKey] = driveRootEntryFileData;
 
   // Put removable volume 'hoge' in the store.
   const hogeVolume = createVolumeFileData(
       VolumeManagerCommon.VolumeType.REMOVABLE, 'removable:hoge', 'Hoge',
       'device/path/1');
-  currentState.allEntries[hogeVolume.fileData.entry.toURL()] =
+  initialState.allEntries[hogeVolume.fileData.entry.toURL()] =
       hogeVolume.fileData;
-  currentState.volumes[hogeVolume.volume.volumeId] = hogeVolume.volume;
+  initialState.volumes[hogeVolume.volume.volumeId] = hogeVolume.volume;
 
   // Create a shortcut for the 'hoge' volume in the store.
   const hogeShortcutEntryFileData = createShortcutEntryFileData(
       hogeVolume.volume.volumeId, 'shortcut-hoge', 'Hoge shortcut');
-  currentState.allEntries[hogeShortcutEntryFileData.entry.toURL()] =
+  initialState.allEntries[hogeShortcutEntryFileData.entry.toURL()] =
       hogeShortcutEntryFileData;
-  currentState.folderShortcuts.push(hogeShortcutEntryFileData.entry.toURL());
+  initialState.folderShortcuts.push(hogeShortcutEntryFileData.entry.toURL());
 
   // Put removable volume 'fuga' in the store. Not a partition, so set a
   // different device path to 'hoge'.
   const fugaVolume = createVolumeFileData(
       VolumeManagerCommon.VolumeType.REMOVABLE, 'removable:fuga', 'Fuga',
       'device/path/2');
-  currentState.allEntries[fugaVolume.fileData.entry.toURL()] =
+  initialState.allEntries[fugaVolume.fileData.entry.toURL()] =
       fugaVolume.fileData;
-  currentState.volumes[fugaVolume.volume.volumeId] = fugaVolume.volume;
+  initialState.volumes[fugaVolume.volume.volumeId] = fugaVolume.volume;
 
-  const newState =
-      refreshNavigationRoots(currentState, refreshNavigationRootsAction());
+  const store = setupStore(initialState);
+
+  // Dispatch an action to refresh navigation roots.
+  store.dispatch(refreshNavigationRoots());
 
   // Expect 6 navigation roots.
-  const {roots} = newState.navigation;
-  assertEquals(6, roots.length);
-  assertEquals(recentEntryFileData.entry.toURL(), roots[0]!.key);
-  assertDeepEquals(
-      {
-        key: hogeShortcutEntryFileData.entry.toURL(),
-        section: NavigationSection.TOP,
-        separator: false,
-        type: NavigationType.SHORTCUT,
-      },
-      roots[1]);
-  assertEquals(myFilesVolume.fileData.entry.toURL(), roots[2]!.key);
-  assertEquals(driveRootEntryFileData.entry.toURL(), roots[3]!.key);
-  assertDeepEquals(
-      {
-        key: hogeVolume.fileData.entry.toURL(),
-        section: NavigationSection.REMOVABLE,
-        separator: true,
-        type: NavigationType.VOLUME,
-      },
-      roots[4]);
-  assertDeepEquals(
-      {
-        key: fugaVolume.fileData.entry.toURL(),
-        section: NavigationSection.REMOVABLE,
-        separator: false,
-        type: NavigationType.VOLUME,
-      },
-      roots[5]);
+  const want: State['navigation']['roots'] = [
+    // recent.
+    {
+      key: recentEntryFileData.entry.toURL(),
+      section: NavigationSection.TOP,
+      separator: false,
+      type: NavigationType.RECENT,
+    },
+    // hoge shortcut.
+    {
+      key: hogeShortcutEntryFileData.entry.toURL(),
+      section: NavigationSection.TOP,
+      separator: false,
+      type: NavigationType.SHORTCUT,
+    },
+    // My Files.
+    {
+      key: myFilesVolume.fileData.entry.toURL(),
+      section: NavigationSection.MY_FILES,
+      separator: true,
+      type: NavigationType.VOLUME,
+    },
+    // Drive.
+    {
+      key: driveRootEntryFileData.entry.toURL(),
+      section: NavigationSection.CLOUD,
+      separator: true,
+      type: NavigationType.DRIVE,
+    },
+    // hoge volume.
+    {
+      key: hogeVolume.fileData.entry.toURL(),
+      section: NavigationSection.REMOVABLE,
+      separator: true,
+      type: NavigationType.VOLUME,
+    },
+    // fuga volume.
+    {
+      key: fugaVolume.fileData.entry.toURL(),
+      section: NavigationSection.REMOVABLE,
+      separator: false,
+      type: NavigationType.VOLUME,
+    },
+  ];
+  await waitDeepEquals(store, want, (state) => state.navigation.roots);
+
+  done();
 }
 
 /**
  * Tests that for multiple partition volumes, only the parent entry will be
  * added to the navigation roots.
  */
-export function testMultipleUsbPartitionsGrouping() {
-  const currentState = getEmptyState();
+export async function testMultipleUsbPartitionsGrouping(done: () => void) {
+  const initialState = getEmptyState();
 
   // Add parent entry list to the store.
   const devicePath = 'device/path/1';
   const parentEntry = new EntryList(
       'Partition wrap', VolumeManagerCommon.RootType.REMOVABLE, devicePath);
-  currentState.allEntries[parentEntry.toURL()] = createFakeFileData({
-    entry: parentEntry,
-    label: 'Partition wrap',
-    type: EntryType.ENTRY_LIST,
-  });
+  initialState.allEntries[parentEntry.toURL()] =
+      convertEntryToFileData(parentEntry);
   // Create 3 volumes with the same device path so the partitions are grouped.
   const partitionVolume1 = createVolumeFileData(
       VolumeManagerCommon.VolumeType.REMOVABLE, 'removable:partition1',
       'partition1', devicePath);
   partitionVolume1.volume.prefixKey = parentEntry.toURL();
-  currentState.allEntries[partitionVolume1.fileData.entry.toURL()] =
+  initialState.allEntries[partitionVolume1.fileData.entry.toURL()] =
       partitionVolume1.fileData;
-  currentState.volumes[partitionVolume1.volume.volumeId] =
+  initialState.volumes[partitionVolume1.volume.volumeId] =
       partitionVolume1.volume;
   const partitionVolume2 = createVolumeFileData(
       VolumeManagerCommon.VolumeType.REMOVABLE, 'removable:partition2',
       'partition2', devicePath);
-  currentState.allEntries[partitionVolume2.fileData.entry.toURL()] =
+  initialState.allEntries[partitionVolume2.fileData.entry.toURL()] =
       partitionVolume2.fileData;
-  currentState.volumes[partitionVolume2.volume.volumeId] =
+  initialState.volumes[partitionVolume2.volume.volumeId] =
       partitionVolume2.volume;
   partitionVolume2.volume.prefixKey = parentEntry.toURL();
   const partitionVolume3 = createVolumeFileData(
       VolumeManagerCommon.VolumeType.REMOVABLE, 'removable:partition3',
       'partition3', devicePath);
-  currentState.allEntries[partitionVolume3.fileData.entry.toURL()] =
+  initialState.allEntries[partitionVolume3.fileData.entry.toURL()] =
       partitionVolume3.fileData;
-  currentState.volumes[partitionVolume3.volume.volumeId] =
+  initialState.volumes[partitionVolume3.volume.volumeId] =
       partitionVolume3.volume;
   partitionVolume3.volume.prefixKey = parentEntry.toURL();
 
-  const newState =
-      refreshNavigationRoots(currentState, refreshNavigationRootsAction());
+  const store = setupStore(initialState);
+
+  // Dispatch an action to refresh navigation roots.
+  store.dispatch(refreshNavigationRoots());
 
   // Expect only the parent entry and MyFiles being added to the navigation
   // roots.
-  const {roots} = newState.navigation;
-  assertEquals(2, roots.length);
-  assertEquals(myFilesEntryListKey, roots[0]!.key);
-  assertDeepEquals(
-      {
-        key: parentEntry.toURL(),
-        section: NavigationSection.REMOVABLE,
-        separator: true,
-        type: NavigationType.VOLUME,
-      },
-      roots[1]);
+  const want: State['navigation']['roots'] = [
+    // My Files entry list.
+    {
+      key: myFilesEntryListKey,
+      section: NavigationSection.MY_FILES,
+      separator: true,
+      type: NavigationType.ENTRY_LIST,
+    },
+    // parent entry for all removable partitions.
+    {
+      key: parentEntry.toURL(),
+      section: NavigationSection.REMOVABLE,
+      separator: true,
+      type: NavigationType.VOLUME,
+    },
+  ];
+  await waitDeepEquals(store, want, (state) => state.navigation.roots);
+
+  done();
 }
 
 /**
  * Tests that the volumes filtered by the volume manager won't be shown in the
  * navigation roots.
  */
-export function testNavigationRootsWithFilteredVolume() {
-  const currentState = getEmptyState();
-  // Put 2 volumes in the store.
+export async function testNavigationRootsWithFilteredVolume(done: () => void) {
+  const initialState = getEmptyState();
+  // Put volume1 in the store.
   const volume1 = createVolumeFileData(
       VolumeManagerCommon.VolumeType.REMOVABLE, 'removable1');
-  currentState.allEntries[volume1.fileData.entry.toURL()] = volume1.fileData;
-  currentState.volumes[volume1.volume.volumeId] = volume1.volume;
-  // Volume2 is not added to VolumeManager's volumeInfoList.
+  initialState.allEntries[volume1.fileData.entry.toURL()] = volume1.fileData;
+  initialState.volumes[volume1.volume.volumeId] = volume1.volume;
+  // Put volume2 in the store. Note: without calling createVolumeFileData(),
+  // volume2 won't be in volumeManager's volumeInfoList, thus should be filtered
+  // out.
   const volumeInfo2 = MockVolumeManager.createMockVolumeInfo(
       VolumeManagerCommon.VolumeType.REMOVABLE, 'removable2');
   const volumeEntry2 = new VolumeEntry(volumeInfo2);
-  currentState.allEntries[volumeEntry2.toURL()] = createFakeFileData({
-    entry: volumeEntry2,
-    label: volumeInfo2.label,
-    type: EntryType.VOLUME_ROOT,
-  });
-  currentState.volumes[volumeInfo2.volumeId] = createFakeVolume({
-    volumeId: volumeInfo2.volumeId,
-    volumeType: volumeInfo2.volumeType,
-    label: volumeInfo2.label,
-    rootKey: volumeEntry2.toURL(),
-  });
+  initialState.allEntries[volumeEntry2.toURL()] =
+      convertEntryToFileData(volumeEntry2);
+  initialState.volumes[volumeInfo2.volumeId] =
+      convertVolumeInfoAndMetadataToVolume(
+          volumeInfo2, createFakeVolumeMetadata(volumeInfo2));
 
-  const newState =
-      refreshNavigationRoots(currentState, refreshNavigationRootsAction());
+  const store = setupStore(initialState);
+
+  // Dispatch an action to refresh navigation roots.
+  store.dispatch(refreshNavigationRoots());
 
   // Expect only volume1 and MyFiles in the navigation roots.
-  const {roots} = newState.navigation;
-  assertEquals(2, roots.length);
-  assertEquals(myFilesEntryListKey, roots[0]!.key);
-  assertEquals(volume1.fileData.entry.toURL(), roots[1]!.key);
+  const want: State['navigation']['roots'] = [
+    // My Files entry list.
+    {
+      key: myFilesEntryListKey,
+      section: NavigationSection.MY_FILES,
+      separator: true,
+      type: NavigationType.ENTRY_LIST,
+    },
+    // volume1.
+    {
+      key: volume1.fileData.entry.toURL(),
+      section: NavigationSection.REMOVABLE,
+      separator: true,
+      type: NavigationType.VOLUME,
+    },
+  ];
+  await waitDeepEquals(store, want, (state) => state.navigation.roots);
+
+  done();
 }
 
 /** Tests that navigation entry can be updated correctly. */
-export function testUpdateNavigationEntry() {
-  const currentState = getEmptyState();
+export async function testUpdateNavigationEntry(done: () => void) {
+  const initialState = getEmptyState();
   // Add MyFiles entry to the store.
   const myFilesVolume = createMyFilesEntryFileData();
   const myFilesEntryKey = myFilesVolume.fileData.entry.toURL();
-  currentState.allEntries[myFilesEntryKey] = myFilesVolume.fileData;
+  initialState.allEntries[myFilesEntryKey] = myFilesVolume.fileData;
 
-  assertFalse(currentState.allEntries[myFilesEntryKey].expanded);
-  const newState = updateNavigationEntry(
-      currentState,
-      updateNavigationEntryAction({key: myFilesEntryKey, expanded: true}));
-  assertTrue(newState.allEntries[myFilesEntryKey].expanded);
+  const store = setupStore(initialState);
+
+  // Dispatch an action to update navigation entry.
+  store.dispatch(updateNavigationEntry({key: myFilesEntryKey, expanded: true}));
+
+  // Expect MyFiles entry is expanded in the store.
+  await waitDeepEquals(
+      store, true, (state) => state.allEntries[myFilesEntryKey].expanded);
+
+  done();
 }
 
 /** Tests that navigation entry won't be updated without valid file data. */
-export function testUpdateNavigationEntryWithoutValidFileData() {
-  const currentState = getEmptyState();
+export async function testUpdateNavigationEntryWithoutValidFileData(
+    done: () => void) {
+  const initialState = getEmptyState();
+  const store = setupStore(initialState);
 
-  const newState = updateNavigationEntry(
-      currentState,
-      updateNavigationEntryAction({key: 'not-exist-key', expanded: true}));
+  // Dispatch an action to update an non existed navigation entry.
+  store.dispatch(updateNavigationEntry({key: 'not-exist-key', expanded: true}));
+
   // Check state won't be touched.
-  assertEquals(currentState, newState);
+  await waitDeepEquals(store, initialState, (state) => state);
+
+  done();
 }
diff --git a/ui/file_manager/file_manager/state/reducers/ui_entries_unittest.ts b/ui/file_manager/file_manager/state/reducers/ui_entries_unittest.ts
index ae1d835..e9b5644 100644
--- a/ui/file_manager/file_manager/state/reducers/ui_entries_unittest.ts
+++ b/ui/file_manager/file_manager/state/reducers/ui_entries_unittest.ts
@@ -2,18 +2,20 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {assertArrayEquals, assertEquals} from 'chrome://webui-test/chai_assert.js';
+import {assertEquals} from 'chrome://webui-test/chai_assert.js';
 
 import {MockVolumeManager} from '../../background/js/mock_volume_manager.js';
 import {FakeEntryImpl, GuestOsPlaceholder, VolumeEntry} from '../../common/js/files_app_entry_types.js';
+import {waitUntil} from '../../common/js/test_error_reporting.js';
 import {VolumeManagerCommon} from '../../common/js/volume_manager_types.js';
-import {EntryType, FileData, State} from '../../externs/ts/state.js';
+import {FileData, State} from '../../externs/ts/state.js';
 import {VolumeInfo} from '../../externs/volume_info.js';
-import {addUiEntry as addUiEntryAction, removeUiEntry as removeUiEntryAction} from '../actions/ui_entries.js';
-import {createFakeFileData, createFakeVolume, setUpFileManagerOnWindow} from '../for_tests.js';
+import {addUiEntry, removeUiEntry} from '../actions/ui_entries.js';
+import {createFakeVolumeMetadata, setUpFileManagerOnWindow, setupStore, waitDeepEquals} from '../for_tests.js';
 import {getEmptyState} from '../store.js';
 
-import {addUiEntry, removeUiEntry} from './ui_entries.js';
+import {convertEntryToFileData} from './all_entries.js';
+import {convertVolumeInfoAndMetadataToVolume} from './volumes.js';
 
 export function setUp() {
   // sortEntries() from addUiEntry() reducer requires volumeManager and
@@ -27,224 +29,289 @@
   const {volumeManager} = window.fileManager;
   const downloadsVolumeInfo = volumeManager.getCurrentProfileVolumeInfo(
       VolumeManagerCommon.VolumeType.DOWNLOADS)!;
-  const fileData = createFakeFileData({
-    entry: new VolumeEntry(downloadsVolumeInfo),
-    volumeType: VolumeManagerCommon.VolumeType.DOWNLOADS,
-    label: 'My files',
-    type: EntryType.VOLUME_ROOT,
-  });
+  const fileData = convertEntryToFileData(new VolumeEntry(downloadsVolumeInfo));
   return {fileData, volumeInfo: downloadsVolumeInfo};
 }
 
-/** Tests a normal ui entry can be added correctly. */
-export function testAddUiEntry() {
-  const currentState: State = getEmptyState();
+/** Tests a normal UI entry can be added correctly. */
+export async function testAddUiEntry(done: () => void) {
+  const initialState = getEmptyState();
+  const store = setupStore(initialState);
+
+  // Dispatch an action to add a UI entry.
   const uiEntry =
       new FakeEntryImpl('Ui entry', VolumeManagerCommon.RootType.RECENT);
-  const newState = addUiEntry(currentState, addUiEntryAction({entry: uiEntry}));
-  assertEquals(1, newState.uiEntries.length);
-  assertEquals(uiEntry.toURL(), newState.uiEntries[0]);
+  store.dispatch(addUiEntry({entry: uiEntry}));
+
+  // Expect the newly added entry is in the store.
+  const want: Partial<State> = {
+    allEntries: {
+      [uiEntry.toURL()]: convertEntryToFileData(uiEntry),
+    },
+    uiEntries: [uiEntry.toURL()],
+  };
+  await waitDeepEquals(store, want, (state) => ({
+                                      allEntries: state.allEntries,
+                                      uiEntries: state.uiEntries,
+                                    }));
+
+  done();
 }
 
-/** Tests that a duplicate ui entry won't be added. */
-export function testAddDuplicateUiEntry() {
-  const currentState: State = getEmptyState();
+/** Tests that a duplicate UI entry won't be added. */
+export async function testAddDuplicateUiEntry(done: () => void) {
+  const initialState = getEmptyState();
+  // Add one UI entry in the store.
   const uiEntry =
       new FakeEntryImpl('Ui entry', VolumeManagerCommon.RootType.RECENT);
-  currentState.uiEntries.push(uiEntry.toURL());
-  const newState = addUiEntry(currentState, addUiEntryAction({entry: uiEntry}));
-  assertEquals(1, newState.uiEntries.length);
-  assertEquals(uiEntry.toURL(), newState.uiEntries[0]);
-  // The reference of uiEntries won't change.
-  assertEquals(currentState.uiEntries, newState.uiEntries);
+  initialState.uiEntries.push(uiEntry.toURL());
+
+  const store = setupStore(initialState);
+
+  // Dispatch an action to add an already existed UI entry.
+  store.dispatch(addUiEntry({entry: uiEntry}));
+
+  // Expect nothing changes in the store.
+  const want: State['uiEntries'] = [uiEntry.toURL()];
+  await waitDeepEquals(store, want, (state) => state.uiEntries);
+
+  done();
 }
 
 /**
- * Tests that adding ui entry for MyFiles will reset the children filed of
+ * Tests that adding UI entry for MyFiles will reset the children filed of
  * MyFiles entry.
  */
-export function testAddUiEntryForMyFiles() {
-  const currentState: State = getEmptyState();
+export async function testAddUiEntryForMyFiles(done: () => void) {
+  const initialState = getEmptyState();
   // Setup MyFiles entry in the store.
   const {fileData, volumeInfo} = createMyFilesDataWithVolumeEntry();
   const myFilesEntry = fileData.entry as VolumeEntry;
-  const myFilesVolume = createFakeVolume({
-    volumeType: volumeInfo.volumeType,
-    volumeId: volumeInfo.volumeId,
-    label: volumeInfo.label,
-    rootKey: volumeInfo.displayRoot.toURL(),
-  });
-  currentState.allEntries[fileData.entry.toURL()] = fileData;
-  currentState.volumes[volumeInfo.volumeId] = myFilesVolume;
+  const myFilesVolume = convertVolumeInfoAndMetadataToVolume(
+      volumeInfo, createFakeVolumeMetadata(volumeInfo));
+  initialState.allEntries[fileData.entry.toURL()] = fileData;
+  initialState.volumes[volumeInfo.volumeId] = myFilesVolume;
   // Add children to the MyFiles entry.
   const childEntry = new GuestOsPlaceholder(
       'Play files', 0, chrome.fileManagerPrivate.VmType.ARCVM);
-  currentState.allEntries[childEntry.toURL()] = createFakeFileData({
-    entry: childEntry,
-    label: 'Play files',
-    type: EntryType.PLACEHOLDER,
-  });
+  initialState.allEntries[childEntry.toURL()] =
+      convertEntryToFileData(childEntry);
   myFilesEntry.addEntry(childEntry);
   fileData.children.push(childEntry.toURL());
+  initialState.uiEntries.push(childEntry.toURL());
 
+  const store = setupStore(initialState);
+
+  // Dispatch an action to add a new UI entry which belongs to MyFiles.
   const uiEntry =
       new FakeEntryImpl('Linux files', VolumeManagerCommon.RootType.CROSTINI);
-  // Before calling addUiEntry(), the new uiEntry should already be in store,
-  // this is handled by cacheEntries(), we should emulate the process here. This
-  // is required for sortEntries().
-  currentState.allEntries[uiEntry.toURL()] = createFakeFileData({
-    entry: uiEntry,
-    label: 'Linux files',
-    type: EntryType.PLACEHOLDER,
-  });
-  const newState = addUiEntry(currentState, addUiEntryAction({entry: uiEntry}));
-  assertEquals(1, newState.uiEntries.length);
-  assertEquals(uiEntry.toURL(), newState.uiEntries[0]);
-  // Check the ui entry is added to MyFiles entry.
+  store.dispatch(addUiEntry({entry: uiEntry}));
+
+  // Expect 2 ui entries in the store.
+  const want: Partial<State> = {
+    allEntries: {
+      [myFilesEntry.toURL()]: {
+        ...fileData,
+        children: [
+          // Children are in sorted order.
+          uiEntry.toURL(),
+          childEntry.toURL(),
+        ],
+      },
+      [childEntry.toURL()]: convertEntryToFileData(childEntry),
+      [uiEntry.toURL()]: convertEntryToFileData(uiEntry),
+    },
+    // No sorting order, order is based on the push order.
+    uiEntries: [childEntry.toURL(), uiEntry.toURL()],
+  };
+  await waitDeepEquals(store, want, (state) => ({
+                                      allEntries: state.allEntries,
+                                      uiEntries: state.uiEntries,
+                                    }));
+
+  // Check the UI entry is added to MyFiles entry.
   assertEquals(2, myFilesEntry.getUIChildren().length);
   assertEquals(uiEntry, myFilesEntry.getUIChildren()[1]);
-  // Check the children of MyFiles entry gets updated and resorted.
-  assertArrayEquals(
-      [
-        uiEntry.toURL(),
-        childEntry.toURL(),
-      ],
-      newState.allEntries[myFilesEntry.toURL()].children);
+
+  done();
 }
 
 /**
- * Tests that ui entry won't be added to MyFiles if it's already existed.
+ * Tests that UI entry won't be added to MyFiles if it's already existed.
  */
-export function testAddDuplicateUiEntryForMyFiles() {
-  const currentState: State = getEmptyState();
+export async function testAddDuplicateUiEntryForMyFiles(done: () => void) {
+  const initialState = getEmptyState();
   const uiEntry = new GuestOsPlaceholder(
       'Play files', 0, chrome.fileManagerPrivate.VmType.ARCVM);
   // Setup MyFiles entry and add the new ui entry in the store.
   const {fileData, volumeInfo} = createMyFilesDataWithVolumeEntry();
   const myFilesEntry = fileData.entry as VolumeEntry;
-  const myFilesVolume = createFakeVolume({
-    volumeType: volumeInfo.volumeType,
-    volumeId: volumeInfo.volumeId,
-    label: volumeInfo.label,
-    rootKey: volumeInfo.displayRoot.toURL(),
-  });
-  currentState.allEntries[fileData.entry.toURL()] = fileData;
-  currentState.volumes[volumeInfo.volumeId] = myFilesVolume;
+  const myFilesVolume = convertVolumeInfoAndMetadataToVolume(
+      volumeInfo, createFakeVolumeMetadata(volumeInfo));
+  initialState.allEntries[fileData.entry.toURL()] = fileData;
+  initialState.volumes[volumeInfo.volumeId] = myFilesVolume;
   myFilesEntry.addEntry(uiEntry);
+  fileData.children.push(uiEntry.toURL());
+  initialState.uiEntries.push(uiEntry.toURL());
 
-  const newState = addUiEntry(currentState, addUiEntryAction({entry: uiEntry}));
-  assertEquals(1, newState.uiEntries.length);
-  assertEquals(uiEntry.toURL(), newState.uiEntries[0]);
-  // Check the ui entry is not being added to MyFiles entry again.
+  const store = setupStore(initialState);
+
+  // Dispatch an action to add an already existed UI entry.
+  store.dispatch(addUiEntry({entry: uiEntry}));
+
+  // Expect no changes in the store.
+  await waitDeepEquals(store, initialState, (state) => state);
+
+  // Check the UI entry is not being added to MyFiles entry again.
   assertEquals(1, myFilesEntry.getUIChildren().length);
   assertEquals(uiEntry, myFilesEntry.getUIChildren()[0]);
-  // Check the children of MyFiles has not been touched.
-  assertEquals(
-      newState.allEntries[myFilesEntry.toURL()].children,
-      currentState.allEntries[myFilesEntry.toURL()].children);
+
+  done();
 }
 
 /**
- * Tests that ui entry won't be added to MyFiles if the corresponding volume
+ * Tests that UI entry won't be added to MyFiles if the corresponding volume
  * is already existed.
  */
-export function testAddDuplicateUiEntryForMyFilesWhenVolumeExists() {
-  const currentState: State = getEmptyState();
-  // Placeholder ui entry and the volume entry it represents have the same
+export async function testAddDuplicateUiEntryForMyFilesWhenVolumeExists(
+    done: () => void) {
+  const initialState = getEmptyState();
+  // Placeholder UI entry and the volume entry it represents have the same
   // label.
   const label = 'Play files';
-  const uiEntry =
-      new GuestOsPlaceholder(label, 0, chrome.fileManagerPrivate.VmType.ARCVM);
-  // Setup MyFiles entry and add the new volume entry in the store.
+  // Setup MyFiles entry and add the volume entry in the store.
   const {fileData, volumeInfo} = createMyFilesDataWithVolumeEntry();
   const myFilesEntry = fileData.entry as VolumeEntry;
-  const myFilesVolume = createFakeVolume({
-    volumeType: volumeInfo.volumeType,
-    volumeId: volumeInfo.volumeId,
-    label: volumeInfo.label,
-    rootKey: volumeInfo.displayRoot.toURL(),
-  });
-  currentState.allEntries[fileData.entry.toURL()] = fileData;
-  currentState.volumes[volumeInfo.volumeId] = myFilesVolume;
+  const myFilesVolume = convertVolumeInfoAndMetadataToVolume(
+      volumeInfo, createFakeVolumeMetadata(volumeInfo));
+  initialState.allEntries[fileData.entry.toURL()] = fileData;
+  initialState.volumes[volumeInfo.volumeId] = myFilesVolume;
   const playFilesVolumeInfo = MockVolumeManager.createMockVolumeInfo(
       VolumeManagerCommon.VolumeType.ANDROID_FILES, label);
   const playFilesVolumeEntry = new VolumeEntry(playFilesVolumeInfo);
   myFilesEntry.addEntry(playFilesVolumeEntry);
+  fileData.children.push(playFilesVolumeEntry.toURL());
 
-  const newState = addUiEntry(currentState, addUiEntryAction({entry: uiEntry}));
-  // Check the ui entry has not been touched.
-  assertEquals(currentState.uiEntries, newState.uiEntries);
-  // Check the ui entry is not being added to MyFiles entry again.
+  const store = setupStore(initialState);
+
+  // Dispatch an action to add UI entry.
+  const uiEntry =
+      new GuestOsPlaceholder(label, 0, chrome.fileManagerPrivate.VmType.ARCVM);
+  store.dispatch(addUiEntry({entry: uiEntry}));
+
+  // Expect the UI entry is not being added to the store.
+  await waitDeepEquals(store, [], (state) => state.uiEntries);
+
+  // Check the UI entry is not being added to MyFiles entry again.
   assertEquals(1, myFilesEntry.getUIChildren().length);
   assertEquals(playFilesVolumeEntry, myFilesEntry.getUIChildren()[0]);
-  // Check the children of MyFiles has not been touched.
-  assertEquals(
-      newState.allEntries[myFilesEntry.toURL()].children,
-      currentState.allEntries[myFilesEntry.toURL()].children);
-}
 
-/** Tests that ui entry can be removed from store correctly. */
-export function testRemoveUiEntry() {
-  const currentState: State = getEmptyState();
-  const uiEntry =
-      new FakeEntryImpl('Ui entry', VolumeManagerCommon.RootType.RECENT);
-  // Setup the ui entry in both uiEntries and allEntries in the store.
-  currentState.allEntries[uiEntry.toURL()] = createFakeFileData({
-    entry: uiEntry,
-    label: 'Ui entry',
-    type: EntryType.PLACEHOLDER,
-  });
-  currentState.uiEntries.push(uiEntry.toURL());
-
-  const newState =
-      removeUiEntry(currentState, removeUiEntryAction({key: uiEntry.toURL()}));
-  assertEquals(0, newState.uiEntries.length);
-}
-
-/** Tests that removing non-existed ui entry won't do anything. */
-export function testRemoveNonExistedUiEntry() {
-  const currentState: State = getEmptyState();
-  const uiEntry =
-      new FakeEntryImpl('Ui entry', VolumeManagerCommon.RootType.TRASH);
-  const newState =
-      removeUiEntry(currentState, removeUiEntryAction({key: uiEntry.toURL()}));
-  // Check that uiEntries won't be touched.
-  assertEquals(newState.uiEntries, currentState.uiEntries);
+  done();
 }
 
 /**
- * Tests removing ui entry from MyFiles will reset the children field of
+ * Tests that UI entry will be disabled if the corresponding volume
+ * type is disabled in the volume manager.
+ */
+export async function testAddUiEntryWithDisabledVolumeType(done: () => void) {
+  const initialState = getEmptyState();
+  const store = setupStore(initialState);
+
+  // Dispatch an action to add UI entry.
+  const {volumeManager} = window.fileManager;
+  // Disable Android files volume type.
+  volumeManager.isDisabled = (volumeType) => {
+    return volumeType === VolumeManagerCommon.VolumeType.ANDROID_FILES;
+  };
+  const uiEntry = new GuestOsPlaceholder(
+      'Play files', 0, chrome.fileManagerPrivate.VmType.ARCVM);
+  store.dispatch(addUiEntry({entry: uiEntry}));
+
+  // Expect the UI entry is being disabled.
+  await waitUntil(() => uiEntry.disabled === true);
+
+  done();
+}
+
+/** Tests that UI entry can be removed from store correctly. */
+export async function testRemoveUiEntry(done: () => void) {
+  const initialState = getEmptyState();
+  const uiEntry =
+      new FakeEntryImpl('Ui entry', VolumeManagerCommon.RootType.RECENT);
+  // Setup the UI entry in both uiEntries and allEntries in the store.
+  initialState.allEntries[uiEntry.toURL()] = convertEntryToFileData(uiEntry);
+  initialState.uiEntries.push(uiEntry.toURL());
+
+  const store = setupStore(initialState);
+
+  // Dispatch an action to remove the UI entry.
+  store.dispatch(removeUiEntry({key: uiEntry.toURL()}));
+
+  // Expect the UI entry has been removed.
+  await waitDeepEquals(store, [], (state) => state.uiEntries);
+
+  done();
+}
+
+/** Tests that removing non-existed UI entry won't do anything. */
+export async function testRemoveNonExistedUiEntry(done: () => void) {
+  const initialState = getEmptyState();
+  const store = setupStore(initialState);
+
+  // Dispatch an action to remove a non-existed UI entry.
+  const uiEntry =
+      new FakeEntryImpl('Ui entry', VolumeManagerCommon.RootType.TRASH);
+  store.dispatch(removeUiEntry({key: uiEntry.toURL()}));
+
+  // Expect nothing changes in the store.
+  await waitDeepEquals(store, initialState, (state) => state);
+
+  done();
+}
+
+/**
+ * Tests removing UI entry from MyFiles will reset the children field of
  * MyFiles entry.
  */
-export function testRemoveUiEntryFromMyFiles() {
-  const currentState: State = getEmptyState();
+export async function testRemoveUiEntryFromMyFiles(done: () => void) {
+  const initialState = getEmptyState();
   const uiEntry =
       new FakeEntryImpl('Linux files', VolumeManagerCommon.RootType.CROSTINI);
   // Setup MyFiles entry and add the ui entry in the store.
   const {fileData, volumeInfo} = createMyFilesDataWithVolumeEntry();
   const myFilesEntry = fileData.entry as VolumeEntry;
-  const myFilesVolume = createFakeVolume({
-    volumeType: volumeInfo.volumeType,
-    volumeId: volumeInfo.volumeId,
-    label: volumeInfo.label,
-    rootKey: volumeInfo.displayRoot.toURL(),
-  });
-  currentState.allEntries[fileData.entry.toURL()] = fileData;
-  currentState.volumes[volumeInfo.volumeId] = myFilesVolume;
+  const myFilesVolume = convertVolumeInfoAndMetadataToVolume(
+      volumeInfo, createFakeVolumeMetadata(volumeInfo));
+  initialState.allEntries[fileData.entry.toURL()] = fileData;
+  initialState.volumes[volumeInfo.volumeId] = myFilesVolume;
   myFilesEntry.addEntry(uiEntry);
   fileData.children.push(uiEntry.toURL());
-  currentState.allEntries[uiEntry.toURL()] = createFakeFileData({
-    entry: uiEntry,
-    volumeType: VolumeManagerCommon.VolumeType.CROSTINI,
-    label: 'Linux files',
-    type: EntryType.PLACEHOLDER,
-  });
-  currentState.uiEntries.push(uiEntry.toURL());
+  initialState.allEntries[uiEntry.toURL()] = convertEntryToFileData(uiEntry);
+  initialState.uiEntries.push(uiEntry.toURL());
 
-  const newState =
-      removeUiEntry(currentState, removeUiEntryAction({key: uiEntry.toURL()}));
-  assertEquals(0, newState.uiEntries.length);
-  // Check the ui entry has also been removed from MyFiles entry.
+  const store = setupStore(initialState);
+
+  // Dispatch an action to
+  store.dispatch(removeUiEntry({key: uiEntry.toURL()}));
+
+  // Expect the entry has been removed from MyFiles.
+  const want: Partial<State> = {
+    allEntries: {
+      [myFilesEntry.toURL()]: {
+        ...convertEntryToFileData(myFilesEntry),
+        children: [],
+      },
+      [uiEntry.toURL()]: convertEntryToFileData(uiEntry),
+    },
+    uiEntries: [],
+  };
+  await waitDeepEquals(store, want, (state) => ({
+                                      allEntries: state.allEntries,
+                                      uiEntries: state.uiEntries,
+                                    }));
+
+  // Check the UI entry has also been removed from MyFiles entry.
   assertEquals(0, myFilesEntry.getUIChildren().length);
-  assertEquals(0, newState.allEntries[myFilesEntry.toURL()].children.length);
+
+  done();
 }
diff --git a/ui/file_manager/file_manager/state/reducers/volumes_unittest.ts b/ui/file_manager/file_manager/state/reducers/volumes_unittest.ts
index 55bff85..ab1748a 100644
--- a/ui/file_manager/file_manager/state/reducers/volumes_unittest.ts
+++ b/ui/file_manager/file_manager/state/reducers/volumes_unittest.ts
@@ -2,226 +2,380 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {assertEquals} from 'chrome://webui-test/chai_assert.js';
-
 import {MockVolumeManager} from '../../background/js/mock_volume_manager.js';
-import {EntryList, VolumeEntry} from '../../common/js/files_app_entry_types.js';
+import {EntryList, FakeEntryImpl, VolumeEntry} from '../../common/js/files_app_entry_types.js';
+import {waitUntil} from '../../common/js/test_error_reporting.js';
+import {str} from '../../common/js/util.js';
 import {VolumeManagerCommon} from '../../common/js/volume_manager_types.js';
-import {EntryType, FileData, PropStatus} from '../../externs/ts/state.js';
+import {FileData, State} from '../../externs/ts/state.js';
 import {VolumeInfo} from '../../externs/volume_info.js';
-import {addVolume as addVolumeAction, removeVolume as removeVolumeAction} from '../actions/volumes.js';
-import {createFakeFileData, createFakeVolume, createFakeVolumeMetadata} from '../for_tests.js';
-import {getEmptyState} from '../store.js';
+import {constants} from '../../foreground/js/constants.js';
+import {addVolume, removeVolume} from '../actions/volumes.js';
+import {createFakeVolumeMetadata, setUpFileManagerOnWindow, setupStore, waitDeepEquals} from '../for_tests.js';
+import {getEmptyState, getEntry} from '../store.js';
 
-import {addVolume, driveRootEntryListKey, removeVolume} from './volumes.js';
+import {convertEntryToFileData} from './all_entries.js';
+import {convertVolumeInfoAndMetadataToVolume, driveRootEntryListKey} from './volumes.js';
+
+export function setUp() {
+  setUpFileManagerOnWindow();
+}
+
+/** Generate MyFiles entry with fake entry list. */
+function createMyFilesDataWithEntryList(): FileData {
+  const myFilesEntryList =
+      new EntryList('My files', VolumeManagerCommon.RootType.MY_FILES);
+  return convertEntryToFileData(myFilesEntryList);
+}
 
 /** Generate MyFiles entry with real volume entry. */
 function createMyFilesDataWithVolumeEntry():
     {fileData: FileData, volumeInfo: VolumeInfo} {
-  const volumeManager = new MockVolumeManager();
+  const {volumeManager} = window.fileManager;
   const downloadsVolumeInfo = volumeManager.getCurrentProfileVolumeInfo(
       VolumeManagerCommon.VolumeType.DOWNLOADS)!;
-  const fileData = createFakeFileData({
-    entry: new VolumeEntry(downloadsVolumeInfo),
-    volumeType: VolumeManagerCommon.VolumeType.DOWNLOADS,
-    label: 'My files',
-    type: EntryType.VOLUME_ROOT,
-  });
+  const fileData = convertEntryToFileData(new VolumeEntry(downloadsVolumeInfo));
   return {fileData, volumeInfo: downloadsVolumeInfo};
 }
 
 /** Tests that MyFiles volume can be added correctly. */
-export function testAddMyFilesVolume() {
-  const currentState = getEmptyState();
-  const {fileData, volumeInfo} = createMyFilesDataWithVolumeEntry();
-  const myFilesVolume = createFakeVolume({
-    volumeType: volumeInfo.volumeType,
-    volumeId: volumeInfo.volumeId,
-    label: volumeInfo.label,
-    rootKey: volumeInfo.displayRoot.toURL(),
-  });
-  currentState.allEntries[fileData.entry.toURL()] = fileData;
-  currentState.volumes[volumeInfo.volumeId] = myFilesVolume;
-  // Mark its volume entry as disabled.
-  (fileData.entry as VolumeEntry).disabled = true;
-  // Put MyFiles and its sub volumes in the store.
-  const playFilesVolume = createFakeVolume({
-    volumeId: 'playFilesId',
-    volumeType: VolumeManagerCommon.VolumeType.ANDROID_FILES,
-    label: 'Play files',
-    rootKey: 'filesystem:chrome://android-files-url',
-  });
-  currentState.volumes[playFilesVolume.volumeId] = playFilesVolume;
-  const crostiniFilesVolume = createFakeVolume({
-    volumeId: 'volumeInMyFiles2',
-    volumeType: VolumeManagerCommon.VolumeType.CROSTINI,
-    label: 'Linux files',
-    rootKey: 'filesystem:chrome://crostini-files-url',
-  });
-  currentState.volumes[crostiniFilesVolume.volumeId] = crostiniFilesVolume;
+export async function testAddMyFilesVolume(done: () => void) {
+  const initialState = getEmptyState();
+  // Put MyFiles entry list in the store.
+  const myFilesFileData = createMyFilesDataWithEntryList();
+  const myFilesEntryList = myFilesFileData.entry as EntryList;
+  initialState.allEntries[myFilesEntryList.toURL()] = myFilesFileData;
+  initialState.uiEntries.push(myFilesEntryList.toURL());
+  // Put Play files placeholder UI entry in the store.
+  const playFilesEntry = new FakeEntryImpl(
+      'Play files', VolumeManagerCommon.RootType.ANDROID_FILES);
+  initialState.allEntries[playFilesEntry.toURL()] =
+      convertEntryToFileData(playFilesEntry);
+  myFilesEntryList.addEntry(playFilesEntry);
+  initialState.uiEntries.push(playFilesEntry.toURL());
+  myFilesFileData.children.push(playFilesEntry.toURL());
+  // Put Linux files volume entry in the store.
+  const linuxFilesVolumeInfo = MockVolumeManager.createMockVolumeInfo(
+      VolumeManagerCommon.VolumeType.CROSTINI, 'linuxFilesId', 'Linux files');
+  const linuxFilesEntry = new VolumeEntry(linuxFilesVolumeInfo);
+  const {volumeManager} = window.fileManager;
+  volumeManager.volumeInfoList.add(linuxFilesVolumeInfo);
+  initialState.allEntries[linuxFilesEntry.toURL()] =
+      convertEntryToFileData(linuxFilesEntry);
+  const linuxFilesVolume = convertVolumeInfoAndMetadataToVolume(
+      linuxFilesVolumeInfo, createFakeVolumeMetadata(linuxFilesVolumeInfo));
+  initialState.volumes[linuxFilesVolume.volumeId] = linuxFilesVolume;
+  myFilesFileData.children.push(linuxFilesEntry.toURL());
+  myFilesEntryList.addEntry(linuxFilesEntry);
 
-  const volumeMetadata = createFakeVolumeMetadata(
-      {volumeType: volumeInfo.volumeType, volumeId: volumeInfo.volumeId});
-  const newState = addVolume(currentState, addVolumeAction({
-                               volumeInfo,
-                               volumeMetadata,
-                             }));
-  const volume = newState.volumes[volumeInfo.volumeId];
-  // Check all volume fields are set correctly.
-  assertEquals(volume.volumeId, volumeMetadata.volumeId);
-  assertEquals(volume.volumeType, volumeMetadata.volumeType);
-  assertEquals(volume.rootKey, volumeInfo.displayRoot.toURL());
-  assertEquals(volume.status, PropStatus.SUCCESS);
-  assertEquals(volume.label, volumeInfo.label);
-  assertEquals(volume.error, volumeMetadata.mountCondition);
-  assertEquals(volume.deviceType, volumeMetadata.deviceType);
-  assertEquals(volume.devicePath, volumeMetadata.devicePath);
-  assertEquals(volume.isReadOnly, volumeMetadata.isReadOnly);
-  assertEquals(
-      volume.isReadOnlyRemovableDevice,
-      volumeMetadata.isReadOnlyRemovableDevice);
-  assertEquals(volume.providerId, volumeMetadata.providerId);
-  assertEquals(volume.configurable, volumeMetadata.configurable);
-  assertEquals(volume.watchable, volumeMetadata.watchable);
-  assertEquals(volume.source, volumeMetadata.source);
-  assertEquals(volume.diskFileSystemType, volumeMetadata.diskFileSystemType);
-  assertEquals(volume.iconSet, volumeMetadata.iconSet);
-  assertEquals(volume.driveLabel, volumeMetadata.driveLabel);
-  assertEquals(volume.vmType, volumeMetadata.vmType);
-  // Because its volume entry is disabled.
-  assertEquals(volume.isDisabled, true);
-  assertEquals(volume.prefixKey, undefined);
-  // Check all child volumes has prefix key setup.
-  assertEquals(
-      fileData.entry.toURL(),
-      newState.volumes[playFilesVolume.volumeId].prefixKey);
-  assertEquals(
-      fileData.entry.toURL(),
-      newState.volumes[crostiniFilesVolume.volumeId].prefixKey);
+  const store = setupStore(initialState);
+
+  // Dispatch an action to add MyFiles volume.
+  const {fileData, volumeInfo} = createMyFilesDataWithVolumeEntry();
+  const myFilesVolumeEntry = fileData.entry as VolumeEntry;
+  const volumeMetadata = createFakeVolumeMetadata(volumeInfo);
+  store.dispatch(addVolume({
+    volumeInfo,
+    volumeMetadata,
+  }));
+
+  // Expect the newly added volume is in the store.
+  myFilesVolumeEntry.addEntry(playFilesEntry);
+  myFilesVolumeEntry.addEntry(linuxFilesEntry);
+  const want: Partial<State> = {
+    allEntries: {
+      [playFilesEntry.toURL()]: convertEntryToFileData(playFilesEntry),
+      [linuxFilesEntry.toURL()]: convertEntryToFileData(linuxFilesEntry),
+      [myFilesVolumeEntry.toURL()]: fileData,
+    },
+    volumes: {
+      [linuxFilesVolumeInfo.volumeId]: {
+        ...linuxFilesVolume,
+        // Updated to MyFiles volume key.
+        prefixKey: fileData.entry.toURL(),
+      },
+      [volumeInfo.volumeId]:
+          convertVolumeInfoAndMetadataToVolume(volumeInfo, volumeMetadata),
+    },
+    uiEntries: [playFilesEntry.toURL()],
+  };
+  await waitDeepEquals(store, want, (state) => ({
+                                      allEntries: state.allEntries,
+                                      volumes: state.volumes,
+                                      uiEntries: state.uiEntries,
+                                    }));
+
+  done();
 }
 
 /** Tests that volume nested in MyFiles can be added correctly. */
-export function testAddNestedMyFilesVolume() {
-  const currentState = getEmptyState();
+export async function testAddNestedMyFilesVolume(done: () => void) {
+  const initialState = getEmptyState();
   // Put MyFiles in the store.
   const {fileData, volumeInfo} = createMyFilesDataWithVolumeEntry();
-  const myFilesVolume = createFakeVolume({
-    volumeType: volumeInfo.volumeType,
-    volumeId: volumeInfo.volumeId,
-    label: volumeInfo.label,
-    rootKey: volumeInfo.displayRoot.toURL(),
-  });
-  currentState.allEntries[fileData.entry.toURL()] = fileData;
-  currentState.volumes[volumeInfo.volumeId] = myFilesVolume;
+  const myFilesVolumeEntry = fileData.entry as VolumeEntry;
+  const myFilesVolume = convertVolumeInfoAndMetadataToVolume(
+      volumeInfo, createFakeVolumeMetadata(volumeInfo));
+  initialState.allEntries[myFilesVolumeEntry.toURL()] = fileData;
+  initialState.volumes[volumeInfo.volumeId] = myFilesVolume;
+  // Put Play files placeholder UI entry in the store.
+  const playFilesUiEntry = new FakeEntryImpl(
+      'Play files', VolumeManagerCommon.RootType.ANDROID_FILES);
+  myFilesVolumeEntry.addEntry(playFilesUiEntry);
+  fileData.children.push(playFilesUiEntry.toURL());
+  initialState.uiEntries.push(playFilesUiEntry.toURL());
+  initialState.allEntries[playFilesUiEntry.toURL()] =
+      convertEntryToFileData(playFilesUiEntry);
 
+  const store = setupStore(initialState);
+
+  // Dispatch an action to add Play files volume.
+  const {volumeManager} = window.fileManager;
   const playFilesVolumeInfo = MockVolumeManager.createMockVolumeInfo(
       VolumeManagerCommon.VolumeType.ANDROID_FILES, 'playFilesId',
-      'Play files');
-  const playFilesVolumeMetadata = createFakeVolumeMetadata({
-    volumeType: playFilesVolumeInfo.volumeType,
-    volumeId: playFilesVolumeInfo.volumeId,
-  });
-  const newState = addVolume(currentState, addVolumeAction({
-                               volumeInfo: playFilesVolumeInfo,
-                               volumeMetadata: playFilesVolumeMetadata,
-                             }));
-  // Check the newly added volume has prefix key setup.
-  assertEquals(
-      fileData.entry.toURL(),
-      newState.volumes[playFilesVolumeInfo.volumeId].prefixKey);
+      playFilesUiEntry.label);
+  volumeManager.volumeInfoList.add(playFilesVolumeInfo);
+  const playFilesVolumeMetadata = createFakeVolumeMetadata(playFilesVolumeInfo);
+  store.dispatch(addVolume({
+    volumeInfo: playFilesVolumeInfo,
+    volumeMetadata: playFilesVolumeMetadata,
+  }));
+
+  // Expect the new play file volume will be nested inside MyFiles and the old
+  // placeholder will be removed.
+  const playFilesVolumeEntry = new VolumeEntry(playFilesVolumeInfo);
+  myFilesVolumeEntry.addEntry(playFilesVolumeEntry);
+  const want: Partial<State> = {
+    allEntries: {
+      [myFilesVolumeEntry.toURL()]: {
+        ...fileData,
+        children: [playFilesVolumeEntry.toURL()],
+      },
+      [playFilesVolumeEntry.toURL()]:
+          convertEntryToFileData(playFilesVolumeEntry),
+    },
+    volumes: {
+      [myFilesVolume.volumeId]: myFilesVolume,
+      [playFilesVolumeInfo.volumeId]: {
+        ...convertVolumeInfoAndMetadataToVolume(
+            playFilesVolumeInfo, playFilesVolumeMetadata),
+        prefixKey: myFilesVolumeEntry.toURL(),
+      },
+    },
+    uiEntries: [],
+  };
+  await waitDeepEquals(store, want, (state) => ({
+                                      allEntries: state.allEntries,
+                                      volumes: state.volumes,
+                                      uiEntries: state.uiEntries,
+                                    }));
+
+  done();
 }
 
 /** Tests that drive volume can be added correctly. */
-export function testAddDriveVolume(done: () => void) {
-  const currentState = getEmptyState();
-  // Put FakeDriveRoot in the store.
-  const fakeDriveRootEntry = new EntryList(
-      'Google Drive', VolumeManagerCommon.RootType.DRIVE_FAKE_ROOT);
-  currentState.allEntries[driveRootEntryListKey] = createFakeFileData({
-    entry: fakeDriveRootEntry,
-    label: 'Google Drive',
-    type: EntryType.ENTRY_LIST,
-  });
+export async function testAddDriveVolume(done: () => void) {
+  const initialState = getEmptyState();
+  const store = setupStore(initialState);
 
-  const volumeManager = new MockVolumeManager();
+  // Dispatch an action to add Drive volume.
+  const {volumeManager} = window.fileManager;
   const driveVolumeInfo = volumeManager.getCurrentProfileVolumeInfo(
       VolumeManagerCommon.VolumeType.DRIVE)!;
-  const driveVolumeMetadata = createFakeVolumeMetadata({
-    volumeType: driveVolumeInfo.volumeType,
-    volumeId: driveVolumeInfo.volumeId,
-  });
+  const driveVolumeMetadata = createFakeVolumeMetadata(driveVolumeInfo);
   // DriveFS takes time to resolve.
-  driveVolumeInfo.resolveDisplayRoot(() => {
-    const newState = addVolume(currentState, addVolumeAction({
-                                 volumeInfo: driveVolumeInfo,
-                                 volumeMetadata: driveVolumeMetadata,
-                               }));
-    // Check the newly added volume has prefix key setup.
-    assertEquals(
-        fakeDriveRootEntry.toURL(),
-        newState.volumes[driveVolumeInfo.volumeId].prefixKey);
+  await driveVolumeInfo.resolveDisplayRoot();
+  store.dispatch(addVolume({
+    volumeInfo: driveVolumeInfo,
+    volumeMetadata: driveVolumeMetadata,
+  }));
 
-    done();
-  });
+  // Expect all fake entries inside Drive will be added as its children.
+  const myFilesFileData = createMyFilesDataWithEntryList();
+  const driveFakeRootEntryList = new EntryList(
+      str('DRIVE_DIRECTORY_LABEL'),
+      VolumeManagerCommon.RootType.DRIVE_FAKE_ROOT);
+  const driveVolumeEntry = new VolumeEntry(driveVolumeInfo);
+  const {sharedDriveDisplayRoot, computersDisplayRoot, fakeEntries} =
+      driveVolumeInfo;
+  const fakeSharedWithMeEntry =
+      fakeEntries[VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME];
+  const fakeOfflineEntry =
+      fakeEntries[VolumeManagerCommon.RootType.DRIVE_OFFLINE];
+  driveFakeRootEntryList.addEntry(driveVolumeEntry);
+  driveFakeRootEntryList.addEntry(sharedDriveDisplayRoot);
+  driveFakeRootEntryList.addEntry(computersDisplayRoot);
+  driveFakeRootEntryList.addEntry(fakeSharedWithMeEntry);
+  driveFakeRootEntryList.addEntry(fakeOfflineEntry);
+  const want: Partial<State> = {
+    allEntries: {
+      // My Drive.
+      [driveVolumeEntry.toURL()]: convertEntryToFileData(driveVolumeEntry),
+      // My Files entry list.
+      [myFilesFileData.entry.toURL()]: myFilesFileData,
+      // Fake Drive root entry list.
+      [driveRootEntryListKey]: convertEntryToFileData(driveFakeRootEntryList),
+      // Shared with me.
+      [fakeSharedWithMeEntry.toURL()]:
+          convertEntryToFileData(fakeSharedWithMeEntry),
+      // Offline.
+      [fakeOfflineEntry.toURL()]: convertEntryToFileData(fakeOfflineEntry),
+      // Shared drives and Computers won't be here because they will be cleared.
+    },
+    volumes: {
+      [driveVolumeInfo.volumeId]: {
+        ...convertVolumeInfoAndMetadataToVolume(
+            driveVolumeInfo, driveVolumeMetadata),
+        prefixKey: driveRootEntryListKey,
+      },
+    },
+    uiEntries: [
+      myFilesFileData.entry.toURL(),
+      driveRootEntryListKey,
+      fakeSharedWithMeEntry.toURL(),
+      fakeOfflineEntry.toURL(),
+    ],
+  };
+  await waitDeepEquals(store, want, (state) => ({
+                                      allEntries: state.allEntries,
+                                      volumes: state.volumes,
+                                      uiEntries: state.uiEntries,
+                                    }));
+
+  done();
 }
 
 /** Tests that multiple partition volumes can be added correctly. */
-export function testAddVolumeForMultipleUsbPartitionsGrouping() {
-  const currentState = getEmptyState();
-  // Add USB/partition-1 into the store.
-  const devicePath = 'device/path/1';
-  const partition1 = createFakeVolume({
-    volumeId: 'removable:partition1',
-    volumeType: VolumeManagerCommon.VolumeType.REMOVABLE,
-    rootKey: 'partition1-url',
-    label: 'Partition 1',
-    devicePath,
-    driveLabel: 'USB_Drive',
-  });
-  currentState.volumes[partition1.volumeId] = partition1;
-  // Add its parent entry to the store.
-  const parentEntry = new EntryList(
-      partition1.driveLabel!, VolumeManagerCommon.RootType.REMOVABLE,
-      partition1.devicePath);
-  currentState.allEntries[parentEntry.toURL()] = createFakeFileData({
-    entry: parentEntry,
-    label: partition1.driveLabel!,
-    type: EntryType.ENTRY_LIST,
-  });
+export async function testAddVolumeForMultipleUsbPartitionsGrouping(
+    done: () => void) {
+  const initialState = getEmptyState();
+  // Put partition-1 volume in the store.
+  const {volumeManager} = window.fileManager;
+  const partition1VolumeInfo = MockVolumeManager.createMockVolumeInfo(
+      VolumeManagerCommon.VolumeType.REMOVABLE, 'removable:partition1',
+      'Partition 1', '/device/path/1');
+  volumeManager.volumeInfoList.add(partition1VolumeInfo);
+  const partition1VolumeEntry = new VolumeEntry(partition1VolumeInfo);
+  const partition1FileData = convertEntryToFileData(partition1VolumeEntry);
+  const partition1VolumeMetadata =
+      createFakeVolumeMetadata(partition1VolumeInfo);
+  partition1VolumeMetadata.driveLabel = 'USB_Drive';
+  const partition1Volume = convertVolumeInfoAndMetadataToVolume(
+      partition1VolumeInfo, partition1VolumeMetadata);
+  initialState.volumes[partition1Volume.volumeId] = partition1Volume;
+  initialState.allEntries[partition1VolumeEntry.toURL()] = partition1FileData;
 
+  const store = setupStore(initialState);
+
+  // Dispatch an action to add partition-2 volume.
   const partition2VolumeInfo = MockVolumeManager.createMockVolumeInfo(
       VolumeManagerCommon.VolumeType.REMOVABLE, 'removable:partition2',
-      'Partition 2', devicePath);
-  const partition2VolumeMetadata = createFakeVolumeMetadata({
-    volumeType: partition2VolumeInfo.volumeType,
-    volumeId: partition2VolumeInfo.volumeId,
-    devicePath: partition1.devicePath,
-    driveLabel: partition1.driveLabel,
+      'Partition 2', partition1VolumeInfo.devicePath);
+  const partition2VolumeMetadata =
+      createFakeVolumeMetadata(partition2VolumeInfo);
+  partition2VolumeMetadata.driveLabel = partition1VolumeMetadata.driveLabel;
+  store.dispatch(addVolume({
+    volumeInfo: partition2VolumeInfo,
+    volumeMetadata: partition2VolumeMetadata,
+  }));
+
+  // Expect the partition-2 volume is in the store and there will be a wrapper
+  // entry created to group both partition-1 and partition-2.
+  const myFilesFileData = createMyFilesDataWithEntryList();
+  const partition2VolumeEntry = new VolumeEntry(partition2VolumeInfo);
+  const parentEntry = new EntryList(
+      partition1VolumeMetadata.driveLabel,
+      VolumeManagerCommon.RootType.REMOVABLE, partition1VolumeInfo.devicePath);
+  parentEntry.addEntry(partition1VolumeEntry);
+  parentEntry.addEntry(partition2VolumeEntry);
+  const want: Partial<State> = {
+    allEntries: {
+      // Partition-1 volume.
+      [partition1VolumeEntry.toURL()]: {
+        ...partition1FileData,
+        icon: constants.ICON_TYPES.UNKNOWN_REMOVABLE,
+      },
+      // Partition-2 volume.
+      [partition2VolumeEntry.toURL()]: {
+        ...convertEntryToFileData(partition2VolumeEntry),
+        icon: constants.ICON_TYPES.UNKNOWN_REMOVABLE,
+      },
+      // My Files entry list.
+      [myFilesFileData.entry.toURL()]: myFilesFileData,
+      // Parent wrapper entry.
+      [parentEntry.toURL()]: {
+        ...convertEntryToFileData(parentEntry),
+        isEjectable: true,
+      },
+    },
+    volumes: {
+      [partition1VolumeInfo.volumeId]: {
+        ...partition1Volume,
+        prefixKey: parentEntry.toURL(),
+      },
+      [partition2VolumeInfo.volumeId]: {
+        ...convertVolumeInfoAndMetadataToVolume(
+            partition2VolumeInfo, partition2VolumeMetadata),
+        prefixKey: parentEntry.toURL(),
+      },
+    },
+  };
+  await waitDeepEquals(store, want, (state) => ({
+                                      allEntries: state.allEntries,
+                                      volumes: state.volumes,
+                                    }));
+
+  done();
+}
+
+/**
+ * Tests that volume will be disabled if it is disabled in the volume manager.
+ */
+export async function testAddDisabledVolume(done: () => void) {
+  const initialState = getEmptyState();
+  const store = setupStore(initialState);
+
+  // Dispatch an action to add crostini volume.
+  const {volumeManager} = window.fileManager;
+  const volumeInfo = MockVolumeManager.createMockVolumeInfo(
+      VolumeManagerCommon.VolumeType.CROSTINI, 'crostini', 'Linux files');
+  volumeManager.volumeInfoList.add(volumeInfo);
+  const volumeMetadata = createFakeVolumeMetadata(volumeInfo);
+  // Disable crostini volume type.
+  volumeManager.isDisabled = (volumeType) => {
+    return volumeType === VolumeManagerCommon.VolumeType.CROSTINI;
+  };
+  store.dispatch(addVolume({volumeInfo, volumeMetadata}));
+
+  // Expect the volume entry is being disabled.
+  await waitUntil(() => {
+    const volumeEntry =
+        getEntry(store.getState(), volumeInfo.displayRoot.toURL()) as
+        VolumeEntry;
+    return volumeEntry && volumeEntry.disabled === true;
   });
-  const newState = addVolume(currentState, addVolumeAction({
-                               volumeInfo: partition2VolumeInfo,
-                               volumeMetadata: partition2VolumeMetadata,
-                             }));
-  // Check the newly added volume and all existing volumes belonging to the same
-  // group have prefix key setup.
-  assertEquals(
-      parentEntry.toURL(), newState.volumes[partition1.volumeId].prefixKey);
-  assertEquals(
-      parentEntry.toURL(),
-      newState.volumes[partition2VolumeInfo.volumeId].prefixKey);
+
+  done();
 }
 
 /** Tests that volume can be removed correctly. */
-export function testRemoveVolume() {
-  const currentState = getEmptyState();
-  const volume = createFakeVolume({
-    volumeId: 'test',
-    volumeType: VolumeManagerCommon.VolumeType.ARCHIVE,
-    label: 'test.zip',
-    rootKey: 'test-root',
-  });
-  currentState.volumes[volume.volumeId] = volume;
-  const newState = removeVolume(
-      currentState, removeVolumeAction({volumeId: volume.volumeId}));
-  assertEquals(undefined, newState.volumes[volume.volumeId]);
+export async function testRemoveVolume(done: () => void) {
+  const initialState = getEmptyState();
+  const {volumeManager} = window.fileManager;
+  const volumeInfo = MockVolumeManager.createMockVolumeInfo(
+      VolumeManagerCommon.VolumeType.ARCHIVE, 'test', 'test.zip');
+  volumeManager.volumeInfoList.add(volumeInfo);
+  const volume = convertVolumeInfoAndMetadataToVolume(
+      volumeInfo, createFakeVolumeMetadata(volumeInfo));
+  initialState.volumes[volume.volumeId] = volume;
+
+  const store = setupStore(initialState);
+
+  // Dispatch an action to remove the volume.
+  store.dispatch(removeVolume({volumeId: volume.volumeId}));
+
+  // Expect the volume will be removed from the store.
+  await waitDeepEquals(store, {}, (state) => state.volumes);
+
+  done();
 }
diff --git a/ui/file_manager/file_manager/widgets/xf_icon_unittest.ts b/ui/file_manager/file_manager/widgets/xf_icon_unittest.ts
index a4bf3c5..b5410ef 100644
--- a/ui/file_manager/file_manager/widgets/xf_icon_unittest.ts
+++ b/ui/file_manager/file_manager/widgets/xf_icon_unittest.ts
@@ -77,10 +77,10 @@
 
   const span = getSpanFromIcon(icon);
   assertTrue(span.classList.contains('keep-color'));
-  assertTrue(window.getComputedStyle(span).backgroundImage.includes(
-      '-webkit-image-set'));
-  assertTrue(window.getComputedStyle(span).backgroundImage.includes('1x'));
-  assertFalse(window.getComputedStyle(span).backgroundImage.includes('2x'));
+  assertTrue(
+      window.getComputedStyle(span).backgroundImage.includes('image-set'));
+  assertTrue(window.getComputedStyle(span).backgroundImage.includes('1dppx'));
+  assertFalse(window.getComputedStyle(span).backgroundImage.includes('2dppx'));
 
   done();
 }
@@ -96,10 +96,10 @@
 
   const span = getSpanFromIcon(icon);
   assertTrue(span.classList.contains('keep-color'));
-  assertTrue(window.getComputedStyle(span).backgroundImage.includes(
-      '-webkit-image-set'));
-  assertFalse(window.getComputedStyle(span).backgroundImage.includes('1x'));
-  assertTrue(window.getComputedStyle(span).backgroundImage.includes('2x'));
+  assertTrue(
+      window.getComputedStyle(span).backgroundImage.includes('image-set'));
+  assertFalse(window.getComputedStyle(span).backgroundImage.includes('1dppx'));
+  assertTrue(window.getComputedStyle(span).backgroundImage.includes('2dppx'));
 
   done();
 }
@@ -115,10 +115,10 @@
 
   const span = getSpanFromIcon(icon);
   assertTrue(span.classList.contains('keep-color'));
-  assertTrue(window.getComputedStyle(span).backgroundImage.includes(
-      '-webkit-image-set'));
-  assertTrue(window.getComputedStyle(span).backgroundImage.includes('1x'));
-  assertTrue(window.getComputedStyle(span).backgroundImage.includes('2x'));
+  assertTrue(
+      window.getComputedStyle(span).backgroundImage.includes('image-set'));
+  assertTrue(window.getComputedStyle(span).backgroundImage.includes('1dppx'));
+  assertTrue(window.getComputedStyle(span).backgroundImage.includes('2dppx'));
 
   done();
 }
diff --git a/ui/views/controls/menu/menu_host_root_view.cc b/ui/views/controls/menu/menu_host_root_view.cc
index b5172aa2..4f9c3ea9 100644
--- a/ui/views/controls/menu/menu_host_root_view.cc
+++ b/ui/views/controls/menu/menu_host_root_view.cc
@@ -74,11 +74,8 @@
   return RootView::GetTooltipHandlerForPoint(point);
 }
 
-void MenuHostRootView::OnEventProcessingFinished(
-    ui::Event* event,
-    ui::EventTarget* target,
-    const ui::EventDispatchDetails& details) {
-  RootView::OnEventProcessingFinished(event, target, details);
+void MenuHostRootView::OnEventProcessingFinished(ui::Event* event) {
+  RootView::OnEventProcessingFinished(event);
 
   // Forward unhandled gesture events to our menu controller.
   // TODO(tdanderson): Investigate whether this should be moved into a
diff --git a/ui/views/controls/menu/menu_host_root_view.h b/ui/views/controls/menu/menu_host_root_view.h
index 3c63954b4..87b5c81 100644
--- a/ui/views/controls/menu/menu_host_root_view.h
+++ b/ui/views/controls/menu/menu_host_root_view.h
@@ -48,10 +48,7 @@
 
  private:
   // ui::EventProcessor:
-  void OnEventProcessingFinished(
-      ui::Event* event,
-      ui::EventTarget* target,
-      const ui::EventDispatchDetails& details) override;
+  void OnEventProcessingFinished(ui::Event* event) override;
 
   // Returns the MenuController for this MenuHostRootView.
   MenuController* GetMenuController();
diff --git a/ui/views/widget/root_view.cc b/ui/views/widget/root_view.cc
index 7025f7d..78b57f7 100644
--- a/ui/views/widget/root_view.cc
+++ b/ui/views/widget/root_view.cc
@@ -396,10 +396,7 @@
   gesture_handler_set_before_processing_ = !!gesture_handler_;
 }
 
-void RootView::OnEventProcessingFinished(
-    ui::Event* event,
-    ui::EventTarget* target,
-    const ui::EventDispatchDetails& details) {
+void RootView::OnEventProcessingFinished(ui::Event* event) {
   VLOG(5) << "RootView::OnEventProcessingFinished(" << event->ToString() << ")";
   // If |event| was not handled and |gesture_handler_| was not set by the
   // dispatch of a previous gesture event, then no default gesture handler
diff --git a/ui/views/widget/root_view.h b/ui/views/widget/root_view.h
index 228be4a..a786ee1 100644
--- a/ui/views/widget/root_view.h
+++ b/ui/views/widget/root_view.h
@@ -110,10 +110,7 @@
   ui::EventTarget* GetRootForEvent(ui::Event* event) override;
   ui::EventTargeter* GetDefaultEventTargeter() override;
   void OnEventProcessingStarted(ui::Event* event) override;
-  void OnEventProcessingFinished(
-      ui::Event* event,
-      ui::EventTarget* target,
-      const ui::EventDispatchDetails& details) override;
+  void OnEventProcessingFinished(ui::Event* event) override;
 
   // View:
   const Widget* GetWidget() const override;
diff --git a/ui/webui/resources/js/icon.ts b/ui/webui/resources/js/icon.ts
index 9758d6b..6c2a3b2 100644
--- a/ui/webui/resources/js/icon.ts
+++ b/ui/webui/resources/js/icon.ts
@@ -54,14 +54,14 @@
 }
 
 /**
- * Generates a CSS -webkit-image-set for a chrome:// url.
+ * Generates a CSS image-set for a chrome:// url.
  * An entry in the image set is added for each of getSupportedScaleFactors().
  * The scale-factor-specific url is generated by replacing the first instance
  * of 'scalefactor' in |path| with the numeric scale factor.
  *
  * @param path The URL to generate an image set for.
  *     'scalefactor' should be a substring of |path|.
- * @return The CSS -webkit-image-set.
+ * @return The CSS image-set.
  */
 function getImageSet(path: string): string {
   const supportedScaleFactors = getSupportedScaleFactors();
@@ -83,7 +83,7 @@
       s += ', ';
     }
   }
-  return '-webkit-image-set(' + s + ')';
+  return 'image-set(' + s + ')';
 }
 
 /**
@@ -109,10 +109,10 @@
 }
 
 /**
- * Creates a CSS -webkit-image-set for a favicon.
+ * Creates a CSS image-set for a favicon.
  *
  * @param url URL of the favicon
- * @return -webkit-image-set for the favicon
+ * @return image-set for the favicon
  */
 export function getFavicon(url: string): string {
   const faviconUrl = getBaseFaviconUrl();
@@ -121,7 +121,7 @@
 }
 
 /**
- * Creates a CSS -webkit-image-set for a favicon request based on a page URL.
+ * Creates a CSS image-set for a favicon request based on a page URL.
  *
  * @param url URL of the original page
  * @param isSyncedUrlForHistoryUi Should be set to true only if the
@@ -133,7 +133,7 @@
  * @param forceLightMode Flag to force the service to show the light
  *     mode version of the default favicon.
  *
- * @return -webkit-image-set for the favicon.
+ * @return image-set for the favicon.
  */
 export function getFaviconForPageURL(
     url: string, isSyncedUrlForHistoryUi: boolean,