diff --git a/DEPS b/DEPS
index 4951357..2fd2e6c6 100644
--- a/DEPS
+++ b/DEPS
@@ -297,7 +297,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': 'ce78d26f61f68080e2dbcf488217f23b0d8c9f38',
+  'skia_revision': '687aa00d6924b1b59e509f4e010ec635c45bcc21',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
@@ -305,7 +305,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': 'b47603e0448d6cedd5d56b8ca4a2658702fcdfb1',
+  'angle_revision': 'd0fe12db855d057ecca7fc1f0fa9e37f1d9a40f2',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -313,7 +313,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': '61d8ae861c7576a063422b5cbec9fdae21af58cd',
+  'pdfium_revision': 'd14da8e682e244127db32490365d1c094243e5f3',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling BoringSSL
   # and whatever else without interference from each other.
@@ -368,7 +368,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling catapult
   # and whatever else without interference from each other.
-  'catapult_revision': '4c50669a2f87912897af9227846fa5a419560616',
+  'catapult_revision': 'dd1a8cd9c4b12f9b40620c80d50675313d5e03c7',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libFuzzer
   # and whatever else without interference from each other.
@@ -376,7 +376,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': 'c756660b512ce494ce842fa1ff8eed2bef29b87e',
+  'devtools_frontend_revision': '8ec1e0e28e54f5deebf62c95315038a0ec415415',
   # 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.
@@ -777,7 +777,7 @@
   },
 
   'src/ios/third_party/earl_grey2/src': {
-      'url': Var('chromium_git') + '/external/github.com/google/EarlGrey.git' + '@' + '5bca387a75990c91c693995c87aa0dd1ec09237d',
+      'url': Var('chromium_git') + '/external/github.com/google/EarlGrey.git' + '@' + '53a2982c85ac6cf802719603d037ad3be7091ebb',
       'condition': 'checkout_ios',
   },
 
@@ -1473,7 +1473,7 @@
     Var('chromium_git') + '/webm/libwebm.git' + '@' + 'e4fbea0c9751ae8aa86629b197a28d8276a2b0da',
 
   'src/third_party/libwebp/src':
-    Var('chromium_git') + '/webm/libwebp.git' + '@' +  'a8e366166ab57bb1b4aaf6739fc775515bc71b51',
+    Var('chromium_git') + '/webm/libwebp.git' + '@' +  '3c4a0fbfbcc606193f7e943b7e50af4077ce1a6c',
 
   'src/third_party/libyuv':
     Var('chromium_git') + '/libyuv/libyuv.git' + '@' + 'd248929c059ff7629a85333699717d7a677d8d96',
@@ -1584,7 +1584,7 @@
   },
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' + 'c2235982559f7008e5384d5145e1febe571573cd',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' + '361efbf9aab595e4dfa79ec48f242d9e722393c9',
 
   'src/third_party/perl': {
       'url': Var('chromium_git') + '/chromium/deps/perl.git' + '@' + '6f3e5028eb65d0b4c5fdd792106ac4c84eee1eb3',
@@ -1751,7 +1751,7 @@
     Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + '44e4c8770158c505b03ee7feafa4859d083b0912',
 
   'src/third_party/webgpu-cts/src':
-    Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + '72e3a2249135b5d42d7a1c611af497565e9e6f02',
+    Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + 'c4eb1df3f306c0ee3e43ba2446eb3616e42d6855',
 
   'src/third_party/webrtc':
     Var('webrtc_git') + '/src.git' + '@' + 'dc5cf31cad576376abd3aa6306169453cfd85ba5',
@@ -1827,7 +1827,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@5159c6f0469e394c633fb8b22b2ad56354621518',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@733a35bf21b6aa2214623566eea6e2badaf23fd4',
     'condition': 'checkout_src_internal',
   },
 
diff --git a/android_webview/browser/enterprise_authentication_app_link_policy_handler.cc b/android_webview/browser/enterprise_authentication_app_link_policy_handler.cc
index 449d0f8..1157917 100644
--- a/android_webview/browser/enterprise_authentication_app_link_policy_handler.cc
+++ b/android_webview/browser/enterprise_authentication_app_link_policy_handler.cc
@@ -68,14 +68,17 @@
   if (!value)
     return;
 
-  std::vector<base::Value> filtered_values;
+  base::Value::List filtered_values;
   for (const auto& entry : value->GetList()) {
     const std::string* url = entry.FindStringKey("url");
     if (ValidatePolicyEntry(url))
-      filtered_values.emplace_back(*url);
+      filtered_values.Append(*url);
   }
-  if (filtered_values.size() > policy::kMaxUrlFiltersPerPolicy)
-    filtered_values.resize(policy::kMaxUrlFiltersPerPolicy);
+  if (filtered_values.size() > policy::kMaxUrlFiltersPerPolicy) {
+    filtered_values.erase(
+        filtered_values.begin() + policy::kMaxUrlFiltersPerPolicy,
+        filtered_values.end());
+  }
 
   prefs->SetValue(pref_path_, base::Value(std::move(filtered_values)));
 }
diff --git a/android_webview/nonembedded/java/src/org/chromium/android_webview/nonembedded/AwPureJavaExceptionReporter.java b/android_webview/nonembedded/java/src/org/chromium/android_webview/nonembedded/AwPureJavaExceptionReporter.java
index 8010b07..f6242cc 100644
--- a/android_webview/nonembedded/java/src/org/chromium/android_webview/nonembedded/AwPureJavaExceptionReporter.java
+++ b/android_webview/nonembedded/java/src/org/chromium/android_webview/nonembedded/AwPureJavaExceptionReporter.java
@@ -35,6 +35,11 @@
 
     @Override
     protected void uploadMinidump(File minidump) {
+        // The minidump file will only be ready for upload if PureJavaExceptionReporter attached
+        // logcat successfully, WebView should upload it even if attaching logcat was failed.
+        if (!CrashFileManager.isReadyUploadForFirstTime(minidump)) {
+            CrashFileManager.trySetReadyForUpload(minidump);
+        }
         CrashUploadUtil.scheduleNewJob(ContextUtils.getApplicationContext());
     }
 
diff --git a/ash/app_list/app_list_test_api.cc b/ash/app_list/app_list_test_api.cc
index bfb0599..18e019a 100644
--- a/ash/app_list/app_list_test_api.cc
+++ b/ash/app_list/app_list_test_api.cc
@@ -421,14 +421,24 @@
 
 std::u16string AppListTestApi::GetAppListItemViewName(
     const std::string& item_id) {
+  AppListItemView* item_view = GetTopLevelItemViewFromId(item_id);
+  if (!item_view)
+    return u"";
+
+  return item_view->title()->GetText();
+}
+
+AppListItemView* AppListTestApi::GetTopLevelItemViewFromId(
+    const std::string& item_id) {
   views::ViewModelT<AppListItemView>* view_model =
       GetTopLevelAppsGridView()->view_model();
   for (size_t i = 0; i < view_model->view_size(); ++i) {
     AppListItemView* app_list_item_view = view_model->view_at(i);
     if (app_list_item_view->item()->id() == item_id)
-      return app_list_item_view->title()->GetText();
+      return app_list_item_view;
   }
-  return u"";
+
+  return nullptr;
 }
 
 std::vector<std::string> AppListTestApi::GetTopLevelViewIdList() {
diff --git a/ash/app_list/model/app_list_model.cc b/ash/app_list/model/app_list_model.cc
index 417dda5..afe2af7 100644
--- a/ash/app_list/model/app_list_model.cc
+++ b/ash/app_list/model/app_list_model.cc
@@ -115,8 +115,6 @@
     // from chrome side is null. Do not alter `item` default icon in this case.
     data->icon = item->GetDefaultIcon();
     data->icon_color = item->GetDefaultIconColor();
-  } else if (data->icon_color != item->GetDefaultIconColor()) {
-    SetItemDefaultIconAndColor(item, data->icon, data->icon_color);
   }
 
   if (data->folder_id != item->folder_id())
@@ -403,13 +401,4 @@
     observer.OnAppListItemUpdated(item);
 }
 
-void AppListModel::SetItemDefaultIconAndColor(AppListItem* item,
-                                              const gfx::ImageSkia& icon,
-                                              const IconColor& icon_color) {
-  DCHECK(FindItem(item->id()));
-  item->SetDefaultIconAndColor(icon, icon_color);
-  for (auto& observer : observers_)
-    observer.OnAppListItemUpdated(item);
-}
-
 }  // namespace ash
diff --git a/ash/app_list/model/app_list_model.h b/ash/app_list/model/app_list_model.h
index ee5710f..4676607 100644
--- a/ash/app_list/model/app_list_model.h
+++ b/ash/app_list/model/app_list_model.h
@@ -180,11 +180,6 @@
   void SetRootItemPosition(AppListItem* item,
                            const syncer::StringOrdinal& new_position);
 
-  // Sets the default icon and the icon's associated color data.
-  void SetItemDefaultIconAndColor(AppListItem* item,
-                                  const gfx::ImageSkia& icon,
-                                  const IconColor& icon_color);
-
   // Used to initiate updates on app list items from the ash side.
   AppListModelDelegate* const delegate_;
 
diff --git a/ash/app_list/views/app_list_item_view.h b/ash/app_list/views/app_list_item_view.h
index 03ec7ea..072da16 100644
--- a/ash/app_list/views/app_list_item_view.h
+++ b/ash/app_list/views/app_list_item_view.h
@@ -227,6 +227,7 @@
 
   bool IsNotificationIndicatorShownForTest() const;
   GridDelegate* grid_delegate_for_test() { return grid_delegate_; }
+  const gfx::ImageSkia& icon_image_for_test() const { return icon_image_; }
 
   AppListMenuModelAdapter* item_menu_model_adapter() const {
     return item_menu_model_adapter_.get();
diff --git a/ash/app_list/views/apps_grid_view.cc b/ash/app_list/views/apps_grid_view.cc
index d70baf8..7cbafd3 100644
--- a/ash/app_list/views/apps_grid_view.cc
+++ b/ash/app_list/views/apps_grid_view.cc
@@ -331,6 +331,7 @@
   MaybeAbortWholeGridAnimation();
 
   view_model_.Clear();
+  pulsing_blocks_model_.Clear();
   RemoveAllChildViews();
 
   // `OnBoundsAnimatorDone`, which uses `bounds_animator_`, is called on
diff --git a/ash/app_list/views/apps_grid_view_unittest.cc b/ash/app_list/views/apps_grid_view_unittest.cc
index e5740d74..38bc648 100644
--- a/ash/app_list/views/apps_grid_view_unittest.cc
+++ b/ash/app_list/views/apps_grid_view_unittest.cc
@@ -5779,6 +5779,87 @@
   EXPECT_EQ(0u, GetPulsingBlocksModel().view_size());
 }
 
+// Tests that the pulsing blocks animation runs with the productivity launcher.
+TEST_P(AppsGridViewClamshellAndTabletTest,
+       PulsingBlocksAnimationOnFiringAnimationTimer) {
+  model_->PopulateApps(3);
+  UpdateLayout();
+  EXPECT_EQ(0u, GetPulsingBlocksModel().view_size());
+
+  ui::ScopedAnimationDurationScaleMode non_zero_duration_mode(
+      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
+
+  // For scrolling app list, the "page size" is very large, so cap the number of
+  // pulsing blocks to the size of the tablet mode page (~20 items).
+  const size_t tiles_per_page =
+      SharedAppListConfig::instance().GetMaxNumOfItemsPerPage();
+  model_->SetStatus(AppListModelStatus::kStatusSyncing);
+  UpdateLayout();
+  ASSERT_EQ(tiles_per_page - 3, GetPulsingBlocksModel().view_size());
+
+  PulsingBlockView* pulsing_block_view = GetPulsingBlocksModel().view_at(0);
+
+  EXPECT_FALSE(pulsing_block_view->IsAnimating());
+  EXPECT_TRUE(pulsing_block_view->FireAnimationTimerForTest());
+  EXPECT_TRUE(pulsing_block_view->IsAnimating());
+
+  // Set the model status as normal to avoid the test hanging due to the
+  // pulsing blocks animation.
+  model_->SetStatus(AppListModelStatus::kStatusNormal);
+  EXPECT_EQ(0u, GetPulsingBlocksModel().view_size());
+}
+
+// Verify that as new app items get synced into the app list, newer items slowly
+// fade in place of a placeholder.
+TEST_F(AppsGridViewBubbleTest, AppIconSubtitutesPulsingBlockView) {
+  model_->PopulateApps(3);
+  UpdateLayout();
+  EXPECT_EQ(0u, GetPulsingBlocksModel().view_size());
+
+  ui::ScopedAnimationDurationScaleMode non_zero_duration_mode(
+      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
+
+  // For scrolling app list, the "page size" is very large, so cap the number of
+  // pulsing blocks to the size of the tablet mode page (~20 items).
+  const size_t tiles_per_page =
+      SharedAppListConfig::instance().GetMaxNumOfItemsPerPage();
+  model_->SetStatus(AppListModelStatus::kStatusSyncing);
+  UpdateLayout();
+  ASSERT_EQ(tiles_per_page - 3, GetPulsingBlocksModel().view_size());
+
+  PulsingBlockView* pulsing_block_view = GetPulsingBlocksModel().view_at(0);
+
+  EXPECT_TRUE(pulsing_block_view->FireAnimationTimerForTest());
+  EXPECT_TRUE(pulsing_block_view->IsAnimating());
+
+  gfx::Rect placeholder_bounds = pulsing_block_view->GetBoundsInScreen();
+
+  // Add another app to simulate a synced app.
+  model_->PopulateApps(1);
+  UpdateLayout();
+
+  // The number of pulsing blocks will be decreased by one in order for the
+  // incoming app to fade in its place.
+  ASSERT_EQ(tiles_per_page - 4, GetPulsingBlocksModel().view_size());
+
+  AppListItemView* item_view = GetItemViewInTopLevelGrid(3);
+
+  ASSERT_TRUE(item_view->layer());
+  EXPECT_TRUE(item_view->layer()->GetAnimator()->is_animating());
+  EXPECT_EQ(1.0f, item_view->layer()->GetTargetOpacity());
+
+  LayerAnimationStoppedWaiter animation_waiter;
+  animation_waiter.Wait(item_view->layer());
+
+  // The new item should be placed at the first placeholder bounds.
+  EXPECT_EQ(placeholder_bounds, item_view->GetBoundsInScreen());
+
+  // Set the model status as normal to avoid the test hanging due to the
+  // pulsing blocks animation.
+  model_->SetStatus(AppListModelStatus::kStatusNormal);
+  EXPECT_EQ(0u, GetPulsingBlocksModel().view_size());
+}
+
 // Tests that right clicking an app will remove focus from other apps within the
 // apps grid. See https://crbug.com/1146365.
 TEST_P(AppsGridViewClamshellTest, VerifyFocusRemovedWhenLeftClickingOtherApp) {
diff --git a/ash/app_list/views/pulsing_block_view.cc b/ash/app_list/views/pulsing_block_view.cc
index f349cd1..410807d 100644
--- a/ash/app_list/views/pulsing_block_view.cc
+++ b/ash/app_list/views/pulsing_block_view.cc
@@ -169,4 +169,22 @@
   canvas->FillRect(rect, kBlockColor);
 }
 
+bool PulsingBlockView::IsAnimating() {
+  views::View* animating_view =
+      ash::features::IsLauncherPulsingBlocksRefreshEnabled()
+          ? background_color_view_
+          : this;
+  return animating_view->layer()
+             ? animating_view->layer()->GetAnimator()->is_animating()
+             : false;
+}
+
+bool PulsingBlockView::FireAnimationTimerForTest() {
+  if (!start_delay_timer_.IsRunning())
+    return false;
+
+  start_delay_timer_.FireNow();
+  return true;
+}
+
 }  // namespace ash
diff --git a/ash/app_list/views/pulsing_block_view.h b/ash/app_list/views/pulsing_block_view.h
index c257bce..1a94a0a 100644
--- a/ash/app_list/views/pulsing_block_view.h
+++ b/ash/app_list/views/pulsing_block_view.h
@@ -15,7 +15,7 @@
 
 namespace ash {
 
-// PulsingBlockView shows a pulsing white block via layer animation.
+// PulsingBlockView shows a pulsing white circle via layer animation.
 class PulsingBlockView : public views::View {
  public:
   // Constructs a PulsingBlockView of |size|. Starts the pulsing animation after
@@ -31,8 +31,13 @@
   const char* GetClassName() const override;
   void OnThemeChanged() override;
 
-  // Schedules the animation again from the beginning.
-  void ResetAnimation();
+  // Returns true if the view has a layer animator attached and is currently
+  // running.
+  bool IsAnimating();
+
+  // Starts the animation by immediately firing `start_delay_timer`. Returns
+  // false if the timer was not running.
+  bool FireAnimationTimerForTest();
 
  private:
   void OnStartDelayTimer();
diff --git a/ash/clipboard/clipboard_history_controller_impl.cc b/ash/clipboard/clipboard_history_controller_impl.cc
index 2015244..46213447 100644
--- a/ash/clipboard/clipboard_history_controller_impl.cc
+++ b/ash/clipboard/clipboard_history_controller_impl.cc
@@ -664,6 +664,15 @@
       confirmed_paste_count = 0;
     }
 
+    if (copy) {
+      // Record copy actions once they are confirmed, rather than when clipboard
+      // data first changes, to allow multiple data changes to be debounced into
+      // a single copy operation. This ensures that each user-initiated copy is
+      // recorded only once. See `ClipboardHistory::OnDataChanged()` for further
+      // explanation.
+      base::RecordAction(base::UserMetricsAction("Ash_Clipboard_CopiedItem"));
+    }
+
     // Verify that this operation did not interleave with a clipboard history
     // paste.
     DCHECK_EQ(pastes_to_be_confirmed_, 0);
diff --git a/ash/constants/ash_features.cc b/ash/constants/ash_features.cc
index 2695514..0776995 100644
--- a/ash/constants/ash_features.cc
+++ b/ash/constants/ash_features.cc
@@ -1388,7 +1388,7 @@
 // Enables Shelf Palm Rejection in tablet mode by defining a pixel offset for
 // the swipe gesture to show the extended hotseat. Limited to certain apps.
 const base::Feature kShelfPalmRejectionSwipeOffset{
-    "ShelfPalmRejectionSwipeOffset", base::FEATURE_DISABLED_BY_DEFAULT};
+    "ShelfPalmRejectionSwipeOffset", base::FEATURE_ENABLED_BY_DEFAULT};
 
 // Enables or disables the new shimless rma flow.
 const base::Feature kShimlessRMAFlow{"ShimlessRMAFlow",
diff --git a/ash/constants/notifier_catalogs.h b/ash/constants/notifier_catalogs.h
index 9afa184..fc1f9d6 100644
--- a/ash/constants/notifier_catalogs.h
+++ b/ash/constants/notifier_catalogs.h
@@ -159,7 +159,10 @@
   kTPMAutoUpdatePlanned = 145,
   kTPMAutoUpdateOnReboot = 146,
   kPrivacyIndicators = 147,
-  kMaxValue = kPrivacyIndicators
+  kTailoredSecurityDisabled = 148,
+  kTailoredSecurityEnabled = 149,
+  kTailoredSecurityPromotion = 150,
+  kMaxValue = kTailoredSecurityPromotion
 };
 
 // A living catalog that registers toasts.
diff --git a/ash/public/cpp/test/app_list_test_api.h b/ash/public/cpp/test/app_list_test_api.h
index 1e96efb..8771c04 100644
--- a/ash/public/cpp/test/app_list_test_api.h
+++ b/ash/public/cpp/test/app_list_test_api.h
@@ -61,6 +61,9 @@
   // Returns the name displayed in the launcher for the provided app list item.
   std::u16string GetAppListItemViewName(const std::string& item_id);
 
+  // Returns the top level item view specified by `item_id`.
+  AppListItemView* GetTopLevelItemViewFromId(const std::string& item_id);
+
   // Returns ids of the items in top level app list view.
   std::vector<std::string> GetTopLevelViewIdList();
 
diff --git a/ash/shelf/shelf_view.cc b/ash/shelf/shelf_view.cc
index 35ac465..95ee31a 100644
--- a/ash/shelf/shelf_view.cc
+++ b/ash/shelf/shelf_view.cc
@@ -455,22 +455,6 @@
   // We'll layout when our bounds change.
 }
 
-gfx::Rect ShelfView::GetIdealBoundsOfItemIcon(const ShelfID& id) {
-  int index = model_->ItemIndexByID(id);
-  if (index < 0 ||
-      !base::Contains(visible_views_indices_, static_cast<size_t>(index)))
-    return gfx::Rect();
-
-  const gfx::Rect& ideal_bounds(
-      view_model_->ideal_bounds(static_cast<size_t>(index)));
-  ShelfAppButton* button = GetShelfAppButton(id);
-  gfx::Rect icon_bounds = button->GetIconBounds();
-  return gfx::Rect(GetMirroredXWithWidthInView(
-                       ideal_bounds.x() + icon_bounds.x(), icon_bounds.width()),
-                   ideal_bounds.y() + icon_bounds.y(), icon_bounds.width(),
-                   icon_bounds.height());
-}
-
 bool ShelfView::IsShowingMenu() const {
   return shelf_menu_model_adapter_ &&
          shelf_menu_model_adapter_->IsShowingMenu();
diff --git a/ash/shelf/shelf_view.h b/ash/shelf/shelf_view.h
index 1ab8121..24e8eaba 100644
--- a/ash/shelf/shelf_view.h
+++ b/ash/shelf/shelf_view.h
@@ -128,11 +128,6 @@
   // Initializes shelf view elements.
   void Init();
 
-  // Returns the ideal bounds of the specified item, or an empty rect if id
-  // isn't know. If the item is in an overflow shelf, the overflow icon location
-  // will be returned.
-  gfx::Rect GetIdealBoundsOfItemIcon(const ShelfID& id);
-
   // Returns true if we're showing a menu. Note the menu could be either the
   // context menu or the application select menu.
   bool IsShowingMenu() const;
diff --git a/ash/system/cast/tray_cast.h b/ash/system/cast/tray_cast.h
index 7230a0c5..c9509f0 100644
--- a/ash/system/cast/tray_cast.h
+++ b/ash/system/cast/tray_cast.h
@@ -32,6 +32,10 @@
   // views::View:
   const char* GetClassName() const override;
 
+  views::View* get_add_access_code_device_for_testing() {
+    return add_access_code_device_;
+  }
+
  private:
   void CreateItems();
 
diff --git a/ash/system/cast/unified_cast_detailed_view_controller.h b/ash/system/cast/unified_cast_detailed_view_controller.h
index 2e13b976..6f94fc0 100644
--- a/ash/system/cast/unified_cast_detailed_view_controller.h
+++ b/ash/system/cast/unified_cast_detailed_view_controller.h
@@ -31,6 +31,8 @@
   views::View* CreateView() override;
   std::u16string GetAccessibleName() const override;
 
+  CastDetailedView* get_cast_detailed_view_for_testing() { return view_; }
+
  private:
   const std::unique_ptr<DetailedViewDelegate> detailed_view_delegate_;
 
diff --git a/ash/system/time/calendar_event_list_view_unittest.cc b/ash/system/time/calendar_event_list_view_unittest.cc
index 5f75ba8..8e73305 100644
--- a/ash/system/time/calendar_event_list_view_unittest.cc
+++ b/ash/system/time/calendar_event_list_view_unittest.cc
@@ -74,8 +74,9 @@
   void CreateEventListView(base::Time date) {
     event_list_view_.reset();
     controller_->UpdateMonth(date);
-    Shell::Get()->system_tray_model()->calendar_model()->InsertEventsForTesting(
-        CreateMockEventList().get());
+    Shell::Get()->system_tray_model()->calendar_model()->OnEventsFetched(
+        calendar_utils::GetStartOfMonthUTC(date),
+        google_apis::ApiErrorCode::HTTP_SUCCESS, CreateMockEventList().get());
     controller_->selected_date_ = date;
     event_list_view_ =
         std::make_unique<CalendarEventListView>(controller_.get());
diff --git a/ash/system/time/calendar_model.cc b/ash/system/time/calendar_model.cc
index 24b9b109..b69accc2 100644
--- a/ash/system/time/calendar_model.cc
+++ b/ash/system/time/calendar_model.cc
@@ -421,41 +421,6 @@
   return GetEndTimeAdjusted(event).UTCMidnight();
 }
 
-// TODO(crbug/1330004): Remove function in favor of calling `OnEventsFetched`
-// directly from tests.
-void CalendarModel::InsertEventsForTesting(
-    const google_apis::calendar::EventList* events) {
-  if (!events)
-    return;
-
-  // Make sure the cache is empty.
-  event_months_.clear();
-
-  // Insert, and collect the set of months inserted.
-  std::set<base::Time> months_inserted;
-  for (const auto& event : events->items()) {
-    base::Time start_time_midnight = GetStartTimeMidnightAdjusted(event.get());
-    base::Time start_of_month =
-        calendar_utils::GetStartOfMonthUTC(start_time_midnight);
-    if (IsMultiDayEvent(event.get())) {
-      const base::Time end_time_midnight =
-          GetEndTimeMidnightAdjusted(event.get());
-      // If we have multi-day events, we insert the event to every month map the
-      // event exists in. This to reproduce Google Calendar API requests, which
-      // are fetched by month, so multi-day events get inserted for every month
-      // they are active.
-      while (end_time_midnight >= start_of_month) {
-        InsertMultiDayEvent(event.get(), start_of_month);
-        start_of_month = calendar_utils::GetStartOfNextMonthUTC(start_of_month);
-        months_inserted.emplace(start_of_month);
-      }
-    } else {
-      InsertEventInMonth(event.get(), start_of_month, start_time_midnight);
-      months_inserted.emplace(start_of_month);
-    }
-  }
-}
-
 SingleDayEventList CalendarModel::FindEvents(base::Time day) const {
   SingleDayEventList event_list;
 
diff --git a/ash/system/time/calendar_model.h b/ash/system/time/calendar_model.h
index ec25587..97f2880 100644
--- a/ash/system/time/calendar_model.h
+++ b/ash/system/time/calendar_model.h
@@ -169,10 +169,6 @@
   base::Time GetEndTimeMidnightAdjusted(
       const google_apis::calendar::CalendarEvent* event) const;
 
-  // Inserts EventList `events` in the EventCache. For testing only, it clears
-  // out the entire cache and inserts the `events`.
-  void InsertEventsForTesting(const google_apis::calendar::EventList* events);
-
   // Frees up months of events as needed to keep us within storage limits.
   void PruneEventCache();
 
diff --git a/ash/system/time/calendar_month_view_unittest.cc b/ash/system/time/calendar_month_view_unittest.cc
index 79e37df..93ea7ff 100644
--- a/ash/system/time/calendar_month_view_unittest.cc
+++ b/ash/system/time/calendar_month_view_unittest.cc
@@ -82,9 +82,9 @@
     calendar_month_view_->Layout();
   }
 
-  void UploadEvents() {
-    calendar_month_view_->calendar_model_->InsertEventsForTesting(
-        CreateMockEventList().get());
+  void MockFetchEvents(base::Time start_of_month) {
+    calendar_month_view_->calendar_model_->InsertPendingFetchesForTesting(
+        start_of_month);
   }
 
   void TriggerPaint() {
@@ -107,9 +107,10 @@
   }
 
   void NotifyObservers(base::Time start_of_month) {
-    for (auto& observer : calendar_month_view_->calendar_model_->observers_)
-      observer.OnEventsFetched(CalendarModel::kSuccess, start_of_month,
-                               nullptr);
+    calendar_month_view_->calendar_model_->PruneEventCache();
+    calendar_month_view_->calendar_model_->OnEventsFetched(
+        start_of_month, google_apis::ApiErrorCode::HTTP_SUCCESS,
+        CreateMockEventList().get());
   }
 
   CalendarModel::MonthToEventsMap event_months() {
@@ -319,7 +320,7 @@
 
   CreateMonthView(date, u"America/Los_Angeles");
   // Used to fetch events and notify observers.
-  base::Time date_midnight = date.UTCMidnight();
+  base::Time month_start_midnight = calendar_utils::GetStartOfMonthUTC(today);
 
   TriggerPaint();
   // Grayed out cell. Sep 2nd is the 33 one in this calendar, which is with
@@ -338,7 +339,7 @@
             static_cast<CalendarDateCellView*>(month_view()->children()[17])
                 ->GetTooltipText());
 
-  InsertPendingFetches(date_midnight);
+  InsertPendingFetches(month_start_midnight);
   // Grayed out cell. Sep 2nd is the 33 one in this calendar, which is with
   // index 32.
   EXPECT_EQ(u"2",
@@ -357,8 +358,8 @@
 
   // After events are fetched before the observers are notified the event number
   // is not updated.
-  DeletePendingFetches(date_midnight);
-  UploadEvents();
+  DeletePendingFetches(month_start_midnight);
+  MockFetchEvents(month_start_midnight);
   // Grayed out cell. Sep 2nd is the 33 one in this calendar, which is with
   // index 32.
   EXPECT_EQ(u"2",
@@ -377,7 +378,7 @@
 
   // After notifying observers and repainting, the event numbers are updated for
   // regular cells, not for grayed out cells.
-  NotifyObservers(date_midnight);
+  NotifyObservers(month_start_midnight);
   EXPECT_EQ(u"2",
             static_cast<CalendarDateCellView*>(month_view()->children()[32])
                 ->GetText());
@@ -394,10 +395,6 @@
 }
 
 TEST_F(CalendarMonthViewTest, TimeZone) {
-  // Create a monthview based on Aug,1st 2021. Today is set to 18th.
-  base::Time date;
-  ASSERT_TRUE(base::Time::FromString("1 Aug 2021 10:00 GMT", &date));
-
   // Set "Now" to a date that is in this month.
   base::Time today;
   ASSERT_TRUE(base::Time::FromString("18 Aug 2021 10:00 GMT", &today));
@@ -407,13 +404,13 @@
       /*thread_ticks_override=*/nullptr);
 
   // Used to fetch events and notify observers.
-  base::Time date_midnight = date.UTCMidnight();
+  base::Time month_start_midnight = calendar_utils::GetStartOfMonthUTC(today);
 
   // Sets the timezone to "America/Los_Angeles";
-  CreateMonthView(date, u"America/Los_Angeles");
+  CreateMonthView(today, u"America/Los_Angeles");
   TriggerPaint();
-  UploadEvents();
-  NotifyObservers(date_midnight);
+  MockFetchEvents(month_start_midnight);
+  NotifyObservers(month_start_midnight);
 
   EXPECT_EQ(u"18",
             static_cast<CalendarDateCellView*>(month_view()->children()[17])
@@ -443,10 +440,6 @@
 }
 
 TEST_F(CalendarMonthViewTest, InactiveUserSession) {
-  // Create a monthview based on Aug,1st 2021. Today is set to 18th.
-  base::Time date;
-  ASSERT_TRUE(base::Time::FromString("1 Aug 2021 10:00 GMT", &date));
-
   // Set "Now" to a date that is in this month.
   base::Time today;
   ASSERT_TRUE(base::Time::FromString("18 Aug 2021 10:00 GMT", &today));
@@ -455,13 +448,13 @@
       &CalendarMonthViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
       /*thread_ticks_override=*/nullptr);
 
-  CreateMonthView(date, u"America/Los_Angeles");
+  CreateMonthView(today, u"America/Los_Angeles");
   // Used to fetch events and notify observers.
-  base::Time date_midnight = date.UTCMidnight();
+  base::Time month_start_midnight = calendar_utils::GetStartOfMonthUTC(today);
 
   TriggerPaint();
-  UploadEvents();
-  NotifyObservers(date_midnight);
+  MockFetchEvents(month_start_midnight);
+  NotifyObservers(month_start_midnight);
   EXPECT_EQ(u"18",
             static_cast<CalendarDateCellView*>(month_view()->children()[17])
                 ->GetText());
diff --git a/ash/webui/os_feedback_ui/os_feedback_ui.cc b/ash/webui/os_feedback_ui/os_feedback_ui.cc
index a0b2d80b..fd5668c 100644
--- a/ash/webui/os_feedback_ui/os_feedback_ui.cc
+++ b/ash/webui/os_feedback_ui/os_feedback_ui.cc
@@ -58,6 +58,7 @@
       {"attachScreenshotLabel", IDS_FEEDBACK_TOOL_SCREENSHOT_LABEL},
       {"attachScreenshotCheckboxAriaLabel",
        IDS_FEEDBACK_TOOL_ATTACH_SCREENSHOT_CHECKBOX_ARIA_LABEL},
+      {"previewImageAriaLabel", IDS_FEEDBACK_TOOL_PREVIEW_IMAGE_ARIA_LABEL},
       {"addFileLabel", IDS_FEEDBACK_TOOL_ADD_FILE_LABEL},
       {"replaceFileLabel", IDS_FEEDBACK_TOOL_REPLACE_FILE_LABEL},
       {"userEmailLabel", IDS_FEEDBACK_TOOL_USER_EMAIL_LABEL},
diff --git a/ash/webui/os_feedback_ui/resources/file_attachment.html b/ash/webui/os_feedback_ui/resources/file_attachment.html
index b7bba435..5d1b03865 100644
--- a/ash/webui/os_feedback_ui/resources/file_attachment.html
+++ b/ash/webui/os_feedback_ui/resources/file_attachment.html
@@ -91,7 +91,7 @@
 <div id="replaceFileContainer" hidden="[[!hasSelectedAFile_]]">
   <input type="checkbox" id="selectFileCheckbox">
   <div id="replaceFileInfo">
-    <div id="selectedFileName" class="overflow-text"></div>
+    <div id="selectedFileName" class="overflow-text">[[selectedFileName_]]</div>
     <button id="replaceFileLabel" class="file-input"
         on-click="handleOpenFileInputClick_">
       [[i18n('replaceFileLabel')]]
@@ -106,14 +106,13 @@
   </iron-icon>
   <span id="errorMessage">[[i18n('fileTooBigErrorMessage')]]</span>
 </cr-toast>
-<!-- TODO(longbowei): Locolize strings in cr-dialog -->
 <cr-dialog id="selectedImageDialog">
   <div id="modalDialogTitle" slot="title">
     <cr-icon-button id="closeDialogButton" class="icon-arrow-back"
         on-click="handleSelectedImageDialogCloseClick_"
-        title="Image">
+        title="[[selectedFileName_]]">
     </cr-icon-button>
-    <span>Image</span>
+    <span id="modalDialogTitleText">[[selectedFileName_]]</span>
   </div>
   <div slot="body">
     <img src="[[selectedImageUrl_]]" class="image-preview">
diff --git a/ash/webui/os_feedback_ui/resources/file_attachment.js b/ash/webui/os_feedback_ui/resources/file_attachment.js
index b754640..8ae6fbe 100644
--- a/ash/webui/os_feedback_ui/resources/file_attachment.js
+++ b/ash/webui/os_feedback_ui/resources/file_attachment.js
@@ -62,6 +62,13 @@
     this.selectedFile_ = null;
 
     /**
+     * The name of the file selected
+     * @type {string}
+     * @protected
+     */
+    this.selectedFileName_;
+
+    /**
      * Url of the selected image.
      * @type {string}
      */
@@ -123,6 +130,7 @@
    * Get the image url when uploaded file is image type.
    * @param {!File} file
    * @return {!Promise<string>}
+   * @private
    */
   async getImageUrl_(file) {
     const fileDataBuffer = await file.arrayBuffer();
@@ -179,16 +187,19 @@
       return;
     }
     this.selectedFile_ = file;
-    this.getElement_('#selectedFileName').textContent = file.name;
+    this.selectedFileName_ = file.name;
     this.getElement_('#selectFileCheckbox').checked = true;
 
     // Add a preview image when selected file is image type.
     if (file.type.startsWith('image/')) {
       this.getImageUrl_(file).then((imageUrl) => {
         this.selectedImageUrl_ = imageUrl;
+        this.$.selectedImageButton.ariaLabel =
+            this.i18n('previewImageAriaLabel', file.name);
       });
     } else {
       this.selectedImageUrl_ = '';
+      this.$.selectedImageButton.ariaLabel = '';
     }
   }
 
diff --git a/ash/webui/os_feedback_ui/resources/share_data_page.html b/ash/webui/os_feedback_ui/resources/share_data_page.html
index d963101f..f3d621c9 100644
--- a/ash/webui/os_feedback_ui/resources/share_data_page.html
+++ b/ash/webui/os_feedback_ui/resources/share_data_page.html
@@ -157,8 +157,7 @@
           </cr-checkbox>
           <label id="screenshotCheckLabel">[[i18n('attachScreenshotLabel')]]</label>
           <button id="imageButton" on-click="handleScreenshotClick_">
-            <img id="screenshotImage" aria-label="$i18n{screenshotA11y}"
-              src="[[screenshotUrl]]">
+            <img id="screenshotImage" src="[[screenshotUrl]]">
            </button>
         </div>
         <!-- Attach a file -->
diff --git a/ash/webui/os_feedback_ui/resources/share_data_page.js b/ash/webui/os_feedback_ui/resources/share_data_page.js
index b2e3a57..4351964 100644
--- a/ash/webui/os_feedback_ui/resources/share_data_page.js
+++ b/ash/webui/os_feedback_ui/resources/share_data_page.js
@@ -82,6 +82,8 @@
     this.setSysInfoCheckboxLabelAndAttributes_();
     this.$.screenshotCheckbox.ariaLabel =
         this.i18n('attachScreenshotCheckboxAriaLabel');
+    this.$.imageButton.ariaLabel = this.i18n(
+        'previewImageAriaLabel', this.$.screenshotCheckLabel.textContent);
 
     // Set up event listener for email change to retarget |this| to be the
     // ShareDataPageElement's context.
diff --git a/base/BUILD.gn b/base/BUILD.gn
index 7eddfb10..9fc667ea 100644
--- a/base/BUILD.gn
+++ b/base/BUILD.gn
@@ -1431,10 +1431,7 @@
 
   configs += [
     "//build/config:precompiled_headers",
-
-    # TODO(crbug.com/1292951): Enable.
-    # "//build/config/compiler:prevent_unsafe_narrowing",
-
+    "//build/config/compiler:prevent_unsafe_narrowing",
     "//build/config/compiler:wexit_time_destructors",
     "//build/config/compiler:wglobal_constructors",
   ]
diff --git a/base/allocator/partition_allocator/page_allocator_constants.h b/base/allocator/partition_allocator/page_allocator_constants.h
index 7de7a4c..751c2b3a 100644
--- a/base/allocator/partition_allocator/page_allocator_constants.h
+++ b/base/allocator/partition_allocator/page_allocator_constants.h
@@ -42,8 +42,8 @@
 // Use PageAllocationGranularity(), PageAllocationGranularityShift()
 // to initialize and retrieve these values safely.
 struct PageCharacteristics {
-  std::atomic<int> size;
-  std::atomic<int> shift;
+  std::atomic<size_t> size;
+  std::atomic<size_t> shift;
 };
 extern PageCharacteristics page_characteristics;
 
@@ -82,9 +82,10 @@
 #elif BUILDFLAG(IS_LINUX) && defined(ARCH_CPU_ARM64)
   // arm64 supports 4kb (shift = 12), 16kb (shift = 14), and 64kb (shift = 16)
   // page sizes. Retrieve from or initialize cache.
-  int shift = page_characteristics.shift.load(std::memory_order_relaxed);
+  size_t shift = page_characteristics.shift.load(std::memory_order_relaxed);
   if (PA_UNLIKELY(shift == 0)) {
-    shift = __builtin_ctz((int)PageAllocationGranularity());
+    shift = static_cast<size_t>(
+        __builtin_ctz((unsigned int)PageAllocationGranularity()));
     page_characteristics.shift.store(shift, std::memory_order_relaxed);
   }
   return shift;
@@ -102,9 +103,9 @@
 #elif BUILDFLAG(IS_LINUX) && defined(ARCH_CPU_ARM64)
   // arm64 supports 4kb, 16kb, and 64kb page sizes. Retrieve from or
   // initialize cache.
-  int size = page_characteristics.size.load(std::memory_order_relaxed);
+  size_t size = page_characteristics.size.load(std::memory_order_relaxed);
   if (PA_UNLIKELY(size == 0)) {
-    size = getpagesize();
+    size = static_cast<size_t>(getpagesize());
     page_characteristics.size.store(size, std::memory_order_relaxed);
   }
   return size;
diff --git a/base/files/file_util.h b/base/files/file_util.h
index 79c46ab..d4a3d0d 100644
--- a/base/files/file_util.h
+++ b/base/files/file_util.h
@@ -372,8 +372,7 @@
 
 // Creates a directory, as well as creating any parent directories, if they
 // don't exist. Returns 'true' on successful creation, or if the directory
-// already exists. On most systems, the created directories are only readable by
-// the current user. But on ChromeOS, they are also world-traversable.
+// already exists.  The directory is only readable by the current user.
 // Returns true on success, leaving *error unchanged.
 // Returns false on failure and sets *error appropriately, if it is non-NULL.
 BASE_EXPORT bool CreateDirectoryAndGetError(const FilePath& full_path,
diff --git a/base/files/file_util_posix.cc b/base/files/file_util_posix.cc
index d13cd46..7c44e03 100644
--- a/base/files/file_util_posix.cc
+++ b/base/files/file_util_posix.cc
@@ -715,19 +715,7 @@
   for (const FilePath& subpath : base::Reversed(subpaths)) {
     if (DirectoryExists(subpath))
       continue;
-
-#if BUILDFLAG(IS_CHROMEOS)
-    // On ChromeOS, create directories that are traversable by group and other
-    // (rwx--x--x). This is consistent with the creation of files with mode
-    // (rw-r--r--) on ChromeOS. See also File::DoInitialize() in
-    // base/files/file_posix.cc and DoCopyDirectory() in
-    // base/files/file_util_posix.cc.
-    static constexpr mode_t kMode = S_IRWXU | S_IXGRP | S_IXOTH;  // rwx --x --x
-#else
-    static constexpr mode_t kMode = S_IRWXU;  // rwx --- ---
-#endif
-
-    if (mkdir(subpath.value().c_str(), kMode) == 0)
+    if (mkdir(subpath.value().c_str(), 0700) == 0)
       continue;
     // Mkdir failed, but it might have failed with EEXIST, or some other error
     // due to the directory appearing out of thin air. This can occur if
diff --git a/base/values.cc b/base/values.cc
index 950291bc..83f941d 100644
--- a/base/values.cc
+++ b/base/values.cc
@@ -1776,26 +1776,6 @@
 
 ListValue::ListValue() : Value(Type::LIST) {}
 
-bool ListValue::GetDictionary(size_t index,
-                              const DictionaryValue** out_value) const {
-  const auto& list = GetListDeprecated();
-  if (list.size() <= index)
-    return false;
-  const base::Value& value = list[index];
-  if (!value.is_dict())
-    return false;
-
-  if (out_value)
-    *out_value = static_cast<const DictionaryValue*>(&value);
-
-  return true;
-}
-
-bool ListValue::GetDictionary(size_t index, DictionaryValue** out_value) {
-  return as_const(*this).GetDictionary(
-      index, const_cast<const DictionaryValue**>(out_value));
-}
-
 void ListValue::Append(base::Value::Dict in_dict) {
   list().emplace_back(std::move(in_dict));
 }
diff --git a/base/values.h b/base/values.h
index 73b91b4..e52f0d9 100644
--- a/base/values.h
+++ b/base/values.h
@@ -1416,15 +1416,6 @@
 
   ListValue();
 
-  // Convenience forms of `Get()`.  Modifies `out_value` (and returns true)
-  // only if the index is valid and the Value at that index can be returned
-  // in the specified form.
-  // `out_value` is optional and will only be set if non-NULL.
-  //
-  // DEPRECATED: prefer `Value::List::operator[]` + `GetIfDict()`.
-  bool GetDictionary(size_t index, const DictionaryValue** out_value) const;
-  bool GetDictionary(size_t index, DictionaryValue** out_value);
-
   // Appends a Value to the end of the list.
   // DEPRECATED: prefer `Value::List::Append()`.
   using Value::Append;
diff --git a/base/values_unittest.cc b/base/values_unittest.cc
index e158ee8..2ed5fcd 100644
--- a/base/values_unittest.cc
+++ b/base/values_unittest.cc
@@ -2394,15 +2394,6 @@
   EXPECT_FALSE(main_dict.GetListWithoutPathExpansion("dict", nullptr));
   EXPECT_TRUE(main_dict.GetListWithoutPathExpansion("list", nullptr));
   EXPECT_FALSE(main_dict.GetListWithoutPathExpansion("DNE", nullptr));
-
-  EXPECT_FALSE(main_list.GetDictionary(0, nullptr));
-  EXPECT_FALSE(main_list.GetDictionary(1, nullptr));
-  EXPECT_FALSE(main_list.GetDictionary(2, nullptr));
-  EXPECT_FALSE(main_list.GetDictionary(3, nullptr));
-  EXPECT_FALSE(main_list.GetDictionary(4, nullptr));
-  EXPECT_TRUE(main_list.GetDictionary(5, nullptr));
-  EXPECT_FALSE(main_list.GetDictionary(6, nullptr));
-  EXPECT_FALSE(main_list.GetDictionary(7, nullptr));
 }
 
 TEST(ValuesTest, SelfSwap) {
diff --git a/build/android/test/incremental_javac_gn/incremental_javac_test_android_library.py b/build/android/test/incremental_javac_gn/incremental_javac_test_android_library.py
index e0f7e1f..c84cff0d 100755
--- a/build/android/test/incremental_javac_gn/incremental_javac_test_android_library.py
+++ b/build/android/test/incremental_javac_gn/incremental_javac_test_android_library.py
@@ -116,9 +116,8 @@
       # GOMA does not work with non-standard output directories.
       'use_goma = false',
   ]
-  _copy_and_append_gn_args(
-      options.gn_args_path, out_gn_args_path,
-      extra_gn_args + ['incremental_javac_test_toggle_gn = false'])
+  _copy_and_append_gn_args(options.gn_args_path, out_gn_args_path,
+                           extra_gn_args)
 
   _run_gn([
       '--root-target=' + options.target_name, 'gen',
@@ -133,19 +132,19 @@
   ninja_args = [_NINJA_PATH, '-C', options.out_dir, gn_path]
   ninja_output = _run_command(ninja_args, env=ninja_env)
   if _USING_PARTIAL_JAVAC_MSG in ninja_output:
-    raise Exception('Incorrectly using partial javac for clean compile.')
+    raise Exception("Incorrectly using partial javac for clean compile.")
 
   _copy_and_append_gn_args(
       options.gn_args_path, out_gn_args_path,
       extra_gn_args + ['incremental_javac_test_toggle_gn = true'])
   ninja_output = _run_command(ninja_args, env=ninja_env)
   if _USING_PARTIAL_JAVAC_MSG not in ninja_output:
-    raise Exception('Not using partial javac for incremental compile.')
+    raise Exception("Not using partial javac for incremental compile.")
 
-  expected_output_path = '{}/obj/{}.javac.jar'.format(options.out_dir,
-                                                      gn_path.replace(':', '/'))
+  expected_output_path = "{}/lib.java/{}.jar".format(options.out_dir,
+                                                     gn_path.replace(':', '/'))
   if not os.path.exists(expected_output_path):
-    raise Exception('{} not created.'.format(expected_output_path))
+    raise Exception("{} not created.".format(expected_output_path))
 
   shutil.copyfile(expected_output_path, options.out_jar)
 
diff --git a/build/config/android/internal_rules.gni b/build/config/android/internal_rules.gni
index 8aca7d2..12be71c 100644
--- a/build/config/android/internal_rules.gni
+++ b/build/config/android/internal_rules.gni
@@ -20,9 +20,6 @@
 }
 assert(is_android)
 
-default_android_sdk_dep = "//third_party/android_sdk:android_sdk_java"
-_jacoco_dep = "//third_party/jacoco:jacocoagent_java"
-
 # The following _java_*_types variables capture all the existing target types.
 # If a new type is introduced, please add it to one of these categories,
 # preferring the more specific resource/library types.
@@ -118,8 +115,9 @@
   _target_label =
       get_label_info(":${_parent_invoker.target_name}", "label_no_toolchain")
 
-  # Ensure targets match naming patterns so that __assetres, __header, __host,
-  # and __validate targets work properly.
+  # Ensure targets match naming patterns so that __assetres, __header, __impl
+  # targets work properly. Those generated targets allow for effective deps
+  # filtering.
   if (filter_exclude([ _type ], _java_resource_types) == []) {
     if (filter_exclude([ _target_label ], java_resource_patterns) != []) {
       assert(false, "Invalid java resource target name: $_target_label")
@@ -164,6 +162,8 @@
       foreach(_possible_dep, invoker.possible_config_deps) {
         _dep_label = get_label_info(_possible_dep, "label_no_toolchain")
         if (filter_exclude([ _dep_label ], java_target_patterns) == []) {
+          # Put the bug number in the target name so that false-positives
+          # have a hint in the error message about non-existent dependencies.
           deps += [ "$_dep_label$build_config_target_suffix" ]
           _dep_gen_dir = get_label_info(_possible_dep, "target_gen_dir")
           _dep_name = get_label_info(_possible_dep, "name")
@@ -1162,7 +1162,12 @@
   }
 
   template("proguard") {
-    forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)
+    forward_variables_from(invoker,
+                           TESTONLY_AND_VISIBILITY + [
+                                 "data",
+                                 "data_deps",
+                                 "public_deps",
+                               ])
     _script = "//build/android/gyp/proguard.py"
     _deps = invoker.deps
 
@@ -1364,12 +1369,6 @@
       _deps += [ ":$_expectations_target" ]
     }
     action_with_pydeps(target_name) {
-      forward_variables_from(invoker,
-                             [
-                               "data",
-                               "data_deps",
-                               "public_deps",
-                             ])
       script = _script
       deps = _deps
       inputs = _inputs
@@ -1598,9 +1597,6 @@
           _r8_path,
           _custom_d8_path,
         ]
-        if (defined(invoker.inputs)) {
-          inputs += invoker.inputs
-        }
 
         if (!_is_library) {
           # http://crbug.com/725224. Fix for bots running out of memory.
@@ -1893,7 +1889,7 @@
 
   template("bytecode_processor") {
     action_with_pydeps(target_name) {
-      forward_variables_from(invoker, TESTONLY_AND_VISIBILITY + [ "data_deps" ])
+      forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)
       _bytecode_checker_script = "$root_build_dir/bin/helper/bytecode_processor"
       script = "//build/android/gyp/bytecode_processor.py"
       inputs = [
@@ -2989,7 +2985,7 @@
 
       if (target_name == "chrome_java__header") {
         # Regression test for: https://crbug.com/1154302
-        assert_no_deps = [ "//base:base_java__compile_java" ]
+        assert_no_deps = [ "//base:base_java__impl" ]
       }
 
       depfile = "$target_gen_dir/$target_name.d"
@@ -3130,6 +3126,28 @@
     }
   }
 
+  template("java_lib_group") {
+    forward_variables_from(invoker, [ "testonly" ])
+    _group_name = invoker.group_name
+    not_needed([ "_group_name" ])
+    group(target_name) {
+      if (defined(invoker.deps)) {
+        deps = []
+        foreach(_dep, invoker.deps) {
+          _target_label = get_label_info(_dep, "label_no_toolchain")
+          if (filter_exclude([ _target_label ], java_library_patterns) == [] &&
+              filter_exclude([ _target_label ], java_resource_patterns) != []) {
+            # This is a java library dep, so replace it.
+            deps += [ "${_target_label}__${_group_name}" ]
+          } else {
+            # Transitive java group targets should also include direct deps.
+            deps += [ _dep ]
+          }
+        }
+      }
+    }
+  }
+
   # Create an interface jar from a normal jar.
   #
   # Variables
@@ -3301,16 +3319,10 @@
     _type = invoker.type
     _is_annotation_processor = _type == "java_annotation_processor"
     _is_java_binary = _type == "java_binary" || _type == "robolectric_binary"
-    _is_library = _type == "java_library"
     _supports_android =
         defined(invoker.supports_android) && invoker.supports_android
     _requires_android =
         defined(invoker.requires_android) && invoker.requires_android
-    _supports_host = !_requires_android
-    if (_is_java_binary || _is_annotation_processor) {
-      assert(!_requires_android && !_supports_android)
-    }
-
     _bypass_platform_checks = defined(invoker.bypass_platform_checks) &&
                               invoker.bypass_platform_checks
     _is_robolectric = defined(invoker.is_robolectric) && invoker.is_robolectric
@@ -3399,9 +3411,6 @@
         _jacoco_instrument =
             !invoker.jacoco_never_instrument && _jacoco_instrument
       }
-      if (_jacoco_instrument) {
-        _invoker_deps += [ _jacoco_dep ]
-      }
 
       if (_build_host_jar) {
         # Jar files can be needed at runtime (by Robolectric tests or java binaries),
@@ -3459,83 +3468,77 @@
       }
     }
 
-    _java_assetres_deps = filter_include(_invoker_deps, java_resource_patterns)
-
-    # Cannot use minus operator because it does not work when the operand has
-    # repeated entries.
-    _invoker_deps_minus_assetres =
-        filter_exclude(_invoker_deps, _java_assetres_deps)
-    _lib_deps =
-        filter_include(_invoker_deps_minus_assetres, java_library_patterns)
-    _non_java_deps = filter_exclude(_invoker_deps_minus_assetres, _lib_deps)
-
-    _java_header_deps = []  # Turbine / ijar
-
-    # It would be more ideal to split this into __host and __javac, but we
-    # combine the two concepts to save on a group() target.
-    _java_host_deps = []  # Processed host .jar + javac .jar.
-    _java_validate_deps = []  # Bytecode checker & errorprone.
-
-    foreach(_lib_dep, _lib_deps) {
-      # Expand //foo/java -> //foo/java:java
-      _lib_dep = get_label_info(_lib_dep, "label_no_toolchain")
-      _java_assetres_deps += [ "${_lib_dep}__assetres" ]
-      _java_header_deps += [ "${_lib_dep}__header" ]
-      _java_host_deps += [ "${_lib_dep}__host" ]
-      _java_validate_deps += [ "${_lib_dep}__validate" ]
-    }
-
-    # APK and base module targets are special because:
-    # 1) They do not follow java target naming scheme (since they are not
-    #    generally deps, there is no need for them to).
-    # 2) They do not bother to define a __host target.
-    # Since __host is used as an indirect dep for the compile_java artifacts,
-    # add the __compile_java target directly for them.
-    if (defined(invoker.apk_under_test)) {
-      _java_assetres_deps += [ "${invoker.apk_under_test}__java__assetres" ]
-      _java_header_deps += [ "${invoker.apk_under_test}__java__header" ]
-      _java_validate_deps += [ "${invoker.apk_under_test}__java__validate" ]
-      _java_host_deps += [ "${invoker.apk_under_test}__compile_java" ]
-    }
-    if (defined(invoker.base_module_target)) {
-      _java_assetres_deps += [ "${invoker.base_module_target}__java__assetres" ]
-      _java_header_deps += [ "${invoker.base_module_target}__java__header" ]
-      _java_validate_deps += [ "${invoker.base_module_target}__java__validate" ]
-      _java_host_deps += [ "${invoker.base_module_target}__compile_java" ]
-    }
-
-    not_needed([ "_non_java_deps" ])
-
     if (_is_prebuilt || _has_sources) {
-      # Classpath deps are used for header and dex targets, they do not need
-      # __assetres deps.
-      # _non_java_deps are needed for input_jars_paths that are generated.
-      _header_classpath_deps =
-          _java_header_deps + _non_java_deps + [ ":$_build_config_target_name" ]
+      _java_assetres_deps =
+          filter_include(_invoker_deps, java_resource_patterns)
 
-      _javac_classpath_deps =
-          _java_host_deps + _non_java_deps + [ ":$_build_config_target_name" ]
+      # Cannot use minus operator because it does not work when the operand has
+      # repeated entries.
+      _invoker_deps_minus_assetres =
+          filter_exclude(_invoker_deps, _java_assetres_deps)
+      _lib_deps =
+          filter_include(_invoker_deps_minus_assetres, java_library_patterns)
+      _non_java_deps = filter_exclude(_invoker_deps_minus_assetres, _lib_deps)
+
+      _java_header_deps = []
+      _java_impl_deps = []
+      foreach(_lib_dep, _lib_deps) {
+        # Expand //foo/java -> //foo/java:java
+        _lib_dep = get_label_info(_lib_dep, "label_no_toolchain")
+
+        # This is a java library dep, so it has header and impl targets.
+        _java_header_deps += [ "${_lib_dep}__header" ]
+        _java_impl_deps += [ "${_lib_dep}__impl" ]
+        _java_assetres_deps += [ "${_lib_dep}__assetres" ]
+      }
+
+      # Don't need to depend on the apk-under-test to be packaged.
+      if (defined(invoker.apk_under_test)) {
+        _java_header_deps += [ "${invoker.apk_under_test}__java__header" ]
+        _java_impl_deps += [ "${invoker.apk_under_test}__java__impl" ]
+      }
+
+      # These deps cannot be passed via invoker.deps since bundle_module targets
+      # have bundle_module.build_config without the __java suffix, so they are
+      # special and cannot be passed as regular deps to write_build_config.
+      if (defined(invoker.base_module_target)) {
+        _java_header_deps += [ "${invoker.base_module_target}__java__header" ]
+        _java_impl_deps += [ "${invoker.base_module_target}__java__impl" ]
+      }
+
+      _extra_java_deps = []
+      if (_jacoco_instrument) {
+        _extra_java_deps += [ "//third_party/jacoco:jacocoagent_java" ]
+      }
 
       _include_android_sdk = _build_device_jar
       if (defined(invoker.include_android_sdk)) {
         _include_android_sdk = invoker.include_android_sdk
       }
       if (_include_android_sdk) {
+        _sdk_java_dep = "//third_party/android_sdk:android_sdk_java"
         if (defined(invoker.alternative_android_sdk_dep)) {
-          _android_sdk_dep = invoker.alternative_android_sdk_dep
-        } else {
-          _android_sdk_dep = default_android_sdk_dep
+          _sdk_java_dep = invoker.alternative_android_sdk_dep
         }
 
-        _header_classpath_deps += [ "${_android_sdk_dep}__header" ]
-        _javac_classpath_deps += [ "${_android_sdk_dep}" ]
+        # This is an android_system_java_prebuilt target, so no headers.
+        _extra_java_deps += [ _sdk_java_dep ]
       }
+
+      # Classpath deps is used for header and dex targets, they do not need
+      # resource deps.
+      _classpath_deps = _java_header_deps + _non_java_deps + _extra_java_deps +
+                        [ ":$_build_config_target_name" ]
+
+      _full_classpath_deps =
+          _java_impl_deps + _java_assetres_deps + _non_java_deps +
+          _extra_java_deps + [ ":$_build_config_target_name" ]
     }
 
     # Often needed, but too hard to figure out when ahead of time.
     not_needed([
-                 "_header_classpath_deps",
-                 "_javac_classpath_deps",
+                 "_classpath_deps",
+                 "_full_classpath_deps",
                ])
 
     if (_java_files != []) {
@@ -3619,15 +3622,12 @@
       if (defined(invoker.public_deps)) {
         possible_config_public_deps = invoker.public_deps
       }
+      if (defined(_extra_java_deps)) {
+        possible_config_deps += _extra_java_deps
+      }
       if (defined(apk_under_test)) {
         possible_config_deps += [ apk_under_test ]
       }
-      if (defined(_jacoco_instrument) && _jacoco_instrument) {
-        possible_config_deps += [ _jacoco_dep ]
-      }
-      if (defined(_android_sdk_dep)) {
-        possible_config_deps += [ _android_sdk_dep ]
-      }
 
       supports_android = _supports_android
       requires_android = _requires_android
@@ -3674,6 +3674,9 @@
       _header_target_name = "${target_name}__header"
     }
 
+    _public_deps = []
+    _analysis_public_deps = []
+
     if (_has_sources) {
       if (defined(invoker.enable_errorprone)) {
         _enable_errorprone = invoker.enable_errorprone
@@ -3690,9 +3693,10 @@
                    "android_library(), or robolectric_library(). " +
                    "Target=$target_name")
 
-        # Serves double purpose: Generating R.java, as well as being the
-        #__assetres target (instead of using a separate group).
-        _fake_rjava_target = "${target_name}__assetres"
+        # has _resources at the end so it looks like a resources pattern, since
+        # it does act like one (and other resources patterns need to depend on
+        # this before they can read its output R.txt).
+        _fake_rjava_target = "${target_name}__rjava_resources"
         generate_r_java(_fake_rjava_target) {
           deps = [ ":$_build_config_target_name" ] + _java_assetres_deps +
                  _non_java_deps
@@ -3719,17 +3723,10 @@
           # Filtering out generated files resulted in no files left.
           group(target_name) {
             not_needed(invoker, "*")
-            deps = _header_classpath_deps
           }
         } else {
           compile_java(target_name) {
-            forward_variables_from(invoker,
-                                   "*",
-                                   TESTONLY_AND_VISIBILITY + [ "deps" ])
-            deps = _header_classpath_deps
-            if (defined(invoker.deps)) {
-              deps += invoker.deps
-            }
+            forward_variables_from(invoker, "*", TESTONLY_AND_VISIBILITY)
             output_jar_path = invoker.output_jar_path
             enable_errorprone = _enable_errorprone
             use_turbine = defined(invoker.use_turbine) && invoker.use_turbine
@@ -3750,6 +3747,10 @@
             chromium_code = _chromium_code
             supports_android = _supports_android
             include_android_sdk = _is_robolectric || _requires_android
+            if (!defined(deps)) {
+              deps = []
+            }
+            deps += _classpath_deps
           }
         }
       }
@@ -3773,6 +3774,7 @@
         generated_jar_path = _generated_jar_path
         deps = _annotation_processor_deps
       }
+      _public_deps += [ ":$_header_target_name" ]
 
       _compile_java_target = "${_main_target_name}__compile_java"
       compile_java_helper(_compile_java_target) {
@@ -3798,16 +3800,14 @@
           generated_jar_path = _generated_jar_path
           output_jar_path = "$target_out_dir/$target_name.errorprone.stamp"
         }
-        _java_validate_deps += [ ":$_compile_java_errorprone_target" ]
+        _analysis_public_deps += [ ":$_compile_java_errorprone_target" ]
       }
     }  # _has_sources
 
     if (_is_prebuilt || _build_device_jar || _build_host_jar) {
+      _unprocessed_jar_deps = []
       if (_has_sources) {
-        _unprocessed_jar_deps = [ ":$_compile_java_target" ]
-      } else {
-        # jars might be generated by a dep.
-        _unprocessed_jar_deps = _non_java_deps
+        _unprocessed_jar_deps += [ ":$_compile_java_target" ]
       }
     }
 
@@ -3843,7 +3843,7 @@
           "--output-jar",
           rebase_path(_rewritten_jar, root_build_dir),
         ]
-        deps = _unprocessed_jar_deps + _javac_classpath_deps +
+        deps = _unprocessed_jar_deps + _full_classpath_deps +
                [ invoker.bytecode_rewriter_target ]
       }
 
@@ -3860,10 +3860,14 @@
         input_jar = _unprocessed_jar_path
         output_jar = _final_ijar_path
 
-        # ijar needs only _unprocessed_jar_deps, but this also needs to export
-        # __header target from deps.
-        deps = _unprocessed_jar_deps + _java_header_deps
+        # Normally ijar does not require any deps, but:
+        # 1 - Some jars are bytecode rewritten by _unprocessed_jar_deps.
+        # 2 - Other jars need to be unzipped by _non_java_deps.
+        # 3 - It is expected that depending on a header target implies depending
+        #     on its transitive header target deps via _java_header_deps.
+        deps = _unprocessed_jar_deps + _non_java_deps + _java_header_deps
       }
+      _public_deps += [ ":$_header_target_name" ]
     }
 
     if (_build_host_jar || _build_device_jar) {
@@ -3871,15 +3875,11 @@
           (!defined(invoker.enable_bytecode_checks) ||
            invoker.enable_bytecode_checks) && android_static_analysis != "off"
       if (_enable_bytecode_checks) {
-        _validate_target_name = "${target_name}__validate"
-        bytecode_processor(_validate_target_name) {
+        _bytecode_checks_target = "${target_name}__validate_classpath"
+        bytecode_processor(_bytecode_checks_target) {
           forward_variables_from(invoker, [ "missing_classes_allowlist" ])
-          deps = _unprocessed_jar_deps + _javac_classpath_deps +
+          deps = _unprocessed_jar_deps + _full_classpath_deps +
                  [ ":$_build_config_target_name" ]
-          data_deps = _java_validate_deps
-          if (defined(_compile_java_errorprone_target)) {
-            data_deps += [ ":$_compile_java_errorprone_target" ]
-          }
 
           include_android_sdk = _requires_android || _is_robolectric
           target_label =
@@ -3888,12 +3888,13 @@
           build_config = _build_config
           is_prebuilt = _is_prebuilt
         }
+        _analysis_public_deps += [ ":$_bytecode_checks_target" ]
       } else {
         not_needed(invoker, [ "missing_classes_allowlist" ])
       }
 
       if (_build_host_jar) {
-        _process_host_jar_target_name = "${target_name}__host"
+        _process_host_jar_target_name = "${target_name}__process_host"
         process_java_library(_process_host_jar_target_name) {
           forward_variables_from(invoker,
                                  [
@@ -3904,20 +3905,15 @@
           # Robolectric tests require these to be on swarming.
           data = [ _host_processed_jar_path ]
           input_jar_path = _unprocessed_jar_path
-          deps = _unprocessed_jar_deps + _javac_classpath_deps
+          deps = _unprocessed_jar_deps + _full_classpath_deps
           output_jar_path = _host_processed_jar_path
           jacoco_instrument = _jacoco_instrument
           if (_jacoco_instrument) {
             java_files = _java_files
             java_sources_file = _java_sources_file
           }
-
-          # _java_host_deps isn't necessary for process_java_library(), but is
-          # necessary so that this target can be used to depend on transitive
-          # __device targets without the need to create a separate group()
-          # target. This trade-off works because process_java_library is fast.
-          deps += _java_host_deps
         }
+        _public_deps += [ ":${_process_host_jar_target_name}" ]
       }
 
       if (_build_device_jar) {
@@ -3930,8 +3926,7 @@
                                      "jar_included_patterns",
                                    ])
             input_jar_path = _unprocessed_jar_path
-
-            deps = _unprocessed_jar_deps + _javac_classpath_deps
+            deps = _unprocessed_jar_deps + _full_classpath_deps
             output_jar_path = _device_processed_jar_path
             jacoco_instrument = _jacoco_instrument
             if (_jacoco_instrument) {
@@ -3940,13 +3935,14 @@
             }
           }
           _process_device_jar_deps = [ ":${_process_device_jar_target_name}" ]
+          _public_deps += _process_device_jar_deps
         } else {
           assert(_unprocessed_jar_path == _device_processed_jar_path)
-          _process_device_jar_deps = _unprocessed_jar_deps
+          _process_device_jar_deps =
+              _unprocessed_jar_deps + _full_classpath_deps
         }
 
-        _dex_target_name = "${target_name}__dex"
-        dex(_dex_target_name) {
+        dex("${target_name}__dex") {
           forward_variables_from(invoker,
                                  [
                                    "desugar_jars_paths",
@@ -3966,24 +3962,16 @@
             # Desugaring with D8 requires full classpath.
             build_config = _build_config
             final_ijar_path = _final_ijar_path
-            deps += _header_classpath_deps + [ ":$_header_target_name" ]
+            deps += _classpath_deps + [
+                      ":$_build_config_target_name",
+                      ":$_header_target_name",
+                    ]
           }
 
           enable_multidex = false
           is_library = true
-
-          # proguard_configs listed on java_library targets need to be marked
-          # as inputs to at least one target so that "gn analyze" will know
-          # about them. Although this target doesn't use them, it's a convenient spot
-          # to list them.
-          # https://crbug.com/827197
-          if (compute_inputs_for_analyze && defined(invoker.proguard_configs)) {
-            inputs = invoker.proguard_configs
-
-            # For the aapt-generated proguard rules.
-            deps += _non_java_deps + _srcjar_deps
-          }
         }
+        _public_deps += [ ":${target_name}__dex" ]
       }
     }
 
@@ -4008,100 +3996,57 @@
           extra_classpath_jars = [ _robolectric_jar_path ]
         }
       }
+      _public_deps += [ ":$_java_binary_script_target_name" ]
     }
 
-    if (!defined(_validate_target_name)) {
-      _validate_target_name = "${target_name}__validate"
+    # The __impl target contains all non-analysis steps for this template.
+    # Having this separated out from the main target (which contains analysis
+    # steps) allows analysis steps for this target to be run concurrently with
+    # the non-analysis steps of other targets that depend on this one.
+    group("${target_name}__impl") {
+      public_deps = _public_deps
 
-      # Allow other targets to depend on this __validate one.
-      group(_validate_target_name) {
-        deps = _java_validate_deps
+      # proguard_configs listed on java_library targets need to be marked
+      # as inputs to at least one target so that "gn analyze" will know
+      # about them. Although this target doesn't use them, it's a convenient spot
+      # to list them.
+      # https://crbug.com/827197
+      if (compute_inputs_for_analyze && defined(invoker.proguard_configs)) {
+        assert(_build_host_jar || _build_device_jar)
+        data = invoker.proguard_configs
+
+        # For the aapt-generated proguard rules.
+        deps = _non_java_deps + _srcjar_deps
       }
     }
 
-    if (_supports_host && !defined(_process_host_jar_target_name)) {
-      group("${target_name}__host") {
-        deps = _java_host_deps
+    java_lib_group("${target_name}__assetres") {
+      deps = _invoker_deps
+      group_name = "assetres"
+
+      if (defined(_fake_rjava_target)) {
+        deps += [ ":$_fake_rjava_target" ]
       }
     }
 
-    # robolectric_library can depend on java_library, so java_library must
-    # define __assetres.
-    if ((_is_library || _supports_android || _is_robolectric) &&
-        !defined(_fake_rjava_target)) {
-      group("${target_name}__assetres") {
-        if (_supports_android || _is_robolectric) {
-          deps = _java_assetres_deps
-        }
-      }
-    }
-
-    # The top-level group is used:
-    # 1) To allow building the target explicitly via ninja,
-    # 2) To trigger all analysis deps,
-    # 3) By custom action() targets that want to use artifacts as inputs.
     group(target_name) {
       forward_variables_from(invoker,
                              [
                                "assert_no_deps",
                                "data",
                                "data_deps",
+                               "deps",
+                               "public_deps",
                                "visibility",
                              ])
-      if (_requires_android || (_supports_android && _is_library)) {
-        # For non-robolectric targets, depend on other java target's top-level
-        # groups so that the __dex step gets depended on.
-        forward_variables_from(invoker,
-                               [
-                                 "deps",
-                                 "public_deps",
-                               ])
-        if (!defined(deps)) {
-          deps = []
-        }
-        if (!defined(public_deps)) {
-          public_deps = []
-        }
-      } else {
-        # For robolectric targets, depend only on non-java deps and the specific
-        # subtargets below, which will not include __dex.
-        deps = _non_java_deps
+      if (!defined(public_deps)) {
         public_deps = []
-        if (defined(invoker.public_deps)) {
-          public_deps +=
-              filter_exclude(invoker.public_deps, java_target_patterns)
-        }
       }
-      if (defined(_jacoco_instrument) && _jacoco_instrument) {
-        deps += [ _jacoco_dep ]
-      }
-      if (defined(_process_device_jar_target_name)) {
-        public_deps += [ ":$_process_device_jar_target_name" ]
-      }
-      if (defined(_dex_target_name)) {
-        public_deps += [ ":$_dex_target_name" ]
-      }
-      if (_supports_android && _is_library) {
-        # Robolectric targets define __assetres, but there's no need to build it
-        # by default.
-        public_deps += [ ":${target_name}__assetres" ]
-      }
-      if (_supports_host) {
-        # android_* targets define __host, but there's no need to build it by
-        # default.
-        public_deps += [ ":${target_name}__host" ]
-      }
-      if (_is_java_binary) {
-        public_deps += [ ":$_java_binary_script_target_name" ]
-      }
+      public_deps += [ ":${target_name}__impl" ]
       if (!defined(data_deps)) {
         data_deps = []
       }
-      if (defined(_validate_target_name)) {
-        data_deps += [ ":$_validate_target_name" ]
-      } else {
-        data_deps += _java_validate_deps
-      }
+      data_deps += _analysis_public_deps
     }
   }
 }
diff --git a/build/config/android/rules.gni b/build/config/android/rules.gni
index 486735c9..15d99775 100644
--- a/build/config/android/rules.gni
+++ b/build/config/android/rules.gni
@@ -1031,9 +1031,9 @@
     }
 
     if (defined(invoker.alternative_android_sdk_dep)) {
-      _android_sdk_dep = invoker.alternative_android_sdk_dep
+      _deps += [ invoker.alternative_android_sdk_dep ]
     } else {
-      _android_sdk_dep = default_android_sdk_dep
+      _deps += [ "//third_party/android_sdk:android_sdk_java" ]
     }
 
     _resource_files = []
@@ -1072,7 +1072,7 @@
                              ])
 
       r_text = _r_text_out_path
-      possible_config_deps = _deps + [ _android_sdk_dep ]
+      possible_config_deps = _deps
 
       # Always merge manifests from resources.
       # * Might want to change this at some point for consistency and clarity,
@@ -1103,7 +1103,7 @@
       }
 
       # Depend on non-library deps and on __assetres subtargets of library deps.
-      deps = filter_exclude(_deps, _lib_deps) + [ _android_sdk_dep ]
+      deps = filter_exclude(_deps, _lib_deps)
       foreach(_lib_dep, _lib_deps) {
         # Expand //foo/java -> //foo/java:java
         _lib_dep = get_label_info(_lib_dep, "label_no_toolchain")
@@ -1237,34 +1237,17 @@
       supports_android = true
       possible_config_deps = _invoker_deps
     }
-
-    _assetres_deps = filter_include(_invoker_deps, java_resource_patterns)
-    _invoker_deps_minus_assetres = filter_exclude(_invoker_deps, _assetres_deps)
-    _lib_deps =
-        filter_include(_invoker_deps_minus_assetres, java_library_patterns)
-
-    _expanded_lib_deps = []
-    foreach(_lib_dep, _lib_deps) {
-      _expanded_lib_deps += [ get_label_info(_lib_dep, "label_no_toolchain") ]
-    }
     foreach(_group_name,
             [
-              "assetres",
               "header",
-              "host",
-              "validate",
+              "impl",
+              "assetres",
             ]) {
-      group("${target_name}__$_group_name") {
-        deps = []
-        foreach(_lib_dep, _expanded_lib_deps) {
-          deps += [ "${_lib_dep}__${_group_name}" ]
-        }
-        if (_group_name == "assetres") {
-          deps += _assetres_deps
-        }
+      java_lib_group("${target_name}__${_group_name}") {
+        deps = _invoker_deps
+        group_name = _group_name
       }
     }
-
     group(target_name) {
       forward_variables_from(invoker,
                              "*",
@@ -1380,7 +1363,7 @@
     if (defined(invoker.alternative_android_sdk_dep)) {
       _android_sdk_dep = invoker.alternative_android_sdk_dep
     } else {
-      _android_sdk_dep = default_android_sdk_dep
+      _android_sdk_dep = "//third_party/android_sdk:android_sdk_java"
     }
 
     # A package name or a manifest is required to have resources. This is
@@ -1510,13 +1493,8 @@
       deps = [
         ":$_apkbuilder_target_name",
         ":$_build_config_target_name",
-        ":${_java_binary_target_name}__host",
-        ":${_java_binary_target_name}__java_binary_script",
-        ":${_java_binary_target_name}__validate",
+        ":$_java_binary_target_name",
       ]
-
-      # Add non-libary deps, since the __host target does not depend on them.
-      deps += filter_exclude(_invoker_deps, java_library_patterns)
     }
   }
 
@@ -1760,7 +1738,7 @@
   #     output = "$root_build_dir/MyLibrary.jar"
   #   }
   template("dist_dex") {
-    _deps = [ default_android_sdk_dep ]
+    _deps = [ "//third_party/android_sdk:android_sdk_java" ]
     if (defined(invoker.deps)) {
       _deps += invoker.deps
     }
@@ -1781,6 +1759,8 @@
       build_config = _build_config
     }
 
+    _deps += [ ":$_build_config_target_name" ]
+
     dex(target_name) {
       forward_variables_from(invoker,
                              TESTONLY_AND_VISIBILITY + [
@@ -1791,7 +1771,7 @@
                                    "proguard_enable_obfuscation",
                                    "min_sdk_version",
                                  ])
-      deps = [ ":$_build_config_target_name" ] + _deps
+      deps = _deps
       build_config = _build_config
       enable_multidex = false
       output = invoker.output
@@ -2615,7 +2595,7 @@
       }
     }
 
-    _final_deps = [ ":$_java_target_name" ]
+    _final_deps = []
 
     _enable_main_dex_list = _enable_multidex && _min_sdk_version < 21
     if (_enable_main_dex_list) {
@@ -2627,7 +2607,7 @@
     if (defined(invoker.alternative_android_sdk_dep)) {
       _android_sdk_dep = invoker.alternative_android_sdk_dep
     } else {
-      _android_sdk_dep = default_android_sdk_dep
+      _android_sdk_dep = "//third_party/android_sdk:android_sdk_java"
     }
 
     if (defined(_shared_resources_allowlist_target)) {
@@ -3088,8 +3068,6 @@
         output = _dist_ijar_path
         data = [ _dist_ijar_path ]
         use_interface_jars = true
-
-        # This will use __header under-the-hood.
         deps = [ ":$_java_target_name" ]
       }
     }
@@ -3097,7 +3075,7 @@
     if (_uses_static_library_synchronized_proguard) {
       _final_dex_target_dep = "${invoker.static_library_provider}__dexsplitter"
     } else if ((_is_bundle_module && _proguard_enabled) || _omit_dex) {
-      # No library dep needed.
+      _final_deps += [ ":$_java_target_name" ]
     } else if (_incremental_apk) {
       if (defined(invoker.enable_proguard_checks)) {
         not_needed(invoker, [ "enable_proguard_checks" ])
@@ -3120,24 +3098,18 @@
           ":$_java_target_name",
         ]
         if (_proguard_enabled) {
-          # Generates proguard configs
-          deps += [ ":$_compile_resources_target" ]
+          deps += _invoker_deps + [ ":$_compile_resources_target" ]
           proguard_mapping_path = _proguard_mapping_path
           proguard_sourcefile_suffix = "$android_channel-$_version_code"
           has_apk_under_test = defined(invoker.apk_under_test)
+        } else if (_min_sdk_version >= default_min_sdk_version) {
+          # Enable dex merging only when min_sdk_version is >= what the library
+          # .dex files were created with.
+          input_dex_filearg =
+              "@FileArg(${_rebased_build_config}:final_dex:all_dex_files)"
         } else {
-          if (_min_sdk_version >= default_min_sdk_version) {
-            # Enable dex merging only when min_sdk_version is >= what the library
-            # .dex files were created with.
-            input_dex_filearg =
-                "@FileArg(${_rebased_build_config}:final_dex:all_dex_files)"
-
-            # Pure dex-merge.
-            enable_desugar = false
-          } else {
-            input_classes_filearg =
-                "@FileArg($_rebased_build_config:deps_info:device_classpath)"
-          }
+          input_classes_filearg =
+              "@FileArg($_rebased_build_config:deps_info:device_classpath)"
         }
 
         if (_is_static_library_provider) {
@@ -3159,14 +3131,11 @@
         # their respective dex steps. False positives that were suppressed at
         # per-target dex steps are emitted here since this may use jar files
         # rather than dex files.
-        if (!defined(enable_desugar)) {
-          ignore_desugar_missing_deps = true
-        }
+        ignore_desugar_missing_deps = true
 
         if (_enable_main_dex_list) {
-          # Generates main-dex config.
-          deps += [ ":$_compile_resources_target" ]
           extra_main_dex_proguard_config = _generated_proguard_main_dex_config
+          deps += [ ":$_compile_resources_target" ]
         }
       }
 
@@ -3303,11 +3272,11 @@
             name = "${invoker.name}.apk"
             build_config = _build_config
             res_size_info_path = _res_size_info_path
-            deps = [
-              ":$_build_config_target",
-              ":$_compile_resources_target",
-              ":$_java_target_name",
-            ]
+            deps = _invoker_deps + [
+                     ":$_build_config_target",
+                     ":$_compile_resources_target",
+                     ":$_java_target_name",
+                   ]
           }
           _final_deps += [ ":$_size_info_target" ]
         } else {
@@ -3465,7 +3434,10 @@
           args += [ "--native-libs=$_rebased_loadable_modules" ]
         }
       }
-      _final_deps += [ ":$_write_installer_json_rule_name" ]
+      _final_deps += [
+        ":$_java_target_name",
+        ":$_write_installer_json_rule_name",
+      ]
     }
 
     # Generate apk operation related script.
@@ -3552,8 +3524,6 @@
                                ])
         build_config = _build_config
         build_config_dep = ":$_build_config_target"
-
-        # This will use library subtargets under-the-hood
         deps = [ ":$_java_target_name" ]
         if (defined(invoker.lint_suppressions_dep)) {
           deps += [ invoker.lint_suppressions_dep ]
@@ -3590,7 +3560,8 @@
       }
 
       # Include unstripped native libraries so tests can symbolize stacks.
-      data_deps += _all_native_libs_deps + [ ":${_java_target_name}__validate" ]
+      data_deps += _all_native_libs_deps
+
       if (_enable_lint) {
         data_deps += [ ":${target_name}__lint" ]
       }
@@ -4978,9 +4949,9 @@
     _rebased_build_config = rebase_path(_build_config, root_build_dir)
     _build_config_target = "$_target_name$build_config_target_suffix"
     if (defined(invoker.proguard_android_sdk_dep)) {
-      _android_sdk_dep = invoker.proguard_android_sdk_dep
+      proguard_android_sdk_dep_ = invoker.proguard_android_sdk_dep
     } else {
-      _android_sdk_dep = default_android_sdk_dep
+      proguard_android_sdk_dep_ = "//third_party/android_sdk:android_sdk_java"
     }
 
     if (_proguard_enabled) {
@@ -4994,7 +4965,7 @@
 
     write_build_config(_build_config_target) {
       type = "android_app_bundle"
-      possible_config_deps = _module_targets + [ _android_sdk_dep ]
+      possible_config_deps = _module_targets + [ proguard_android_sdk_dep_ ]
       build_config = _build_config
       proguard_enabled = _proguard_enabled
       module_build_configs = _module_build_configs
diff --git a/build/rust/run_rs_bindings_from_cc.py b/build/rust/run_rs_bindings_from_cc.py
index c031f44df..73b78ff 100755
--- a/build/rust/run_rs_bindings_from_cc.py
+++ b/build/rust/run_rs_bindings_from_cc.py
@@ -12,7 +12,7 @@
 
 # Set up path to be able to import build_utils.
 THIS_DIR = os.path.dirname(os.path.abspath(__file__))
-CHROMIUM_SRC_DIR = os.path.join(THIS_DIR, os.pardir, os.pardir)
+CHROMIUM_SRC_DIR = os.path.relpath(os.path.join(THIS_DIR, os.pardir, os.pardir))
 sys.path.append(THIS_DIR)
 sys.path.append(os.path.join(CHROMIUM_SRC_DIR, 'build', 'android', 'gyp'))
 from run_bindgen import filter_clang_args
@@ -59,20 +59,17 @@
                       nargs=argparse.REMAINDER)
   args = parser.parse_args()
 
-  # Start building the cmdline args to pass to the `rs_bindings_from_cc` tool.
-  genargs = [RS_BINDINGS_FROM_CC_EXE_PATH]
-
   # Output paths
-  genargs.extend(["--rs_out", args.rs_out])
-  genargs.extend(["--cc_out", args.cc_out])
+  generator_args = []
+  generator_args.append("--rs_out={0}".format(os.path.relpath(args.rs_out)))
+  generator_args.append("--cc_out={0}".format(os.path.relpath(args.cc_out)))
   if "CRUBIT_DEBUG" in os.environ:
-    genargs.extend(["--ir_out", args.rs_out.replace(".rs", ".ir")])
+    generator_args.append("--ir_out={0}".format(
+        os.path.relpath(args.rs_out).replace(".rs", ".ir")))
 
   # Public headers.
-  genargs.extend([
-      "--public_headers",
-      ",".join([os.path.relpath(hdr) for hdr in args.public_headers.split(",")])
-  ])
+  generator_args.append("--public_headers={0}".format(",".join(
+      [os.path.relpath(hdr) for hdr in args.public_headers.split(",")])))
 
   # Targets to headers map.
   with open(args.targets_and_headers_from_gn, "r") as f:
@@ -81,38 +78,52 @@
     hdrs = entry["h"]
     for i in range(len(hdrs)):
       hdrs[i] = os.path.relpath(hdrs[i])
-  genargs.extend(["--targets_and_headers", json.dumps(targets_and_headers)])
+  generator_args.append("--targets_and_headers={0}".format(
+      json.dumps(targets_and_headers)))
 
   # All Crubit invocations in Chromium share the following cmdline args.
-  genargs.extend(["--rustfmt_exe_path", RUSTFMT_EXE_PATH])
-  genargs.extend(["--rustfmt_config_path", RUSTFMT_CONFIG_PATH])
-  genargs.extend([
-      "--crubit_support_path",
-      "third_party/crubit/src/rs_bindings_from_cc/support"
-  ])
+  generator_args.append(f"--rustfmt_exe_path={RUSTFMT_EXE_PATH}")
+  generator_args.append(f"--rustfmt_config_path={RUSTFMT_CONFIG_PATH}")
+  generator_args.append(
+      "--crubit_support_path=third_party/crubit/src/rs_bindings_from_cc/support"
+  )
+
+  # Long cmdlines may not work - work around that by using Abseil's `--flagfile`
+  # https://abseil.io/docs/python/guides/flags#a-note-about---flagfile
+  #
+  # Note that `clang_args` are not written to the flag file, because Abseil's
+  # flag parsing code is only aware of `ABSL_FLAG`-declared flags and doesn't
+  # know about Clang args (e.g. `-W...` or `-I...`).
+  params_file_path = os.path.relpath(args.rs_out).replace(".rs", ".params")
+  with open(params_file_path, "w") as f:
+    for line in generator_args:
+      print(line, file=f)
 
   # Clang arguments.
   #
   # The call to `filter_clang_args` is needed to avoid the following error:
   # error: unable to find plugin 'find-bad-constructs'
-  genargs.extend(filter_clang_args(args.clang_args))
+  clang_args = []
+  clang_args.extend(filter_clang_args(args.clang_args))
   # TODO(crbug.com/1329611): This warning needs to be suppressed, because
   # otherwise Crubit/Clang complains as follows:
   #     error: .../third_party/rust-toolchain/bin/rs_bindings_from_cc:
   #     'linker' input unused [-Werror,-Wunused-command-line-argument]
   # Maybe `build/rust/rs_bindings_from_cc.gni` gives too much in `args`?  But
   # then `{{cflags}}` seems perfectly reasonable...
-  genargs += ["-Wno-unused-command-line-argument"]
+  clang_args += ["-Wno-unused-command-line-argument"]
 
   # Print a copy&pastable final cmdline when asked for debugging help.
+  cmdline = [RS_BINDINGS_FROM_CC_EXE_PATH, f"--flagfile={params_file_path}"]
+  cmdline.extend(clang_args)
   if "CRUBIT_DEBUG" in os.environ:
-    pretty_cmdline = format_cmdline(genargs)
+    pretty_cmdline = format_cmdline(cmdline)
     print(f"CRUBIT_DEBUG: CMDLINE: {pretty_cmdline}", file=sys.stderr)
 
   # TODO(crbug.com/1329611): run_bindgen.py removes the outputs when the tool
   # fails.  Maybe we need to do something similar here?  OTOH in most failure
   # modes Crubit will fail *before* generating its outputs...
-  return subprocess.run(genargs).returncode
+  return subprocess.run(cmdline).returncode
 
 
 if __name__ == '__main__':
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinator.java
index 1be97fe..1ecdac0f 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinator.java
@@ -4,6 +4,7 @@
 
 package org.chromium.chrome.browser.customtabs;
 
+import android.graphics.Rect;
 import android.view.View;
 
 import androidx.annotation.NonNull;
@@ -235,6 +236,21 @@
         mCustomTabHeightStrategy.setScrimFraction(scrimFraction);
     }
 
+    @Override
+    protected Rect getAppRectInWindow() {
+        // This is necessary if app handler cannot rely on the popup window that ensures the menu
+        // will not be clipped off the screen, which can happen if Window#FLAGS_LAYOUT_NO_LIMITS
+        // is set to allow the app to be drawn outside the screen in partial CCT.
+        if (mIntentDataProvider.get().isPartialHeightCustomTab()) {
+            View coord = mActivity.findViewById(R.id.coordinator);
+            int[] location = new int[2];
+            coord.getLocationInWindow(location);
+            return new Rect(location[0], location[1], location[0] + coord.getWidth(),
+                    location[1] + coord.getHeight());
+        }
+        return super.getAppRectInWindow();
+    }
+
     /**
      * Delegates changing the background color to the {@link CustomTabHeightStrategy}.
      * Returns {@code true} if any action were taken, {@code false} if not.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategy.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategy.java
index d21ed918f..f701e79 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategy.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategy.java
@@ -131,6 +131,9 @@
     @Nullable
     private Runnable mFinishRunnable;
 
+    // Y offset when a dragging gesture starts.
+    private int mDraggingStartY;
+
     /** A callback to be called once the Custom Tab has been resized. */
     interface OnResizedCallback {
         /** The Custom Tab has been resized. */
@@ -300,6 +303,7 @@
             @Override
             public void onAnimationEnd(Animator animator) {
                 mSpinner.stop();
+                mSpinnerView.setVisibility(View.GONE);
             }
             @Override
             public void onAnimationCancel(Animator animator) {}
@@ -546,12 +550,20 @@
         mActivity.getWindow().setAttributes(attributes);
         if (mFinishRunnable != null) return;
 
-        assert mSpinnerView != null;
-        centerSpinnerVertically((ViewGroup.LayoutParams) mSpinnerView.getLayoutParams());
+        // Show the spinner lazily, only when the tab is dragged _up_, which requires showing
+        // more area than initial state.
+        if (mStatus != HeightStatus.TRANSITION
+                && (mSpinnerView == null || mSpinnerView.getVisibility() != View.VISIBLE)
+                && y < mDraggingStartY) {
+            showSpinnerView();
+        }
+        if (mSpinnerView != null) {
+            centerSpinnerVertically((ViewGroup.LayoutParams) mSpinnerView.getLayoutParams());
+        }
     }
 
     private void onMoveStart() {
-        showSpinnerView();
+        mDraggingStartY = mActivity.getWindow().getAttributes().y;
         updateNavbarVisibility(false);
     }
 
@@ -564,10 +576,12 @@
 
         // TODO(crbug.com/1328555): Look into observing a view resize event to ensure the fade
         // animation can always cover the transition artifact.
-        mSpinnerView.animate()
-                .alpha(0f)
-                .setDuration(SPINNER_FADEOUT_DURATION_MS)
-                .setListener(mSpinnerFadeoutAnimatorListener);
+        if (mSpinnerView != null && mSpinnerView.getVisibility() == View.VISIBLE) {
+            mSpinnerView.animate()
+                    .alpha(0f)
+                    .setDuration(SPINNER_FADEOUT_DURATION_MS)
+                    .setListener(mSpinnerFadeoutAnimatorListener);
+        }
         updateNavbarVisibility(true);
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbar.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbar.java
index ba2bab80..f4d4cda 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbar.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbar.java
@@ -89,9 +89,6 @@
 import org.chromium.ui.widget.Toast;
 import org.chromium.url.GURL;
 
-import java.util.ArrayList;
-import java.util.List;
-
 /**
  * The Toolbar layout to be used for a custom tab. This is used for both phone and tablet UIs.
  */
@@ -614,9 +611,10 @@
     /**
      * Custom tab-specific implementation of the LocationBar interface.
      */
-    private class CustomTabLocationBar implements LocationBar, UrlBar.UrlBarDelegate,
-                                                  LocationBarDataProvider.Observer,
-                                                  View.OnLongClickListener {
+    @VisibleForTesting
+    class CustomTabLocationBar
+            implements LocationBar, UrlBar.UrlBarDelegate, LocationBarDataProvider.Observer,
+                       View.OnLongClickListener {
         private static final int TITLE_ANIM_DELAY_MS = 800;
         private static final int BRANDING_DELAY_MS = 1800;
 
@@ -625,6 +623,12 @@
         private static final int STATE_DOMAIN_AND_TITLE = 2;
         private int mState = STATE_DOMAIN_ONLY;
 
+        // Used for After branding runnables
+        private static final int KEY_UPDATE_TITLE_POST_BRANDING = 0;
+        private static final int KEY_UPDATE_URL_POST_BRANDING = 1;
+        private static final int KEY_UPDATE_ICON_POST_BRANDING = 2;
+        private static final int TOTAL_POST_BRANDING_KEYS = 3;
+
         private LocationBarDataProvider mLocationBarDataProvider;
         private Supplier<EphemeralTabCoordinator> mEphemeralTabCoordinatorSupplier;
         private Supplier<ModalDialogManager> mModalDialogManagerSupplier;
@@ -644,8 +648,9 @@
             }
         };
 
+        private final Runnable[] mAfterBrandingRunnables = new Runnable[TOTAL_POST_BRANDING_KEYS];
         private boolean mCurrentlyShowingBranding;
-        private List<Runnable> mAfterBrandingRunnables;
+        private boolean mBrandingStarted;
         private CallbackController mCallbackController = new CallbackController();
 
         public View getLayout() {
@@ -661,8 +666,8 @@
         }
 
         public void showBranding() {
+            mBrandingStarted = true;
             mCurrentlyShowingBranding = true;
-            mAfterBrandingRunnables = new ArrayList<>();
 
             // Store the title and domain setting.
             final boolean showTitle = mState != STATE_DOMAIN_ONLY;
@@ -678,10 +683,7 @@
                 mCurrentlyShowingBranding = false;
                 setUrlBarHidden(!showUrlBar);
                 setShowTitle(showTitle);
-                for (Runnable runnable : mAfterBrandingRunnables) {
-                    runnable.run();
-                }
-                mAfterBrandingRunnables.clear();
+                runAfterBrandingRunnables();
             });
             PostTask.postDelayedTask(UiThreadTaskTraits.DEFAULT, hideBranding, BRANDING_DELAY_MS);
         }
@@ -860,10 +862,20 @@
             mTitleAnimationStarter.run();
         }
 
+        private void runAfterBrandingRunnables() {
+            for (int i = 0; i < mAfterBrandingRunnables.length; i++) {
+                Runnable runnable = mAfterBrandingRunnables[i];
+                if (runnable != null) {
+                    runnable.run();
+                    mAfterBrandingRunnables[i] = null;
+                }
+            }
+        }
+
         private void updateSecurityIcon() {
             if (mState == STATE_TITLE_ONLY) return;
             if (mCurrentlyShowingBranding) {
-                mAfterBrandingRunnables.add(this::updateSecurityIcon);
+                mAfterBrandingRunnables[KEY_UPDATE_ICON_POST_BRANDING] = this::updateSecurityIcon;
                 return;
             }
 
@@ -884,7 +896,7 @@
 
         private void updateTitleBar() {
             if (mCurrentlyShowingBranding) {
-                mAfterBrandingRunnables.add(this::updateTitleBar);
+                mAfterBrandingRunnables[KEY_UPDATE_TITLE_POST_BRANDING] = this::updateTitleBar;
                 return;
             }
             String title = mLocationBarDataProvider.getTitle();
@@ -902,9 +914,7 @@
                 // Delay the title animation until security icon animation finishes.
                 // If this is updated after branding, we don't need to wait.
                 PostTask.postDelayedTask(UiThreadTaskTraits.DEFAULT, mTitleAnimationStarter,
-                        mAfterBrandingRunnables != null && mAfterBrandingRunnables.size() > 0
-                                ? 0
-                                : TITLE_ANIM_DELAY_MS);
+                        mBrandingStarted ? 0 : TITLE_ANIM_DELAY_MS);
             }
 
             mTitleBar.setText(title);
@@ -912,7 +922,7 @@
 
         private void updateUrlBar() {
             if (mCurrentlyShowingBranding) {
-                mAfterBrandingRunnables.add(this::updateUrlBar);
+                mAfterBrandingRunnables[KEY_UPDATE_URL_POST_BRANDING] = this::updateUrlBar;
                 return;
             }
             Tab tab = getCurrentTab();
@@ -1042,5 +1052,10 @@
             }
             return false;
         }
+
+        @VisibleForTesting
+        void setAnimDelegateForTesting(CustomTabToolbarAnimationDelegate animDelegate) {
+            mAnimDelegate = animDelegate;
+        }
     }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java
index c1752306..69a8e2ab 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java
@@ -1223,13 +1223,7 @@
                     mActivityLifecycleDispatcher, mToolbarManager, mAppMenuDelegate,
                     mActivity.getWindow().getDecorView(),
                     mActivity.getWindow().getDecorView().findViewById(R.id.menu_anchor_stub),
-                    () -> {
-                        View coord = mActivity.findViewById(R.id.coordinator);
-                        int[] location = new int[2];
-                        coord.getLocationInWindow(location);
-                        return new Rect(location[0], location[1], location[0] + coord.getWidth(),
-                                location[1] + coord.getHeight());
-                    });
+                    this::getAppRectInWindow);
             AppMenuCoordinatorFactory.setExceptionReporter(
                     (throwable)
                             -> ChromePureJavaExceptionReporter.reportJavaException(
@@ -1242,6 +1236,15 @@
         }
     }
 
+    /**
+     * Returns {@link Rect} that represents the app client area the app menu should fit in.
+     */
+    protected Rect getAppRectInWindow() {
+        Rect appRect = new Rect();
+        mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(appRect);
+        return appRect;
+    }
+
     private void hideAppMenu() {
         if (mAppMenuCoordinator != null) mAppMenuCoordinator.getAppMenuHandler().hideAppMenu();
     }
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategyTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategyTest.java
index 408c545c..443aa5c 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategyTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategyTest.java
@@ -9,8 +9,10 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -479,6 +481,54 @@
         assertTrue("Close click handler should be called.", closed[0]);
     }
 
+    @Test
+    public void showSpinnerOnDragUpOnly() {
+        PartialCustomTabHeightStrategy strategy = createPcctAtHeight(500);
+
+        verify(mWindow).addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
+        verify(mWindow).clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+
+        assertEquals(1, mAttributeResults.size());
+        assertEquals(MAX_INIT_POS, mAttributeResults.get(0).y);
+
+        when(mSpinnerView.getVisibility()).thenReturn(View.GONE);
+
+        // Pass null because we have a mock Activity and we don't depend on the GestureDetector
+        // inside as we test MotionEvents directly.
+        PartialCustomTabHeightStrategy.PartialCustomTabHandleStrategy handleStrategy =
+                strategy.new PartialCustomTabHandleStrategy(null);
+
+        // action down
+        handleStrategy.onTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
+                SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, DEVICE_WIDTH / 2, 1500, 0));
+
+        // action move
+        handleStrategy.onTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
+                SystemClock.uptimeMillis(), MotionEvent.ACTION_MOVE, DEVICE_WIDTH / 2, 1450, 0));
+
+        // Verify the spinner is visible.
+        verify(mSpinnerView, times(1)).setVisibility(View.VISIBLE);
+        when(mSpinnerView.getVisibility()).thenReturn(View.VISIBLE);
+        clearInvocations(mSpinnerView);
+
+        // action up
+        handleStrategy.onTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
+                SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, DEVICE_WIDTH / 2, 1400, 0));
+
+        // Wait animation to finish.
+        shadowOf(Looper.getMainLooper()).idle();
+        when(mSpinnerView.getVisibility()).thenReturn(View.GONE);
+
+        // Now the tab is full-height. Start dragging down.
+        handleStrategy.onTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
+                SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, DEVICE_WIDTH / 2, 500, 0));
+        handleStrategy.onTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
+                SystemClock.uptimeMillis(), MotionEvent.ACTION_MOVE, DEVICE_WIDTH / 2, 650, 0));
+
+        // Verify the spinner remained invisible.
+        verify(mSpinnerView, times(0)).setVisibility(anyInt());
+    }
+
     private void verifyWindowFlagsSet() {
         verify(mWindow).addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
         verify(mWindow).clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbarUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbarUnitTest.java
index f4369723..74c07e89 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbarUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbarUnitTest.java
@@ -5,20 +5,88 @@
 package org.chromium.chrome.browser.customtabs.features.toolbar;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
+import android.app.Activity;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.Robolectric;
 import org.robolectric.annotation.Config;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadows.ShadowLooper;
 
+import org.chromium.base.task.TaskTraits;
+import org.chromium.base.task.test.ShadowPostTask;
+import org.chromium.base.task.test.ShadowPostTask.TestImpl;
 import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabToolbar.CustomTabLocationBar;
+import org.chromium.chrome.browser.toolbar.LocationBarModel;
+import org.chromium.ui.base.TestActivity;
 import org.chromium.url.JUnitTestGURLs;
 
 /**
  * Tests AMP url handling in the CustomTab Toolbar.
  */
 @RunWith(BaseRobolectricTestRunner.class)
-@Config(manifest = Config.NONE)
+@Config(manifest = Config.NONE, shadows = {ShadowLooper.class, ShadowPostTask.class})
+@LooperMode(Mode.PAUSED)
 public class CustomTabToolbarUnitTest {
+    @Rule
+    public MockitoRule mRule = MockitoJUnit.rule();
+
+    @Mock
+    LocationBarModel mLocationBarModel;
+    @Mock
+    ActionMode.Callback mActionModeCallback;
+    @Mock
+    CustomTabToolbarAnimationDelegate mAnimationDelegate;
+
+    private CustomTabLocationBar mLocationBar;
+    private TextView mTitleBar;
+    private TextView mUrlBar;
+
+    @Before
+    public void setup() {
+        ShadowPostTask.setTestImpl(new TestImpl() {
+            @Override
+            public void postDelayedTask(TaskTraits taskTraits, Runnable task, long delay) {
+                new Handler(Looper.getMainLooper()).postDelayed(task, delay);
+            }
+        });
+        Mockito.doReturn(R.string.accessibility_security_btn_secure)
+                .when(mLocationBarModel)
+                .getSecurityIconContentDescriptionResourceId();
+        Mockito.doReturn(R.color.default_icon_color_tint_list)
+                .when(mLocationBarModel)
+                .getSecurityIconColorStateList();
+
+        Activity activity = Robolectric.buildActivity(TestActivity.class).get();
+        CustomTabToolbar toolbar = (CustomTabToolbar) LayoutInflater.from(activity).inflate(
+                R.layout.custom_tabs_toolbar, null, false);
+        mLocationBar = (CustomTabLocationBar) toolbar.createLocationBar(
+                mLocationBarModel, mActionModeCallback, () -> null, () -> null);
+        mUrlBar = toolbar.findViewById(R.id.url_bar);
+        mTitleBar = toolbar.findViewById(R.id.title_bar);
+    }
+
     @Test
     public void testParsesPublisherFromAmp() {
         assertEquals("www.nyt.com",
@@ -31,4 +99,54 @@
                 CustomTabToolbar.parsePublisherNameFromUrl(
                         JUnitTestGURLs.getGURL(JUnitTestGURLs.EXAMPLE_URL)));
     }
+
+    @Test
+    public void testShowBranding_DomainOnly() {
+        mLocationBar.setAnimDelegateForTesting(mAnimationDelegate);
+
+        mLocationBar.showBranding();
+        ShadowLooper.idleMainLooper();
+        verify(mLocationBarModel).notifyTitleChanged();
+        verify(mAnimationDelegate).prepareTitleAnim(mUrlBar, mTitleBar);
+        verify(mAnimationDelegate).startTitleAnimation(any());
+        assertEquals("URL bar should not be visible during branding.", mUrlBar.getVisibility(),
+                View.GONE);
+
+        // Run all UI tasks, until the branding is finished.
+        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
+        verify(mLocationBarModel, times(2)).notifyTitleChanged();
+        verify(mLocationBarModel).notifyUrlChanged();
+        verify(mLocationBarModel).notifySecurityStateChanged();
+
+        assertEquals("URL bar is not visible.", mUrlBar.getVisibility(), View.VISIBLE);
+    }
+
+    @Test
+    public void testShowBranding_DomainAndTitle() {
+        mLocationBar.setAnimDelegateForTesting(mAnimationDelegate);
+
+        // Set title before the branding starts, so the state is domain and title.
+        mLocationBar.setShowTitle(true);
+        ShadowLooper.idleMainLooper();
+        verify(mLocationBarModel).notifyTitleChanged();
+        verify(mAnimationDelegate).prepareTitleAnim(mUrlBar, mTitleBar);
+        // Animation not started since branding is not completed.
+        verify(mAnimationDelegate, never()).startTitleAnimation(any());
+
+        mLocationBar.showBranding();
+        ShadowLooper.idleMainLooper();
+        verify(mLocationBarModel, times(2)).notifyTitleChanged();
+        verify(mAnimationDelegate, times(2)).prepareTitleAnim(mUrlBar, mTitleBar);
+        verify(mAnimationDelegate).startTitleAnimation(any());
+        assertEquals("URL bar should not be visible during branding.", mUrlBar.getVisibility(),
+                View.GONE);
+
+        // Run all UI tasks, until the branding is finished.
+        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
+        verify(mLocationBarModel, times(3)).notifyTitleChanged();
+        verify(mLocationBarModel).notifyUrlChanged();
+        verify(mLocationBarModel).notifySecurityStateChanged();
+
+        assertEquals("URL bar is not visible.", mUrlBar.getVisibility(), View.VISIBLE);
+    }
 }
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersCoordinatorTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersCoordinatorTest.java
index bd2746b2..38d7fbb 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersCoordinatorTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersCoordinatorTest.java
@@ -25,6 +25,7 @@
 import androidx.test.core.app.ActivityScenario;
 
 import com.google.android.material.tabs.TabLayout;
+import com.google.common.collect.ImmutableMap;
 
 import org.hamcrest.Matchers;
 import org.junit.After;
@@ -64,6 +65,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArraySet;
 
@@ -205,7 +207,8 @@
                 new ArrayList<>(), mGurl2, 123L, new ArrayList<>());
         mCluster = new HistoryCluster(Arrays.asList(mVisit1, mVisit2), "\"label\"", "label",
                 Collections.emptyList(), 123L, Arrays.asList("pugs", "terriers"));
-        mClusterResult = new HistoryClustersResult(Arrays.asList(mCluster), "dogs", false, false);
+        mClusterResult = new HistoryClustersResult(Arrays.asList(mCluster),
+                new LinkedHashMap<>(ImmutableMap.of("label", 1)), "dogs", false, false);
 
         mActivityScenario =
                 ActivityScenario.launch(ChromeTabbedActivity.class).onActivity(activity -> {
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediatorTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediatorTest.java
index ba0ee343..2b971748 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediatorTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediatorTest.java
@@ -29,6 +29,8 @@
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.google.common.collect.ImmutableMap;
+
 import org.hamcrest.BaseMatcher;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
@@ -71,6 +73,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
@@ -248,12 +251,17 @@
                 new ArrayList<>(), 123L, Collections.emptyList());
         mCluster3 = new HistoryCluster(Arrays.asList(mVisit4), "\"label3\"", "label3",
                 new ArrayList<>(), 789L, Collections.EMPTY_LIST);
-        mHistoryClustersResultWithQuery = new HistoryClustersResult(
-                Arrays.asList(mCluster1, mCluster2), "query", true, false);
-        mHistoryClustersFollowupResultWithQuery =
-                new HistoryClustersResult(Arrays.asList(mCluster3), "query", false, true);
+        mHistoryClustersResultWithQuery =
+                new HistoryClustersResult(Arrays.asList(mCluster1, mCluster2),
+                        new LinkedHashMap<>(ImmutableMap.of("label", 1)), "query", true, false);
+        mHistoryClustersFollowupResultWithQuery = new HistoryClustersResult(
+                Arrays.asList(mCluster3),
+                new LinkedHashMap<>(ImmutableMap.of("label", 1, "hostname.com", 1, "label3", 1)),
+                "query", false, true);
         mHistoryClustersResultEmptyQuery =
-                new HistoryClustersResult(Arrays.asList(mCluster1, mCluster2), "", false, false);
+                new HistoryClustersResult(Arrays.asList(mCluster1, mCluster2),
+                        new LinkedHashMap<>(ImmutableMap.of("label", 1, "hostname.com", 1)), "",
+                        false, false);
     }
 
     @Test
@@ -318,8 +326,9 @@
 
         ListItem item = mModelList.get(3);
         PropertyModel model = item.model;
-        assertTrue(model.getAllSetProperties().containsAll(Arrays.asList(
-                HistoryClustersItemProperties.CLICK_HANDLER, HistoryClustersItemProperties.TITLE)));
+        assertTrue(model.getAllSetProperties().containsAll(
+                Arrays.asList(HistoryClustersItemProperties.CLICK_HANDLER,
+                        HistoryClustersItemProperties.TITLE, HistoryClustersItemProperties.LABEL)));
     }
 
     @Test
@@ -496,6 +505,7 @@
         doReturn(secondPromise).when(mBridge).loadMoreClusters("query");
         doReturn(3).when(mLayoutManager).findLastVisibleItemPosition();
 
+        mMediator.setQueryState(QueryState.forQuery("query", ""));
         mMediator.startQuery("query");
         fulfillPromise(promise, mHistoryClustersResultWithQuery);
 
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index b834023..ef07cd4 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -6687,7 +6687,7 @@
     deps += [
       "//chrome/common/printing",
       "//components/printing/browser",
-      "//components/printing/browser/print_to_pdf",
+      "//components/printing/browser/headless",
       "//components/printing/common:mojo_interfaces",
       "//components/services/print_compositor/public/cpp",
       "//components/services/print_compositor/public/mojom",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index e83c29e5..31846374 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -9001,10 +9001,17 @@
      flag_descriptions::kStylusWritingToInputDescription, kOsAndroid,
      FEATURE_VALUE_TYPE(blink::features::kStylusWritingToInput)},
 #endif  // BUILDFLAG(IS_ANDROID)
-        // NOTE: Adding a new flag requires adding a corresponding entry to enum
-        // "LoginCustomFlags" in tools/metrics/histograms/enums.xml. See "Flag
-        // Histograms" in tools/metrics/histograms/README.md (run the
-        // AboutFlagsHistogramTest unit test to verify this process).
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+    {"enable-media-dynamic-cgroup", flag_descriptions::kMediaDynamicCgroupName,
+     flag_descriptions::kMediaDynamicCgroupDescription, kOsCrOS,
+     PLATFORM_FEATURE_NAME_TYPE("CrOSLateBootMediaDynamicCgroup")},
+#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
+
+    // NOTE: Adding a new flag requires adding a corresponding entry to enum
+    // "LoginCustomFlags" in tools/metrics/histograms/enums.xml. See "Flag
+    // Histograms" in tools/metrics/histograms/README.md (run the
+    // AboutFlagsHistogramTest unit test to verify this process).
 };
 
 class FlagsStateSingleton : public flags_ui::FlagsState::Delegate {
diff --git a/chrome/browser/ash/customization/customization_document_browsertest.cc b/chrome/browser/ash/customization/customization_document_browsertest.cc
index 2ccac9d..ad9b38f 100644
--- a/chrome/browser/ash/customization/customization_document_browsertest.cc
+++ b/chrome/browser/ash/customization/customization_document_browsertest.cc
@@ -240,11 +240,13 @@
       << "Test failed for initial_locale='" << GetParam() << "'";
 
   for (size_t i = 0; i < ui_language_list->GetListDeprecated().size(); ++i) {
-    base::DictionaryValue* language_info = NULL;
-    ASSERT_TRUE(ui_language_list->GetDictionary(i, &language_info))
+    base::Value::Dict* language_info =
+        ui_language_list->GetList()[i].GetIfDict();
+
+    ASSERT_TRUE(language_info)
         << "Test failed for initial_locale='" << GetParam() << "', i=" << i;
 
-    const std::string* value = language_info->FindStringKey("value");
+    std::string* value = language_info->FindString("value");
     ASSERT_TRUE(value) << "Test failed for initial_locale='" << GetParam()
                        << "', i=" << i;
 
diff --git a/chrome/browser/ash/dbus/org.chromium.ChromeFeaturesService.conf b/chrome/browser/ash/dbus/org.chromium.ChromeFeaturesService.conf
index e8a7c17..9cdf90aa 100644
--- a/chrome/browser/ash/dbus/org.chromium.ChromeFeaturesService.conf
+++ b/chrome/browser/ash/dbus/org.chromium.ChromeFeaturesService.conf
@@ -38,6 +38,12 @@
            send_member="IsFeatureEnabled"/>
   </policy>
 
+  <policy user="resourced">
+    <allow send_destination="org.chromium.ChromeFeaturesService"
+           send_interface="org.chromium.ChromeFeaturesServiceInterface"
+           send_member="IsFeatureEnabled"/>
+  </policy>
+
   <!-- limit cras visibility to only IsFeatureEnabled -->
   <policy user="cras">
     <allow send_destination="org.chromium.ChromeFeaturesService"
diff --git a/chrome/browser/ash/login/chrome_restart_request.cc b/chrome/browser/ash/login/chrome_restart_request.cc
index c4f9398..bdee04a1 100644
--- a/chrome/browser/ash/login/chrome_restart_request.cc
+++ b/chrome/browser/ash/login/chrome_restart_request.cc
@@ -117,6 +117,7 @@
     ::switches::kEnableGpuMemoryBufferVideoFrames,
     ::switches::kEnableGpuRasterization,
     ::switches::kEnableLogging,
+    ::switches::kEnableMicrophoneMuteSwitchDeviceSwitch,
     ::switches::kEnableNativeGpuMemoryBuffers,
     ::switches::kEnableTouchDragDrop,
     ::switches::kEnableUnifiedDesktop,
diff --git a/chrome/browser/ash/remote_apps/remote_apps_manager_browsertest.cc b/chrome/browser/ash/remote_apps/remote_apps_manager_browsertest.cc
index 73d1716a..ee89303 100644
--- a/chrome/browser/ash/remote_apps/remote_apps_manager_browsertest.cc
+++ b/chrome/browser/ash/remote_apps/remote_apps_manager_browsertest.cc
@@ -7,6 +7,7 @@
 #include "ash/app_list/app_list_model_provider.h"
 #include "ash/app_list/model/app_list_item.h"
 #include "ash/app_list/model/app_list_model.h"
+#include "ash/app_list/views/app_list_item_view.h"
 #include "ash/components/login/auth/public/user_context.h"
 #include "ash/constants/ash_switches.h"
 #include "ash/public/cpp/accelerators.h"
@@ -56,6 +57,7 @@
 #include "content/public/test/browser_test.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "ui/events/test/event_generator.h"
 #include "ui/gfx/image/image_skia.h"
 #include "ui/gfx/image/image_unittest_util.h"
 
@@ -282,15 +284,13 @@
     ASSERT_EQ(error, future.Get<1>());
   }
 
-  void ShowLauncherAppsGrid() {
+  void ShowLauncherAppsGrid(bool wait_for_opening_animation) {
     AppListClientImpl* client = AppListClientImpl::GetInstance();
     EXPECT_FALSE(client->GetAppListWindow());
     ash::AcceleratorController::Get()->PerformActionIfEnabled(
         ash::TOGGLE_APP_LIST_FULLSCREEN, {});
-    if (ash::features::IsProductivityLauncherEnabled()) {
-      ash::AppListTestApi().WaitForBubbleWindow(
-          /*wait_for_opening_animation=*/false);
-    }
+    if (ash::features::IsProductivityLauncherEnabled())
+      ash::AppListTestApi().WaitForBubbleWindow(wait_for_opening_animation);
     EXPECT_TRUE(client->GetAppListWindow());
   }
 
@@ -303,7 +303,7 @@
 
 IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddApp) {
   // Show launcher UI so that app icons are loaded.
-  ShowLauncherAppsGrid();
+  ShowLauncherAppsGrid(/*wait_for_opening_animation=*/false);
 
   std::string name = "name";
   GURL icon_url("icon_url");
@@ -333,7 +333,7 @@
 // default placeholder icon.
 IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddAppPlaceholderIcon) {
   // Show launcher UI so that app icons are loaded.
-  ShowLauncherAppsGrid();
+  ShowLauncherAppsGrid(/*wait_for_opening_animation=*/true);
 
   const std::string name = "name";
 
@@ -356,7 +356,24 @@
                          future.GetCallback());
 
   // App's icon is placeholder.
-  CheckIconsEqual(future.Get()->uncompressed, item->GetDefaultIcon());
+  // TODO(https://crbug.com/1345682): add a pixel diff test for this scenario.
+  ash::AppListTestApi app_list_test_api;
+  CheckIconsEqual(
+      future.Get()->uncompressed,
+      app_list_test_api.GetTopLevelItemViewFromId(kId1)->icon_image_for_test());
+
+  // App list color sorting should still work for placeholder icons.
+  ui::test::EventGenerator event_generator(ash::Shell::GetPrimaryRootWindow());
+  ash::AppListTestApi::ReorderAnimationEndState actual_state;
+
+  app_list_test_api.ReorderByMouseClickAtToplevelAppsGridMenu(
+      ash::AppListSortOrder::kColor,
+      ash::AppListTestApi::MenuType::kAppListNonFolderItemMenu,
+      &event_generator,
+      /*target_state=*/
+      ash::AppListTestApi::ReorderAnimationEndState::kCompleted, &actual_state);
+  EXPECT_EQ(ash::AppListTestApi::ReorderAnimationEndState::kCompleted,
+            actual_state);
 }
 
 IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddAppError) {
diff --git a/chrome/browser/browser_features.cc b/chrome/browser/browser_features.cc
index cbd5b12..95b5e4c 100644
--- a/chrome/browser/browser_features.cc
+++ b/chrome/browser/browser_features.cc
@@ -155,8 +155,13 @@
 
 // Controls whether the static key pinning list can be updated via component
 // updater.
+#if BUILDFLAG(IS_ANDROID)
 const base::Feature kKeyPinningComponentUpdater{
     "KeyPinningComponentUpdater", base::FEATURE_DISABLED_BY_DEFAULT};
+#else
+const base::Feature kKeyPinningComponentUpdater{
+    "KeyPinningComponentUpdater", base::FEATURE_ENABLED_BY_DEFAULT};
+#endif
 
 // When this feature is enabled, the network service will restart unsandboxed if
 // a previous attempt to launch it sandboxed failed.
diff --git a/chrome/browser/chrome_content_browser_client_receiver_bindings.cc b/chrome/browser/chrome_content_browser_client_receiver_bindings.cc
index acfd081..c32733d 100644
--- a/chrome/browser/chrome_content_browser_client_receiver_bindings.cc
+++ b/chrome/browser/chrome_content_browser_client_receiver_bindings.cc
@@ -112,7 +112,7 @@
 
 #if BUILDFLAG(ENABLE_PRINTING)
 #include "chrome/browser/printing/print_view_manager_basic.h"
-#include "components/printing/browser/print_to_pdf/pdf_print_manager.h"
+#include "components/printing/browser/headless/headless_print_manager.h"
 #if BUILDFLAG(ENABLE_PRINT_PREVIEW)
 #include "chrome/browser/printing/print_view_manager.h"
 #endif  // BUILDFLAG(ENABLE_PRINT_PREVIEW)
@@ -581,7 +581,7 @@
            mojo::PendingAssociatedReceiver<printing::mojom::PrintManagerHost>
                receiver) {
           if (headless::IsChromeNativeHeadless()) {
-            print_to_pdf::PdfPrintManager::BindPrintManagerHost(
+            headless::HeadlessPrintManager::BindPrintManagerHost(
                 std::move(receiver), render_frame_host);
           } else {
 #if BUILDFLAG(ENABLE_PRINT_PREVIEW)
diff --git a/chrome/browser/devtools/BUILD.gn b/chrome/browser/devtools/BUILD.gn
index 821d669..ec5a2e2 100644
--- a/chrome/browser/devtools/BUILD.gn
+++ b/chrome/browser/devtools/BUILD.gn
@@ -267,7 +267,10 @@
       ]
     }
     if (enable_basic_printing) {
-      deps += [ "//components/printing/browser/print_to_pdf" ]
+      deps += [
+        "//components/printing/browser/headless",
+        "//components/printing/browser/print_to_pdf",
+      ]
     }
     sources += rebase_path(_protocol_generated, ".", target_gen_dir)
   }
diff --git a/chrome/browser/devtools/protocol/page_handler.cc b/chrome/browser/devtools/protocol/page_handler.cc
index a286ee1..52c5f62 100644
--- a/chrome/browser/devtools/protocol/page_handler.cc
+++ b/chrome/browser/devtools/protocol/page_handler.cc
@@ -7,7 +7,6 @@
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/web_applications/web_app_helpers.h"
 #include "components/payments/content/payment_request_web_contents_manager.h"
-#include "components/printing/browser/print_to_pdf/pdf_print_result.h"
 #include "components/subresource_filter/content/browser/devtools_interaction_tracker.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/common/manifest/manifest_util.h"
@@ -220,8 +219,8 @@
       transfer_mode.fromMaybe("") ==
       protocol::Page::PrintToPDF::TransferModeEnum::ReturnAsStream;
 
-  if (auto* print_manager =
-          print_to_pdf::PdfPrintManager::FromWebContents(web_contents_.get())) {
+  if (auto* print_manager = headless::HeadlessPrintManager::FromWebContents(
+          web_contents_.get())) {
     print_manager->PrintToPdf(
         web_contents_->GetPrimaryMainFrame(), page_ranges.fromMaybe(""),
         std::move(absl::get<printing::mojom::PrintPagesParamsPtr>(
diff --git a/chrome/browser/devtools/protocol/page_handler.h b/chrome/browser/devtools/protocol/page_handler.h
index 4d7f1d90..6b725f3 100644
--- a/chrome/browser/devtools/protocol/page_handler.h
+++ b/chrome/browser/devtools/protocol/page_handler.h
@@ -16,7 +16,8 @@
 #include "third_party/blink/public/common/manifest/manifest.h"
 
 #if BUILDFLAG(ENABLE_PRINTING)
-#include "components/printing/browser/print_to_pdf/pdf_print_manager.h"
+#include "components/printing/browser/headless/headless_print_manager.h"
+#include "components/printing/browser/print_to_pdf/pdf_print_result.h"
 #endif  // BUILDFLAG(ENABLE_PRINTING)
 
 namespace content {
diff --git a/chrome/browser/enterprise/reporting/browser_report_generator_unittest.cc b/chrome/browser/enterprise/reporting/browser_report_generator_unittest.cc
index ca91326..884be35 100644
--- a/chrome/browser/enterprise/reporting/browser_report_generator_unittest.cc
+++ b/chrome/browser/enterprise/reporting/browser_report_generator_unittest.cc
@@ -26,6 +26,7 @@
 #include "content/public/common/webplugininfo.h"
 #include "content/public/test/browser_task_environment.h"
 #include "device_management_backend.pb.h"
+#include "ppapi/buildflags/buildflags.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 #if !BUILDFLAG(IS_ANDROID)
diff --git a/chrome/browser/enterprise/reporting/report_generator_unittest.cc b/chrome/browser/enterprise/reporting/report_generator_unittest.cc
index c79e19e..18dc544 100644
--- a/chrome/browser/enterprise/reporting/report_generator_unittest.cc
+++ b/chrome/browser/enterprise/reporting/report_generator_unittest.cc
@@ -25,6 +25,7 @@
 #include "components/policy/core/common/cloud/cloud_policy_util.h"
 #include "content/public/common/webplugininfo.h"
 #include "content/public/test/browser_task_environment.h"
+#include "ppapi/buildflags/buildflags.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
diff --git a/chrome/browser/enterprise/reporting/report_request_queue_generator_unittest.cc b/chrome/browser/enterprise/reporting/report_request_queue_generator_unittest.cc
index 6f03134..58cebbc 100644
--- a/chrome/browser/enterprise/reporting/report_request_queue_generator_unittest.cc
+++ b/chrome/browser/enterprise/reporting/report_request_queue_generator_unittest.cc
@@ -24,6 +24,7 @@
 #include "components/policy/core/common/policy_map.h"
 #include "components/sync_preferences/pref_service_syncable.h"
 #include "content/public/test/browser_task_environment.h"
+#include "ppapi/buildflags/buildflags.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 #if BUILDFLAG(IS_ANDROID)
diff --git a/chrome/browser/extensions/extension_special_storage_policy.cc b/chrome/browser/extensions/extension_special_storage_policy.cc
index fde411df..dbd10c1 100644
--- a/chrome/browser/extensions/extension_special_storage_policy.cc
+++ b/chrome/browser/extensions/extension_special_storage_policy.cc
@@ -154,11 +154,9 @@
   // Fetch the list of cookies related content_settings and bind it
   // to CookieSettings::ShouldDeleteCookieOnExit to avoid fetching it on
   // every call.
-  ContentSettingsForOneType entries;
-  cookie_settings_->GetCookieSettings(&entries);
   return base::BindRepeating(
       &content_settings::CookieSettings::ShouldDeleteCookieOnExit,
-      cookie_settings_, std::move(entries));
+      cookie_settings_, cookie_settings_->GetCookieSettings());
 }
 
 bool ExtensionSpecialStoragePolicy::HasSessionOnlyOrigins() {
@@ -167,10 +165,9 @@
   if (cookie_settings_->GetDefaultCookieSetting(NULL) ==
       CONTENT_SETTING_SESSION_ONLY)
     return true;
-  ContentSettingsForOneType entries;
-  cookie_settings_->GetCookieSettings(&entries);
-  for (size_t i = 0; i < entries.size(); ++i) {
-    if (entries[i].GetContentSetting() == CONTENT_SETTING_SESSION_ONLY)
+  for (const ContentSettingPatternSource& entry :
+       cookie_settings_->GetCookieSettings()) {
+    if (entry.GetContentSetting() == CONTENT_SETTING_SESSION_ONLY)
       return true;
   }
   return false;
diff --git a/chrome/browser/feed/android/BUILD.gn b/chrome/browser/feed/android/BUILD.gn
index f53234f5..83cef0b 100644
--- a/chrome/browser/feed/android/BUILD.gn
+++ b/chrome/browser/feed/android/BUILD.gn
@@ -61,6 +61,7 @@
     "java/src/org/chromium/chrome/browser/feed/hooks/FeedHooks.java",
     "java/src/org/chromium/chrome/browser/feed/hooks/FeedHooksImpl.java",
     "java/src/org/chromium/chrome/browser/feed/sections/OnSectionHeaderSelectedListener.java",
+    "java/src/org/chromium/chrome/browser/feed/sections/SectionHeaderBadgeDrawable.java",
     "java/src/org/chromium/chrome/browser/feed/sections/SectionHeaderListProperties.java",
     "java/src/org/chromium/chrome/browser/feed/sections/SectionHeaderProperties.java",
     "java/src/org/chromium/chrome/browser/feed/sections/SectionHeaderView.java",
@@ -203,7 +204,6 @@
     "java/res/values/dimens.xml",
     "java/res/values/styles.xml",
     "java/res/xml/feed_autoplay_preferences.xml",
-    "java/res/xml/tab_layout_badge.xml",
   ]
   deps = [
     "//chrome/browser/ui/android/strings:ui_strings_grd",
diff --git a/chrome/browser/feed/android/java/res/values/dimens.xml b/chrome/browser/feed/android/java/res/values/dimens.xml
index 5fe6acb0..77b78f76 100644
--- a/chrome/browser/feed/android/java/res/values/dimens.xml
+++ b/chrome/browser/feed/android/java/res/values/dimens.xml
@@ -52,6 +52,11 @@
     <dimen name="feed_header_tab_extra_margin_right">7dp</dimen>
     <dimen name="feed_header_background_corner_radius">60dp</dimen>
 
+    <!-- Feed unread dot dimens -->
+    <dimen name="feed_badge_radius">2.5dp</dimen>
+    <dimen name="feed_badge_hoffset">-1dp</dimen>
+    <dimen name="feed_badge_voffset">2dp</dimen>
+
     <!-- Dynamic color dimensions. -->
     <dimen name="feed_header_section_tab_bg_elevation_enabled">@dimen/default_elevation_2</dimen>
     <dimen name="feed_header_tab_selected_bg_elevation">@dimen/default_elevation_0</dimen>
diff --git a/chrome/browser/feed/android/java/res/xml/tab_layout_badge.xml b/chrome/browser/feed/android/java/res/xml/tab_layout_badge.xml
deleted file mode 100644
index d4c8bb5..0000000
--- a/chrome/browser/feed/android/java/res/xml/tab_layout_badge.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright 2021 The Chromium Authors. All rights reserved.
-     Use of this source code is governed by a BSD-style license that can be
-     found in the LICENSE file. -->
-
-<badge
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    app:backgroundColor="@macro/default_text_color_accent1"
-    app:badgeRadius="2.5dp"
-    app:horizontalOffset="-5dp"
-    app:verticalOffset="2dp"/>
diff --git a/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/sections/SectionHeaderBadgeDrawable.java b/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/sections/SectionHeaderBadgeDrawable.java
new file mode 100644
index 0000000..2e8f64ee
--- /dev/null
+++ b/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/sections/SectionHeaderBadgeDrawable.java
@@ -0,0 +1,130 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.feed.sections;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatTextView;
+
+import com.google.android.material.shape.MaterialShapeDrawable;
+
+import org.chromium.chrome.browser.feed.R;
+import org.chromium.components.browser_ui.styles.SemanticColorUtils;
+
+/**
+ * Drawable representing the unread dot.
+ *
+ * Allows for setting of text and animating the width when text changes.
+ */
+public class SectionHeaderBadgeDrawable extends Drawable {
+    private Paint mPaint;
+    // Only used for calculating text width in dps and to easily apply text appearances.
+    private AppCompatTextView mTextView;
+    private MaterialShapeDrawable mShapeDrawable;
+    private String mText;
+    private Context mContext;
+
+    public SectionHeaderBadgeDrawable(Context context) {
+        mContext = context;
+
+        // This textview is only used to easily set the text parameters and calculate textual width.
+        mTextView = new AppCompatTextView(context);
+        mTextView.setTextAppearance(context, R.style.TextAppearance_Material3_LabelSmall);
+        mPaint = mTextView.getPaint();
+        mPaint.setTextAlign(Paint.Align.CENTER);
+
+        mShapeDrawable = new MaterialShapeDrawable();
+        mShapeDrawable.setCornerSize(
+                mContext.getResources().getDimensionPixelSize(R.dimen.feed_badge_radius));
+        mShapeDrawable.setFillColor(
+                ColorStateList.valueOf(SemanticColorUtils.getDefaultTextColorAccent1(context)));
+        mText = "";
+    }
+
+    public void setText(String text) {
+        mText = text;
+        mTextView.setText(text);
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        Rect bounds = getBounds();
+
+        if (bounds.isEmpty() || getAlpha() == 0 || !isVisible()) {
+            return;
+        }
+
+        mShapeDrawable.draw(canvas);
+
+        // Draws the full text with the top left corner at (centerX, centerY-(halfheight of text)).
+        // We define halfheight as the average of ascent and descent to ensure the text does not
+        // appear lopsided even if the font changes.
+        canvas.drawText(mText, 0, mText.length(), bounds.centerX(),
+                bounds.centerY() - ((mPaint.descent() + mPaint.ascent()) / 2), mPaint);
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        mPaint.setAlpha(alpha);
+        invalidateSelf();
+    }
+
+    @Override
+    public void setColorFilter(@Nullable ColorFilter colorFilter) {
+        mPaint.setColorFilter(colorFilter);
+        invalidateSelf();
+    }
+
+    /**
+     * Called when we are attaching the drawable to a new overlay.
+     *
+     * @param anchorBounds the bounding box for the overlay.
+     */
+    @Override
+    public void setBounds(Rect anchorBounds) {
+        // CenterY is the top of the bounding box + a custom vertical offset.
+        int centerY = anchorBounds.top
+                + mContext.getResources().getDimensionPixelSize(R.dimen.feed_badge_voffset);
+        int halfHeight = mContext.getResources().getDimensionPixelSize(R.dimen.feed_badge_radius);
+        // HalfWidth is the radius if no text, or the text width/2.
+        int halfWidth = Math.max(halfHeight, mTextView.getMeasuredWidth() / 2);
+        // CenterX is the right side of the bounding box + radius + offset.
+        int centerX = anchorBounds.right + halfWidth
+                - mContext.getResources().getDimensionPixelSize(R.dimen.feed_badge_hoffset);
+        // The new bounds for the dot + any text to be rendered therein.
+        Rect newBounds = new Rect(centerX - halfWidth, centerY - halfHeight, centerX + halfWidth,
+                centerY + halfHeight);
+        // We don't set bounding box for the textview because
+        // one does not set bounds for views, and it's not part of the drawing.
+        mShapeDrawable.setBounds(newBounds);
+        super.setBounds(newBounds);
+    }
+
+    @Override
+    public int getOpacity() {
+        return mPaint.getAlpha();
+    }
+
+    public void attach(View anchor) {
+        Rect badgeBounds = new Rect();
+        anchor.getDrawingRect(badgeBounds);
+        setBounds(badgeBounds);
+        anchor.getOverlay().add(this);
+        invalidateSelf();
+    }
+
+    public void detach(View anchor) {
+        anchor.getOverlay().remove(this);
+        invalidateSelf();
+    }
+}
diff --git a/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/sections/SectionHeaderView.java b/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/sections/SectionHeaderView.java
index 7340984f..de5af1c 100644
--- a/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/sections/SectionHeaderView.java
+++ b/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/sections/SectionHeaderView.java
@@ -21,8 +21,6 @@
 import androidx.core.content.res.ResourcesCompat;
 import androidx.core.widget.ImageViewCompat;
 
-import com.google.android.material.badge.BadgeDrawable;
-import com.google.android.material.badge.BadgeUtils;
 import com.google.android.material.tabs.TabLayout;
 
 import org.chromium.chrome.browser.feed.FeedUma;
@@ -82,21 +80,17 @@
 
     private class UnreadIndicator implements ViewTreeObserver.OnGlobalLayoutListener {
         private View mAnchor;
-        private BadgeDrawable mBadge;
+        private SectionHeaderBadgeDrawable mNewBadge;
 
         UnreadIndicator(View anchor) {
             mAnchor = anchor;
-            mBadge = BadgeDrawable.createFromResource(
-                    SectionHeaderView.this.getContext(), R.xml.tab_layout_badge);
-
-            mBadge.setVisible(true);
+            mNewBadge = new SectionHeaderBadgeDrawable(SectionHeaderView.this.getContext());
             mAnchor.getViewTreeObserver().addOnGlobalLayoutListener(this);
         }
 
         void destroy() {
             mAnchor.getViewTreeObserver().removeOnGlobalLayoutListener(this);
-            mBadge.setVisible(false);
-            BadgeUtils.detachBadgeDrawable(mBadge, mAnchor);
+            mNewBadge.detach(mAnchor);
         }
 
         @Override
@@ -104,7 +98,7 @@
             // attachBadgeDrawable() will crash if mAnchor has no parent. This can happen after the
             // views are detached from the window.
             if (mAnchor.getParent() != null) {
-                BadgeUtils.attachBadgeDrawable(mBadge, mAnchor);
+                mNewBadge.attach(mAnchor);
             }
         }
     }
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 9b7a3c8d..021f101e 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -2504,6 +2504,11 @@
     "expiry_milestone": 92
   },
   {
+    "name": "enable-media-dynamic-cgroup",
+    "owners": [ "erin.park@intel.com", "youssefesmat" ],
+    "expiry_milestone": 120
+  },
+  {
     "name": "enable-media-foundation-clear",
     "owners": [
       "wicarr@microsoft.com"
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 82d9c61..3dfb2b1b 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -5599,6 +5599,11 @@
 const char kSchedulerConfigurationPerformance[] =
     "Enables Hyper-Threading on relevant CPUs.";
 
+const char kMediaDynamicCgroupName[] = "Media Dynamic Cgroup";
+const char kMediaDynamicCgroupDescription[] =
+    "Dynamic Cgroup allows tasks from media workload to be consolidated on "
+    "limited cpuset";
+
 const char kShowBluetoothDebugLogToggleName[] =
     "Show Bluetooth debug log toggle";
 const char kShowBluetoothDebugLogToggleDescription[] =
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index fed7a9a..43a3ab34 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -3208,6 +3208,9 @@
 extern const char kSchedulerConfigurationConservative[];
 extern const char kSchedulerConfigurationPerformance[];
 
+extern const char kMediaDynamicCgroupName[];
+extern const char kMediaDynamicCgroupDescription[];
+
 extern const char kShowBluetoothDebugLogToggleName[];
 extern const char kShowBluetoothDebugLogToggleDescription[];
 
diff --git a/chrome/browser/history_clusters/history_clusters_bridge.cc b/chrome/browser/history_clusters/history_clusters_bridge.cc
index 13bd5c3f..19ae258 100644
--- a/chrome/browser/history_clusters/history_clusters_bridge.cc
+++ b/chrome/browser/history_clusters/history_clusters_bridge.cc
@@ -177,11 +177,23 @@
   }
   ScopedJavaLocalRef<jclass> cluster_type = base::android::GetClass(
       env, "org/chromium/chrome/browser/history_clusters/HistoryCluster");
+  std::vector<std::u16string> unique_raw_labels;
+  std::vector<int> label_counts;
+  if (query_clusters_state_->query().empty()) {
+    for (const auto& label_entry :
+         query_clusters_state_->raw_label_counts_so_far()) {
+      unique_raw_labels.push_back(label_entry.first);
+      label_counts.push_back(label_entry.second);
+    }
+  }
+
   const ScopedJavaLocalRef<jobject>& j_result =
       Java_HistoryClustersBridge_buildClusterResult(
           env,
           base::android::ToTypedJavaArrayOfObjects(env, j_clusters,
                                                    cluster_type),
+          base::android::ToJavaArrayOfStrings(env, unique_raw_labels),
+          base::android::ToJavaIntArray(env, label_counts),
           base::android::ConvertUTF8ToJavaString(env, query), can_load_more,
           is_continuation);
   base::android::RunObjectCallbackAndroid(j_callback, j_result);
diff --git a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java
index 6f84036e..3e07fb6 100644
--- a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java
+++ b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java
@@ -17,6 +17,7 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.LinkedHashMap;
 import java.util.List;
 
 @JNINamespace("history_clusters")
@@ -69,10 +70,18 @@
     }
 
     @CalledByNative
-    static HistoryClustersResult buildClusterResult(
-            HistoryCluster[] clusters, String query, boolean canLoadMore, boolean isContinuation) {
+    static HistoryClustersResult buildClusterResult(HistoryCluster[] clusters,
+            String[] uniqueRawLabels, int[] labelCounts, String query, boolean canLoadMore,
+            boolean isContinuation) {
+        assert uniqueRawLabels.length == labelCounts.length;
+        LinkedHashMap<String, Integer> labelCountsMap = new LinkedHashMap<>();
+        for (int i = 0; i < uniqueRawLabels.length; i++) {
+            labelCountsMap.put(uniqueRawLabels[i], labelCounts[i]);
+        }
+
         List<HistoryCluster> clustersList = Arrays.asList(clusters);
-        return new HistoryClustersResult(clustersList, query, canLoadMore, isContinuation);
+        return new HistoryClustersResult(
+                clustersList, labelCountsMap, query, canLoadMore, isContinuation);
     }
 
     @CalledByNative
diff --git a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediator.java b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediator.java
index bb13fbf..e3a202f0 100644
--- a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediator.java
+++ b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediator.java
@@ -47,7 +47,9 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
 class HistoryClustersMediator extends RecyclerView.OnScrollListener implements SearchDelegate {
@@ -79,6 +81,7 @@
     private ListItem mClearBrowsingDataItem;
     private QueryState mQueryState = QueryState.forQueryless();
     private final HistoryClustersMetricsLogger mMetricsLogger;
+    private Map<String, PropertyModel> mLabelToModelMap = new LinkedHashMap<>();
 
     /**
      * Create a new HistoryClustersMediator.
@@ -134,7 +137,7 @@
     // SearchDelegate implementation.
     @Override
     public void onSearchTextChanged(String query) {
-        mModelList.clear();
+        resetModel();
         startQuery(query);
     }
 
@@ -166,7 +169,7 @@
         mQueryState = queryState;
         mToolbarModel.set(HistoryClustersToolbarProperties.QUERY_STATE, queryState);
         if (!queryState.isSearching()) {
-            mModelList.clear();
+            resetModel();
             startQuery(mQueryState.getQuery());
         }
     }
@@ -267,7 +270,7 @@
         }
         mDelegate.removeMarkedItems();
 
-        mModelList.clear();
+        resetModel();
         startQuery(mQueryState.getQuery());
     }
 
@@ -282,6 +285,39 @@
         boolean isQueryLess = !mQueryState.isSearching();
         if (isQueryLess) {
             ensureHeaders();
+            for (Map.Entry<String, Integer> entry : result.getLabelCounts().entrySet()) {
+                // Check if label exists in the model already
+                // If not, create a new entry
+                String rawLabel = entry.getKey();
+                PropertyModel existingModel = mLabelToModelMap.get(rawLabel);
+                if (existingModel == null) {
+                    existingModel = new PropertyModel(HistoryClustersItemProperties.ALL_KEYS);
+                    mLabelToModelMap.put(rawLabel, existingModel);
+                    Drawable journeysDrawable =
+                            AppCompatResources.getDrawable(mContext, R.drawable.ic_journeys);
+                    existingModel.set(
+                            HistoryClustersItemProperties.ICON_DRAWABLE, journeysDrawable);
+                    existingModel.set(HistoryClustersItemProperties.DIVIDER_VISIBLE, true);
+                    existingModel.set(HistoryClustersItemProperties.TITLE,
+                            getQuotedLabelFromRawLabel(rawLabel, result.getClusters()));
+                    ListItem clusterItem = new ListItem(ItemType.CLUSTER, existingModel);
+                    mModelList.add(clusterItem);
+                    existingModel.set(HistoryClustersItemProperties.CLICK_HANDLER,
+                            (v)
+                                    -> setQueryState(QueryState.forQuery(
+                                            rawLabel, mDelegate.getSearchEmptyString())));
+                    existingModel.set(HistoryClustersItemProperties.END_BUTTON_DRAWABLE, null);
+                }
+                existingModel.set(HistoryClustersItemProperties.LABEL,
+                        mResources.getQuantityString(R.plurals.history_clusters_n_matches,
+                                entry.getValue(), entry.getValue()));
+            }
+
+            if (result.canLoadMore() && !result.isContinuation()) {
+                continueQuery("");
+            }
+
+            return;
         }
 
         for (HistoryCluster cluster : result.getClusters()) {
@@ -294,15 +330,6 @@
             clusterModel.set(HistoryClustersItemProperties.DIVIDER_VISIBLE, isQueryLess);
             ListItem clusterItem = new ListItem(ItemType.CLUSTER, clusterModel);
             mModelList.add(clusterItem);
-            if (isQueryLess) {
-                clusterModel.set(HistoryClustersItemProperties.CLICK_HANDLER,
-                        (v)
-                                -> setQueryState(QueryState.forQuery(
-                                        cluster.getRawLabel(), mDelegate.getSearchEmptyString())));
-                clusterModel.set(HistoryClustersItemProperties.END_BUTTON_DRAWABLE, null);
-                clusterModel.set(HistoryClustersItemProperties.LABEL, null);
-                continue;
-            }
 
             List<ListItem> visitsAndRelatedSearches =
                     new ArrayList<>(cluster.getVisits().size() + 1);
@@ -365,6 +392,22 @@
         }
     }
 
+    private void resetModel() {
+        mModelList.clear();
+        mLabelToModelMap.clear();
+    }
+
+    private String getQuotedLabelFromRawLabel(String rawLabel, List<HistoryCluster> clusters) {
+        for (HistoryCluster cluster : clusters) {
+            if (cluster.getRawLabel().equals(rawLabel)) {
+                return cluster.getLabel();
+            }
+        }
+
+        // This shouldn't happen, but the unquoted label is a graceful fallback in case it does.
+        return rawLabel;
+    }
+
     private void ensureHeaders() {
         if (mQueryState.isSearching()) {
             return;
diff --git a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersResult.java b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersResult.java
index 06daa27..4a76c13 100644
--- a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersResult.java
+++ b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersResult.java
@@ -7,7 +7,9 @@
 import androidx.annotation.VisibleForTesting;
 
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
 /**
@@ -16,18 +18,25 @@
  */
 public class HistoryClustersResult {
     private final List<HistoryCluster> mClusters;
+    private final LinkedHashMap<String, Integer> mLabelCounts;
     private final String mQuery;
     private final boolean mCanLoadMore;
     private final boolean mIsContinuation;
 
     /** Create a new result with no clusters and an empty query. */
     public static HistoryClustersResult emptyResult() {
-        return new HistoryClustersResult(Collections.EMPTY_LIST, "", false, false);
+        return new HistoryClustersResult(
+                Collections.EMPTY_LIST, new LinkedHashMap<>(), "", false, false);
     }
 
-    HistoryClustersResult(List<HistoryCluster> clusters, String query, boolean canLoadMore,
-            boolean isContinuation) {
+    /**
+     * Constructs a new HistoryClustersResult. {@code labelCounts} must be a LinkedHashMap so that
+     * order is stable and preserved.
+     */
+    HistoryClustersResult(List<HistoryCluster> clusters, LinkedHashMap<String, Integer> labelCounts,
+            String query, boolean canLoadMore, boolean isContinuation) {
         mClusters = clusters;
+        mLabelCounts = labelCounts;
         mQuery = query;
         mCanLoadMore = canLoadMore;
         mIsContinuation = isContinuation;
@@ -44,4 +53,12 @@
     public boolean canLoadMore() {
         return mCanLoadMore;
     }
+
+    public Map<String, Integer> getLabelCounts() {
+        return mLabelCounts;
+    }
+
+    public boolean isContinuation() {
+        return mIsContinuation;
+    }
 }
diff --git a/chrome/browser/infobars/infobars_browsertest.cc b/chrome/browser/infobars/infobars_browsertest.cc
index 00649b9..e0de115 100644
--- a/chrome/browser/infobars/infobars_browsertest.cc
+++ b/chrome/browser/infobars/infobars_browsertest.cc
@@ -50,6 +50,7 @@
 #include "extensions/browser/sandboxed_unpacker.h"
 #include "extensions/browser/test_extension_registry_observer.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
+#include "ppapi/buildflags/buildflags.h"
 #include "sandbox/policy/switches.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "ui/base/l10n/l10n_util.h"
diff --git a/chrome/browser/metrics/power/power_metrics_reporter.cc b/chrome/browser/metrics/power/power_metrics_reporter.cc
index 16e4352..4b461cb 100644
--- a/chrome/browser/metrics/power/power_metrics_reporter.cc
+++ b/chrome/browser/metrics/power/power_metrics_reporter.cc
@@ -262,10 +262,19 @@
   auto long_interval_data =
       long_usage_scenario_data_store_->ResetIntervalData();
 
-  // Report histograms.
-  auto long_interval_suffixes = GetLongIntervalSuffixes(long_interval_data);
+  // Get scenario data.
+  const auto long_interval_scenario_params =
+      GetLongIntervalScenario(long_interval_data);
+  // Histograms are recorded without suffix and with a scenario-specific
+  // suffix.
+  const std::vector<const char*> long_interval_suffixes{
+      "", long_interval_scenario_params.histogram_suffix};
+
+  // Report process metrics histograms.
   ReportAggregatedProcessMetricsHistograms(aggregated_process_metrics,
                                            long_interval_suffixes);
+  base::UmaHistogramEnumeration("PerformanceMonitor.UsageScenario.LongInterval",
+                                long_interval_scenario_params.scenario);
 
 #if HAS_BATTERY_LEVEL_PROVIDER_IMPL()
   // Report UKMs.
@@ -309,6 +318,10 @@
     const ScenarioParams short_interval_scenario_params =
         GetShortIntervalScenarioParams(short_interval_data, long_interval_data);
 
+    base::UmaHistogramEnumeration(
+        "PerformanceMonitor.UsageScenario.ShortInterval",
+        short_interval_scenario_params.scenario);
+
     ReportShortIntervalHistograms(
         short_interval_scenario_params.histogram_suffix,
         short_interval_resource_usage_rate.value());
diff --git a/chrome/browser/metrics/power/usage_scenario.cc b/chrome/browser/metrics/power/usage_scenario.cc
index d4eb2b3..a80602f 100644
--- a/chrome/browser/metrics/power/usage_scenario.cc
+++ b/chrome/browser/metrics/power/usage_scenario.cc
@@ -10,87 +10,100 @@
 // Canary 7 day aggregation ending on March 15th 2022 from "PerformanceMonitor
 // .ResourceCoalition.CPUTime2_10sec.*"
 const ScenarioParams kVideoCaptureParams = {
+    .scenario = Scenario::kVideoCapture,
     .histogram_suffix = ".VideoCapture",
     .short_interval_cpu_threshold = 1.8949,
     .trace_event_title = "High CPU - Video Capture",
 };
 
 const ScenarioParams kFullscreenVideoParams = {
+    .scenario = Scenario::kFullscreenVideo,
     .histogram_suffix = ".FullscreenVideo",
     .short_interval_cpu_threshold = 1.4513,
     .trace_event_title = "High CPU - Fullscreen Video",
 };
 
 const ScenarioParams kEmbeddedVideoNoNavigationParams = {
+    .scenario = Scenario::kEmbeddedVideoNoNavigation,
     .histogram_suffix = ".EmbeddedVideo_NoNavigation",
     .short_interval_cpu_threshold = 1.5436,
     .trace_event_title = "High CPU - Embedded Video No Navigation",
 };
 
 const ScenarioParams kEmbeddedVideoWithNavigationParams = {
+    .scenario = Scenario::kEmbeddedVideoWithNavigation,
     .histogram_suffix = ".EmbeddedVideo_WithNavigation",
     .short_interval_cpu_threshold = 1.9999,
     .trace_event_title = "High CPU - Embedded Video With Navigation",
 };
 
 const ScenarioParams kAudioParams = {
+    .scenario = Scenario::kAudio,
     .histogram_suffix = ".Audio",
     .short_interval_cpu_threshold = 1.5110,
     .trace_event_title = "High CPU - Audio",
 };
 
 const ScenarioParams kNavigationParams = {
+    .scenario = Scenario::kNavigation,
     .histogram_suffix = ".Navigation",
     .short_interval_cpu_threshold = 1.9999,
     .trace_event_title = "High CPU - Navigation",
 };
 
 const ScenarioParams kInteractionParams = {
+    .scenario = Scenario::kInteraction,
     .histogram_suffix = ".Interaction",
     .short_interval_cpu_threshold = 1.2221,
     .trace_event_title = "High CPU - Interaction",
 };
 
 const ScenarioParams kPassiveParams = {
+    .scenario = Scenario::kPassive,
     .histogram_suffix = ".Passive",
     .short_interval_cpu_threshold = 0.4736,
     .trace_event_title = "High CPU - Passive",
 };
 
-#if BUILDFLAG(IS_MAC)
 const ScenarioParams kAllTabsHiddenNoVideoCaptureOrAudioParams = {
+    .scenario = Scenario::kAllTabsHiddenNoVideoCaptureOrAudio,
     .histogram_suffix = ".AllTabsHidden_NoVideoCaptureOrAudio",
     .short_interval_cpu_threshold = 0.2095,
     .trace_event_title =
         "High CPU - All Tabs Hidden, No Video Capture or Audio",
 };
 
-const ScenarioParams kAllTabsHiddenNoVideoCaptureOrAudioRecentParams = {
-    .histogram_suffix = ".AllTabsHidden_NoVideoCaptureOrAudio_Recent",
-    .short_interval_cpu_threshold = 0.3302,
-    .trace_event_title =
-        "High CPU - All Tabs Hidden, No Video Capture or Audio (Recent)",
-};
-
-const ScenarioParams kAllTabsHiddenNoAudioParams = {
+const ScenarioParams kAllTabsHiddenAudioParams = {
+    .scenario = Scenario::kAllTabsHiddenAudio,
     .histogram_suffix = ".AllTabsHidden_Audio",
     .short_interval_cpu_threshold = 0.7036,
-    .trace_event_title = "High CPU - All Tabs Hidden, No Audio",
+    .trace_event_title = "High CPU - All Tabs Hidden, Audio",
 };
 
-const ScenarioParams kAllTabsHiddenNoVideoCapture = {
+const ScenarioParams kAllTabsHiddenVideoCaptureParams = {
+    .scenario = Scenario::kAllTabsHiddenVideoCapture,
     .histogram_suffix = ".AllTabsHidden_VideoCapture",
     .short_interval_cpu_threshold = 0.8679,
     .trace_event_title = "High CPU - All Tabs Hidden, Video Capture",
 };
 
-const ScenarioParams kAllTabsHiddenZeroWindowParams = {
+const ScenarioParams kZeroWindowParams = {
+    .scenario = Scenario::kZeroWindow,
     .histogram_suffix = ".ZeroWindow",
     .short_interval_cpu_threshold = 0.0500,
     .trace_event_title = "High CPU - Zero Window",
 };
 
+#if BUILDFLAG(IS_MAC)
+const ScenarioParams kAllTabsHiddenNoVideoCaptureOrAudioRecentParams = {
+    .scenario = Scenario::kAllTabsHiddenNoVideoCaptureOrAudioRecent,
+    .histogram_suffix = ".AllTabsHidden_NoVideoCaptureOrAudio_Recent",
+    .short_interval_cpu_threshold = 0.3302,
+    .trace_event_title =
+        "High CPU - All Tabs Hidden, No Video Capture or Audio (Recent)",
+};
 const ScenarioParams kAllTabsHiddenZeroWindowRecentParams = {
+    .scenario = Scenario::kZeroWindowRecent,
     .histogram_suffix = ".ZeroWindow_Recent",
     .short_interval_cpu_threshold = 0.0745,
     .trace_event_title = "High CPU - Zero Window (Recent)",
@@ -124,33 +137,22 @@
     return kInteractionParams;
   return kPassiveParams;
 }
+}  // namespace
 
-// Helper function for GetLongIntervalSuffixes().
-const char* GetLongIntervalScenarioSuffix(
+ScenarioParams GetLongIntervalScenario(
     const UsageScenarioDataStore::IntervalData& interval_data) {
   // The order of the conditions is important. See the full description of each
   // scenario in the histograms.xml file.
   if (interval_data.max_tab_count == 0)
-    return ".ZeroWindow";
+    return kZeroWindowParams;
   if (interval_data.max_visible_window_count == 0) {
     if (!interval_data.time_capturing_video.is_zero())
-      return ".AllTabsHidden_VideoCapture";
+      return kAllTabsHiddenVideoCaptureParams;
     if (!interval_data.time_playing_audio.is_zero())
-      return ".AllTabsHidden_Audio";
-    return ".AllTabsHidden_NoVideoCaptureOrAudio";
+      return kAllTabsHiddenAudioParams;
+    return kAllTabsHiddenNoVideoCaptureOrAudioParams;
   }
-  return GetScenarioParamsWithVisibleWindow(interval_data).histogram_suffix;
-}
-
-}  // namespace
-
-// Returns suffixes to use for histograms related to a long interval described
-// by `interval_data`.
-std::vector<const char*> GetLongIntervalSuffixes(
-    const UsageScenarioDataStore::IntervalData& interval_data) {
-  // Histograms are recorded without suffix and with a scenario-specific
-  // suffix.
-  return {"", GetLongIntervalScenarioSuffix(interval_data)};
+  return GetScenarioParamsWithVisibleWindow(interval_data);
 }
 
 #if BUILDFLAG(IS_MAC)
@@ -162,13 +164,13 @@
   if (short_interval_data.max_tab_count == 0) {
     if (pre_interval_data.max_tab_count != 0)
       return kAllTabsHiddenZeroWindowRecentParams;
-    return kAllTabsHiddenZeroWindowParams;
+    return kZeroWindowParams;
   }
   if (short_interval_data.max_visible_window_count == 0) {
     if (!short_interval_data.time_capturing_video.is_zero())
-      return kAllTabsHiddenNoVideoCapture;
+      return kAllTabsHiddenVideoCaptureParams;
     if (!short_interval_data.time_playing_audio.is_zero())
-      return kAllTabsHiddenNoAudioParams;
+      return kAllTabsHiddenAudioParams;
     if (pre_interval_data.max_visible_window_count != 0 ||
         !pre_interval_data.time_capturing_video.is_zero() ||
         !pre_interval_data.time_playing_audio.is_zero()) {
diff --git a/chrome/browser/metrics/power/usage_scenario.h b/chrome/browser/metrics/power/usage_scenario.h
index 86e50bc..65a2534d 100644
--- a/chrome/browser/metrics/power/usage_scenario.h
+++ b/chrome/browser/metrics/power/usage_scenario.h
@@ -5,23 +5,40 @@
 #ifndef CHROME_BROWSER_METRICS_POWER_USAGE_SCENARIO_H_
 #define CHROME_BROWSER_METRICS_POWER_USAGE_SCENARIO_H_
 
-#include <vector>
-
 #include "chrome/browser/metrics/usage_scenario/usage_scenario_data_store.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
+// Describes the different usaage scenarios in Chrome.
+enum class Scenario {
+  kAllTabsHiddenAudio = 1,
+  kAllTabsHiddenNoVideoCaptureOrAudio = 2,
+  kAllTabsHiddenNoVideoCaptureOrAudioRecent = 3,  // Short scenario only.
+  kAllTabsHiddenVideoCapture = 4,
+  kAudio = 5,
+  kEmbeddedVideoNoNavigation = 6,
+  kEmbeddedVideoWithNavigation = 7,
+  kFullscreenVideo = 8,
+  kInteraction = 9,
+  kNavigation = 10,
+  kPassive = 11,
+  kVideoCapture = 12,
+  kZeroWindow = 13,
+  kZeroWindowRecent = 14,  // Short scenario only.
+  kMaxValue = kZeroWindowRecent
+};
+
 // Contains data to determine when and how to generate histograms and trace
 // events for a usage scenario.
 struct ScenarioParams {
+  Scenario scenario;
   const char* histogram_suffix;
   // CPU usage threshold to emit a "high CPU" trace event.
   double short_interval_cpu_threshold;
   const char* trace_event_title;
 };
 
-// Returns the suffixes to use for histograms and UKMs related to a long
-// interval described by `interval_data`.
-std::vector<const char*> GetLongIntervalSuffixes(
+// Returns the scenario params associated with `interval_data`.
+ScenarioParams GetLongIntervalScenario(
     const UsageScenarioDataStore::IntervalData& interval_data);
 
 #if BUILDFLAG(IS_MAC)
diff --git a/chrome/browser/metrics/power/usage_scenario_unittest.cc b/chrome/browser/metrics/power/usage_scenario_unittest.cc
index 6aabe75..117c28c1 100644
--- a/chrome/browser/metrics/power/usage_scenario_unittest.cc
+++ b/chrome/browser/metrics/power/usage_scenario_unittest.cc
@@ -10,15 +10,15 @@
 using testing::ElementsAre;
 using testing::StrEq;
 
-TEST(UsageScenarioTest, GetLongIntervalSuffixes_ZeroWindow) {
+TEST(UsageScenarioTest, GetLongIntervalScenario_ZeroWindow) {
   UsageScenarioDataStore::IntervalData interval_data;
   interval_data.max_tab_count = 0;
 
-  EXPECT_THAT(GetLongIntervalSuffixes(interval_data),
-              ElementsAre(StrEq(""), StrEq(".ZeroWindow")));
+  EXPECT_EQ(GetLongIntervalScenario(interval_data).scenario,
+            Scenario::kZeroWindow);
 }
 
-TEST(UsageScenarioTest, GetLongIntervalSuffixes_AllTabsHidden_VideoCapture) {
+TEST(UsageScenarioTest, GetLongIntervalScenario_AllTabsHidden_VideoCapture) {
   UsageScenarioDataStore::IntervalData interval_data;
   interval_data.max_tab_count = 1;
   interval_data.max_visible_window_count = 0;
@@ -31,11 +31,11 @@
   interval_data.top_level_navigation_count = 1;
   interval_data.user_interaction_count = 1;
 
-  EXPECT_THAT(GetLongIntervalSuffixes(interval_data),
-              ElementsAre(StrEq(""), StrEq(".AllTabsHidden_VideoCapture")));
+  EXPECT_EQ(GetLongIntervalScenario(interval_data).scenario,
+            Scenario::kAllTabsHiddenVideoCapture);
 }
 
-TEST(UsageScenarioTest, GetLongIntervalSuffixes_AllTabsHidden_Audio) {
+TEST(UsageScenarioTest, GetLongIntervalScenario_AllTabsHidden_Audio) {
   UsageScenarioDataStore::IntervalData interval_data;
   interval_data.max_tab_count = 1;
   interval_data.max_visible_window_count = 0;
@@ -48,12 +48,12 @@
   interval_data.top_level_navigation_count = 1;
   interval_data.user_interaction_count = 1;
 
-  EXPECT_THAT(GetLongIntervalSuffixes(interval_data),
-              ElementsAre(StrEq(""), StrEq(".AllTabsHidden_Audio")));
+  EXPECT_EQ(GetLongIntervalScenario(interval_data).scenario,
+            Scenario::kAllTabsHiddenAudio);
 }
 
 TEST(UsageScenarioTest,
-     GetLongIntervalSuffixes_AllTabsHidden_NoVideoCaptureOrAudio) {
+     GetLongIntervalScenario_AllTabsHidden_NoVideoCaptureOrAudio) {
   UsageScenarioDataStore::IntervalData interval_data;
   interval_data.max_tab_count = 1;
   interval_data.max_visible_window_count = 0;
@@ -66,12 +66,11 @@
   interval_data.top_level_navigation_count = 1;
   interval_data.user_interaction_count = 1;
 
-  EXPECT_THAT(
-      GetLongIntervalSuffixes(interval_data),
-      ElementsAre(StrEq(""), StrEq(".AllTabsHidden_NoVideoCaptureOrAudio")));
+  EXPECT_EQ(GetLongIntervalScenario(interval_data).scenario,
+            Scenario::kAllTabsHiddenNoVideoCaptureOrAudio);
 }
 
-TEST(UsageScenarioTest, GetLongIntervalSuffixes_VideoCapture) {
+TEST(UsageScenarioTest, GetLongIntervalScenario_VideoCapture) {
   UsageScenarioDataStore::IntervalData interval_data;
   interval_data.max_tab_count = 1;
   interval_data.max_visible_window_count = 1;
@@ -84,11 +83,11 @@
   interval_data.top_level_navigation_count = 1;
   interval_data.user_interaction_count = 1;
 
-  EXPECT_THAT(GetLongIntervalSuffixes(interval_data),
-              ElementsAre(StrEq(""), StrEq(".VideoCapture")));
+  EXPECT_EQ(GetLongIntervalScenario(interval_data).scenario,
+            Scenario::kVideoCapture);
 }
 
-TEST(UsageScenarioTest, GetLongIntervalSuffixes_FullscreenVideo) {
+TEST(UsageScenarioTest, GetLongIntervalScenario_FullscreenVideo) {
   UsageScenarioDataStore::IntervalData interval_data;
   interval_data.max_tab_count = 1;
   interval_data.max_visible_window_count = 1;
@@ -101,11 +100,11 @@
   interval_data.top_level_navigation_count = 1;
   interval_data.user_interaction_count = 1;
 
-  EXPECT_THAT(GetLongIntervalSuffixes(interval_data),
-              ElementsAre(StrEq(""), StrEq(".FullscreenVideo")));
+  EXPECT_EQ(GetLongIntervalScenario(interval_data).scenario,
+            Scenario::kFullscreenVideo);
 }
 
-TEST(UsageScenarioTest, GetLongIntervalSuffixes_EmbeddedVideo_NoNavigation) {
+TEST(UsageScenarioTest, GetLongIntervalScenario_EmbeddedVideo_NoNavigation) {
   UsageScenarioDataStore::IntervalData interval_data;
   interval_data.max_tab_count = 1;
   interval_data.max_visible_window_count = 1;
@@ -118,11 +117,11 @@
   interval_data.time_playing_audio = base::Seconds(1);
   interval_data.user_interaction_count = 1;
 
-  EXPECT_THAT(GetLongIntervalSuffixes(interval_data),
-              ElementsAre(StrEq(""), StrEq(".EmbeddedVideo_NoNavigation")));
+  EXPECT_EQ(GetLongIntervalScenario(interval_data).scenario,
+            Scenario::kEmbeddedVideoNoNavigation);
 }
 
-TEST(UsageScenarioTest, GetLongIntervalSuffixes_EmbeddedVideo_WithNavigation) {
+TEST(UsageScenarioTest, GetLongIntervalScenario_EmbeddedVideo_WithNavigation) {
   UsageScenarioDataStore::IntervalData interval_data;
   interval_data.max_tab_count = 1;
   interval_data.max_visible_window_count = 1;
@@ -135,11 +134,11 @@
   interval_data.time_playing_audio = base::Seconds(1);
   interval_data.user_interaction_count = 1;
 
-  EXPECT_THAT(GetLongIntervalSuffixes(interval_data),
-              ElementsAre(StrEq(""), StrEq(".EmbeddedVideo_WithNavigation")));
+  EXPECT_EQ(GetLongIntervalScenario(interval_data).scenario,
+            Scenario::kEmbeddedVideoWithNavigation);
 }
 
-TEST(UsageScenarioTest, GetLongIntervalSuffixes_Audio) {
+TEST(UsageScenarioTest, GetLongIntervalScenario_Audio) {
   UsageScenarioDataStore::IntervalData interval_data;
   interval_data.max_tab_count = 1;
   interval_data.max_visible_window_count = 1;
@@ -152,11 +151,10 @@
   interval_data.user_interaction_count = 1;
   interval_data.top_level_navigation_count = 1;
 
-  EXPECT_THAT(GetLongIntervalSuffixes(interval_data),
-              ElementsAre(StrEq(""), StrEq(".Audio")));
+  EXPECT_EQ(GetLongIntervalScenario(interval_data).scenario, Scenario::kAudio);
 }
 
-TEST(UsageScenarioTest, GetLongIntervalSuffixes_Navigation) {
+TEST(UsageScenarioTest, GetLongIntervalScenario_Navigation) {
   UsageScenarioDataStore::IntervalData interval_data;
   interval_data.max_tab_count = 1;
   interval_data.max_visible_window_count = 1;
@@ -169,11 +167,11 @@
   // Values below should be ignored.
   interval_data.user_interaction_count = 1;
 
-  EXPECT_THAT(GetLongIntervalSuffixes(interval_data),
-              ElementsAre(StrEq(""), StrEq(".Navigation")));
+  EXPECT_EQ(GetLongIntervalScenario(interval_data).scenario,
+            Scenario::kNavigation);
 }
 
-TEST(UsageScenarioTest, GetLongIntervalSuffixes_Interaction) {
+TEST(UsageScenarioTest, GetLongIntervalScenario_Interaction) {
   UsageScenarioDataStore::IntervalData interval_data;
   interval_data.max_tab_count = 1;
   interval_data.max_visible_window_count = 1;
@@ -185,11 +183,11 @@
   interval_data.top_level_navigation_count = 0;
   interval_data.user_interaction_count = 1;
 
-  EXPECT_THAT(GetLongIntervalSuffixes(interval_data),
-              ElementsAre(StrEq(""), StrEq(".Interaction")));
+  EXPECT_EQ(GetLongIntervalScenario(interval_data).scenario,
+            Scenario::kInteraction);
 }
 
-TEST(UsageScenarioTest, GetLongIntervalSuffixes_Passive) {
+TEST(UsageScenarioTest, GetLongIntervalScenario_Passive) {
   UsageScenarioDataStore::IntervalData interval_data;
   interval_data.max_tab_count = 1;
   interval_data.max_visible_window_count = 1;
@@ -201,8 +199,8 @@
   interval_data.top_level_navigation_count = 0;
   interval_data.user_interaction_count = 0;
 
-  EXPECT_THAT(GetLongIntervalSuffixes(interval_data),
-              ElementsAre(StrEq(""), StrEq(".Passive")));
+  EXPECT_EQ(GetLongIntervalScenario(interval_data).scenario,
+            Scenario::kPassive);
 }
 
 #if BUILDFLAG(IS_MAC)
diff --git a/chrome/browser/metrics/variations/variations_safe_mode_end_to_end_browsertest.cc b/chrome/browser/metrics/variations/variations_safe_mode_end_to_end_browsertest.cc
index b940c3c9..85998f2 100644
--- a/chrome/browser/metrics/variations/variations_safe_mode_end_to_end_browsertest.cc
+++ b/chrome/browser/metrics/variations/variations_safe_mode_end_to_end_browsertest.cc
@@ -185,7 +185,13 @@
   base::FilePath local_state_file_;
 };
 
-TEST_F(VariationsSafeModeEndToEndBrowserTest, ExtendedSafeModeEndToEnd) {
+// TODO(crbug.com/1344852): test is flaky on Mac.
+#if BUILDFLAG(IS_MAC)
+#define MAYBE_ExtendedSafeModeEndToEnd DISABLED_ExtendedSafeModeEndToEnd
+#else
+#define MAYBE_ExtendedSafeModeEndToEnd ExtendedSafeModeEndToEnd
+#endif
+TEST_F(VariationsSafeModeEndToEndBrowserTest, MAYBE_ExtendedSafeModeEndToEnd) {
   // Reuse the browser_tests binary (i.e., that this test code is in), to
   // manually run the sub-test.
   base::CommandLine sub_test =
diff --git a/chrome/browser/net/private_network_access_browsertest.cc b/chrome/browser/net/private_network_access_browsertest.cc
index 3c13560..c2415f3d 100644
--- a/chrome/browser/net/private_network_access_browsertest.cc
+++ b/chrome/browser/net/private_network_access_browsertest.cc
@@ -1347,8 +1347,8 @@
 
   static constexpr char kPageFile[] = "page.html";
 
-  std::vector<base::Value> resources;
-  resources.emplace_back(std::string(kPageFile));
+  base::Value::List resources;
+  resources.Append(kPageFile);
   constexpr char kContents[] = R"(
   <html>
     <head>
diff --git a/chrome/browser/password_check/android/password_check_manager.cc b/chrome/browser/password_check/android/password_check_manager.cc
index 492a266e..d0cdbdc 100644
--- a/chrome/browser/password_check/android/password_check_manager.cc
+++ b/chrome/browser/password_check/android/password_check_manager.cc
@@ -155,7 +155,7 @@
                            credential.signon_realm, is_using_account_store),
       &saved_passwords_presenter_,
       base::BindOnce(&PasswordCheckManager::OnEditUIDismissed,
-                     base::Unretained(this)),
+                     weak_ptr_factory_.GetWeakPtr()),
       context, settings_launcher);
 }
 
@@ -359,7 +359,7 @@
   }
   ResetPrecondition(kScriptsCachePrewarmed);
   password_script_fetcher_->RefreshScriptsIfNecessary(base::BindOnce(
-      &PasswordCheckManager::OnScriptsFetched, base::Unretained(this)));
+      &PasswordCheckManager::OnScriptsFetched, weak_ptr_factory_.GetWeakPtr()));
 }
 
 void PasswordCheckManager::OnScriptsFetched() {
diff --git a/chrome/browser/password_check/android/password_check_manager.h b/chrome/browser/password_check/android/password_check_manager.h
index eb0b096..57862f1ab 100644
--- a/chrome/browser/password_check/android/password_check_manager.h
+++ b/chrome/browser/password_check/android/password_check_manager.h
@@ -7,6 +7,7 @@
 
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
+#include "base/memory/weak_ptr.h"
 #include "base/scoped_observation.h"
 #include "base/strings/string_piece_forward.h"
 #include "chrome/browser/password_check/android/password_check_ui_status.h"
@@ -285,6 +286,9 @@
       password_manager::BulkLeakCheckServiceInterface,
       password_manager::BulkLeakCheckServiceInterface::Observer>
       observed_bulk_leak_check_service_{this};
+
+  // Weak pointer factory for callback binding safety.
+  base::WeakPtrFactory<PasswordCheckManager> weak_ptr_factory_{this};
 };
 
 #endif  // CHROME_BROWSER_PASSWORD_CHECK_ANDROID_PASSWORD_CHECK_MANAGER_H_
diff --git a/chrome/browser/policy/chrome_browser_cloud_management_register_watcher.cc b/chrome/browser/policy/chrome_browser_cloud_management_register_watcher.cc
index e9bb87f..91f146f 100644
--- a/chrome/browser/policy/chrome_browser_cloud_management_register_watcher.cc
+++ b/chrome/browser/policy/chrome_browser_cloud_management_register_watcher.cc
@@ -45,6 +45,15 @@
   if (token_storage->RetrieveDMToken().is_valid())
     return RegisterResult::kEnrollmentSuccessBeforeDialogDisplayed;
 
+  // Unretained(this) is safe because `run_loop_` runs in the current scope
+  // and is not quit until after `callback` executes. Without the run loop it
+  // would NOT be safe, because `this` is deleted in PostMainMessageLoopRun. If
+  // execution returned from the current scope, potentially the browser could
+  // start shutting down and exit the main thread, destroying `this` with
+  // `callback` scheduled to run on the ThreadPool. (Which sequence runs
+  // `callback` is an implementation detail of EnterpriseStartupDialog that
+  // ChromeBrowserCloudManagementRegisterWatcher should not make assumptions
+  // about.)
   EnterpriseStartupDialog::DialogResultCallback callback = base::BindOnce(
       &ChromeBrowserCloudManagementRegisterWatcher::OnDialogClosed,
       base::Unretained(this));
diff --git a/chrome/browser/printing/printing_init.cc b/chrome/browser/printing/printing_init.cc
index f357a062..8255983 100644
--- a/chrome/browser/printing/printing_init.cc
+++ b/chrome/browser/printing/printing_init.cc
@@ -6,8 +6,8 @@
 
 #include "chrome/browser/headless/headless_mode_util.h"
 #include "components/embedder_support/user_agent_utils.h"
+#include "components/printing/browser/headless/headless_print_manager.h"
 #include "components/printing/browser/print_manager_utils.h"
-#include "components/printing/browser/print_to_pdf/pdf_print_manager.h"
 #include "content/public/browser/web_contents.h"
 #include "printing/buildflags/buildflags.h"
 
@@ -22,7 +22,7 @@
 
 void InitializePrinting(content::WebContents* web_contents) {
   if (headless::IsChromeNativeHeadless()) {
-    print_to_pdf::PdfPrintManager::CreateForWebContents(web_contents);
+    headless::HeadlessPrintManager::CreateForWebContents(web_contents);
     return;
   }
 #if BUILDFLAG(ENABLE_PRINT_PREVIEW)
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/BUILD.gn b/chrome/browser/resources/chromeos/accessibility/accessibility_common/BUILD.gn
index 6a92bbc..9cb4ce47 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/BUILD.gn
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/BUILD.gn
@@ -45,6 +45,7 @@
     "dictation/editing_util.js",
     "dictation/focus_handler.js",
     "dictation/input_controller.js",
+    "dictation/locale_info.js",
     "dictation/macros/delete_prev_sent_macro.js",
     "dictation/macros/hidden_macro_manager.js",
     "dictation/macros/input_text_view_macro.js",
@@ -136,6 +137,7 @@
     "dictation/dictation_ui_test.js",
     "dictation/editing_util_test.js",
     "dictation/focus_handler_test.js",
+    "dictation/locale_info_test.js",
     "dictation/macros/dictation_macros_test.js",
     "dictation/parse/dictation_parse_test.js",
     "magnifier/magnifier_test.js",
@@ -214,6 +216,7 @@
     ":dictation_editing_util",
     ":dictation_focus_handler",
     ":dictation_input_controller",
+    ":dictation_locale_info",
     ":dictation_metrics",
     ":dictation_ui_controller",
     ":speech_parsing",
@@ -230,6 +233,7 @@
   deps = [
     ":dictation_editing_util",
     ":dictation_focus_handler",
+    ":dictation_locale_info",
   ]
   externs_list = [
     "$externs_path/input_method_private.js",
@@ -240,6 +244,7 @@
 
 js_library("dictation_ui_controller") {
   sources = [ "dictation/ui_controller.js" ]
+  deps = [ ":dictation_locale_info" ]
   externs_list = [ "$externs_path/accessibility_private.js" ]
 }
 
@@ -261,6 +266,7 @@
   ]
   deps = [
     ":dictation_input_controller",
+    ":dictation_locale_info",
     "../common:event_generator",
     "../common:key_code",
   ]
@@ -281,6 +287,7 @@
   }
   deps = [
     ":dictation_input_controller",
+    ":dictation_locale_info",
     ":dictation_macros",
   ]
 }
@@ -305,3 +312,7 @@
 js_library("dictation_editing_util") {
   sources = [ "dictation/editing_util.js" ]
 }
+
+js_library("dictation_locale_info") {
+  sources = [ "dictation/locale_info.js" ]
+}
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation.js
index 3c851a7..4cbb4ee 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation.js
@@ -4,6 +4,7 @@
 
 import {FocusHandler} from './focus_handler.js';
 import {InputController} from './input_controller.js';
+import {LocaleInfo} from './locale_info.js';
 import {HiddenMacroManager} from './macros/hidden_macro_manager.js';
 import {Macro} from './macros/macro.js';
 import {MacroName} from './macros/macro_names.js';
@@ -34,9 +35,6 @@
     /** @private {HiddenMacroManager} */
     this.hiddenMacroManager_ = null;
 
-    /** @private {string} */
-    this.localePref_ = '';
-
     /**
      * Whether or not Dictation is active.
      * @private {boolean}
@@ -86,9 +84,7 @@
         () => this.stopDictation_(/*notify=*/ true), this.focusHandler_);
     this.uiController_ = new UIController();
     this.speechParser_ = new SpeechParser(this.inputController_);
-    if (this.localePref_) {
-      this.propagateLocale_(this.localePref_);
-    }
+    this.speechParser_.refresh();
     this.hiddenMacroManager_ = new HiddenMacroManager(this.inputController_);
 
     // Set default speech recognition properties. Locale will be updated when
@@ -310,7 +306,7 @@
     this.clearInterimText_();
 
     // Record metrics.
-    this.metricsUtils_ = new MetricsUtils(type, this.localePref_);
+    this.metricsUtils_ = new MetricsUtils(type, LocaleInfo.locale);
     this.metricsUtils_.recordSpeechRecognitionStarted();
 
     this.uiController_.setState(
@@ -356,8 +352,8 @@
           if (pref.value) {
             const locale = /** @type {string} */ (pref.value);
             this.speechRecognitionOptions_.locale = locale;
-            this.localePref_ = locale;
-            this.propagateLocale_(locale);
+            LocaleInfo.locale = locale;
+            this.speechParser_.refresh();
           }
           break;
         case Dictation.SPOKEN_FEEDBACK_PREF:
@@ -503,18 +499,6 @@
     this.hiddenMacroManager_.runMacroWithTwoStringArgsForTesting(
         name, arg1, arg2);
   }
-
-  /**
-   * @param {string} locale
-   * @private
-   */
-  propagateLocale_(locale) {
-    const commandsSupported =
-        SpeechParser.areCommandsSupported(locale, chrome.i18n.getUILanguage());
-    this.speechParser_.initialize(locale, commandsSupported);
-    this.inputController_.setLocale(locale);
-    this.uiController_.setHintsSupported(commandsSupported);
-  }
 }
 
 /**
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/input_controller.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/input_controller.js
index 38b695f..2b573cde8 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/input_controller.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/input_controller.js
@@ -4,6 +4,7 @@
 
 import {EditingUtil} from './editing_util.js';
 import {FocusHandler} from './focus_handler.js';
+import {LocaleInfo} from './locale_info.js';
 
 const AutomationNode = chrome.automation.AutomationNode;
 const EventType = chrome.automation.EventType;
@@ -30,9 +31,6 @@
     /** @private {?function():void} */
     this.onConnectCallback_ = null;
 
-    /** @private {?string} */
-    this.locale_ = null;
-
     this.initialize_();
   }
 
@@ -103,12 +101,8 @@
       return;
     }
 
-    const language = this.locale_.split('-')[0];
-    const useSmartSpacingAndCapitalization =
-        InputController.SMART_SPACING_AND_CAPITALIZATION_LANGUAGES_.includes(
-            language);
     const data = this.getEditableNodeData_();
-    if (useSmartSpacingAndCapitalization && data) {
+    if (LocaleInfo.allowSmartCapAndSpacing() && data) {
       const {value, caretIndex} = data;
       text = EditingUtil.smartCapitalization(value, caretIndex, text);
       text = EditingUtil.smartSpacing(value, caretIndex, text);
@@ -279,11 +273,6 @@
     node.setSelection(newCaretIndex, newCaretIndex);
   }
 
-  /** @param {string} locale */
-  setLocale(locale) {
-    this.locale_ = locale;
-  }
-
   /**
    * @param {string} value
    * @param {number} index
@@ -339,12 +328,3 @@
  * @const
  */
 InputController.NO_ACTIVE_IME_CONTEXT_ID_ = -1;
-
-
-/**
- * The languages that are supported by smart spacing and capitalization.
- * @private {!Array<string>}
- * @const
- */
-InputController.SMART_SPACING_AND_CAPITALIZATION_LANGUAGES_ =
-    ['en', 'fr', 'it', 'de', 'es'];
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/locale_info.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/locale_info.js
new file mode 100644
index 0000000..ba925759
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/locale_info.js
@@ -0,0 +1,89 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/** Contains all locale-related information for Dictation. */
+export class LocaleInfo {
+  /** @return {boolean} */
+  static allowSmartCapAndSpacing() {
+    const language = LocaleInfo.locale.split('-')[0];
+    return LocaleInfo.SMART_CAP_AND_SPACING_LANGUAGES_.has(language);
+  }
+
+  /** @return {boolean} */
+  static allowSmartEditing() {
+    // Restrict smart editing commands to left-to-right locales.
+    // TODO(crbug.com/1331351): Add support for RTL locales.
+    return !LocaleInfo.isRTLLocale();
+  }
+
+  /** @return {boolean} */
+  static isRTLLocale() {
+    const locale = LocaleInfo.locale;
+    return LocaleInfo.RTL_LOCALES_.has(locale);
+  }
+
+  /** @return {string|undefined} */
+  static getUILanguage() {
+    const locale = LocaleInfo.locale.toLowerCase();
+    return LocaleInfo.LOCALE_TO_UI_LANGUAGE_MAP_[locale];
+  }
+
+  /**
+   * Determines whether commands are supported for this Dictation language
+   * and UI system language.
+   * @return {boolean} Whether commands are supported.
+   */
+  static areCommandsSupported() {
+    // Currently Dictation cannot support commands when the UI language
+    // doesn't match the Dictation language. See crbug.com/1340590.
+    const systemLocale = chrome.i18n.getUILanguage().toLowerCase();
+    const systemLanguage = systemLocale.split('-')[0];
+    const dictationLanguage = LocaleInfo.locale.toLowerCase().split('-')[0];
+    if (systemLanguage === dictationLanguage) {
+      return true;
+    }
+
+    return Boolean(LocaleInfo.getUILanguage()) &&
+        (LocaleInfo.getUILanguage() === systemLanguage ||
+         LocaleInfo.getUILanguage() === systemLocale);
+  }
+}
+
+/**
+ * The current Dictation locale.
+ * @type {string}
+ */
+LocaleInfo.locale = '';
+
+/**
+ * @const {!Set<string>}
+ * @private
+ */
+LocaleInfo.SMART_CAP_AND_SPACING_LANGUAGES_ =
+    new Set(['en', 'fr', 'it', 'de', 'es']);
+
+/**
+ * All RTL locales from Dictation::GetAllSupportedLocales.
+ * @private {!Set<string>}
+ * @const
+ */
+LocaleInfo.RTL_LOCALES_ = new Set([
+  'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IL', 'ar-IQ', 'ar-JO',
+  'ar-KW', 'ar-LB', 'ar-MA', 'ar-OM', 'ar-PS', 'ar-QA', 'ar-SA',
+  'ar-TN', 'ar-YE', 'fa-IR', 'iw-IL', 'ur-IN', 'ur-PK',
+]);
+
+/**
+ * TODO: get this data from l10n or i18n.
+ * Hebrew in Dictation is 'iw-IL' but 'he' in UI languages.
+ * yue-Hant-HK can map to 'zh-TW' because both are written as traditional
+ * Chinese. Norwegian in Dictation is 'no-NO' but 'nb' in UI languages.
+ * @private {!Object<string, string>}
+ * @const
+ */
+LocaleInfo.LOCALE_TO_UI_LANGUAGE_MAP_ = {
+  'iw-il': 'he',
+  'yue-hant-hk': 'zh-tw',
+  'no-no': 'nb',
+};
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/locale_info_test.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/locale_info_test.js
new file mode 100644
index 0000000..03d46b8
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/locale_info_test.js
@@ -0,0 +1,121 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+GEN_INCLUDE(['dictation_test_base.js']);
+
+DictationLocaleInfoTest = class extends DictationE2ETestBase {
+  /** @override */
+  async setUpDeferred() {
+    await super.setUpDeferred();
+    await importModule(
+        'LocaleInfo', '/accessibility_common/dictation/locale_info.js');
+  }
+};
+
+AX_TEST_F('DictationLocaleInfoTest', 'AllowSmartCapAndSpacing', function() {
+  // Restrict behavior to English + FIGS (French, Italian, German, Spanish).
+  LocaleInfo.locale = 'en-US';
+  assertTrue(LocaleInfo.allowSmartCapAndSpacing());
+  LocaleInfo.locale = 'fr';
+  assertTrue(LocaleInfo.allowSmartCapAndSpacing());
+  LocaleInfo.locale = 'it-IT';
+  assertTrue(LocaleInfo.allowSmartCapAndSpacing());
+  LocaleInfo.locale = 'de';
+  assertTrue(LocaleInfo.allowSmartCapAndSpacing());
+  LocaleInfo.locale = 'es';
+  assertTrue(LocaleInfo.allowSmartCapAndSpacing());
+
+  LocaleInfo.locale = 'ja-JP';
+  assertFalse(LocaleInfo.allowSmartCapAndSpacing());
+});
+
+AX_TEST_F('DictationLocaleInfoTest', 'AllowSmartEditing', function() {
+  // Restrict behavior to left-to-right locales.
+  LocaleInfo.locale = 'en-US';
+  assertTrue(LocaleInfo.allowSmartEditing());
+  LocaleInfo.locale = 'ja-JP';
+  assertTrue(LocaleInfo.allowSmartEditing());
+
+  LocaleInfo.locale = 'ar-LB';
+  assertFalse(LocaleInfo.allowSmartEditing());
+});
+
+AX_TEST_F('DictationLocaleInfoTest', 'IsRTLLocale', function() {
+  LocaleInfo.locale = 'ja-JP';
+  assertFalse(LocaleInfo.isRTLLocale());
+  LocaleInfo.locale = 'ar-LB';
+  assertTrue(LocaleInfo.isRTLLocale());
+});
+
+AX_TEST_F('DictationLocaleInfoTest', 'GetUILanguage', function() {
+  LocaleInfo.locale = 'iw-il';
+  assertEquals('he', LocaleInfo.getUILanguage());
+  LocaleInfo.locale = 'iw-IL';
+  assertEquals('he', LocaleInfo.getUILanguage());
+  LocaleInfo.locale = 'yue-hant-hk';
+  assertEquals('zh-tw', LocaleInfo.getUILanguage());
+  LocaleInfo.locale = 'no-no';
+  assertEquals('nb', LocaleInfo.getUILanguage());
+  LocaleInfo.locale = 'en-US';
+  assertEquals(undefined, LocaleInfo.getUILanguage());
+});
+
+AX_TEST_F('DictationLocaleInfoTest', 'AreCommandsSupported', function() {
+  let systemLocale;
+  chrome.i18n.getUILanguage = () => {
+    return systemLocale;
+  };
+  const areCommandsSupported = LocaleInfo.areCommandsSupported;
+
+  // True if the language part of the code matches.
+  LocaleInfo.locale = 'en-US';
+  systemLocale = 'en';
+  assertTrue(areCommandsSupported());
+
+  LocaleInfo.locale = 'EN-US';
+  systemLocale = 'en';
+  assertTrue(areCommandsSupported());
+
+  LocaleInfo.locale = 'en-US';
+  systemLocale = 'en-GB';
+  assertTrue(areCommandsSupported());
+
+  // False if the language part of the code doesn't match, in most cases.
+  LocaleInfo.locale = 'en-US';
+  systemLocale = 'ja-JP';
+  assertFalse(areCommandsSupported());
+
+  LocaleInfo.locale = 'ja-JP';
+  systemLocale = 'en-US';
+  assertFalse(areCommandsSupported());
+
+  // Special cases: these Dictation locales can map to UI languages.
+  LocaleInfo.locale = 'iw-IL';
+  systemLocale = 'he';
+  assertTrue(areCommandsSupported());
+
+  LocaleInfo.locale = 'iw-IL';
+  systemLocale = 'he-IL';
+  assertTrue(areCommandsSupported());
+
+  LocaleInfo.locale = 'no-NO';
+  systemLocale = 'nb';
+  assertTrue(areCommandsSupported());
+
+  LocaleInfo.locale = 'no-NO';
+  systemLocale = 'nb-NB';
+  assertTrue(areCommandsSupported());
+
+  LocaleInfo.locale = 'yue-Hant-HK';
+  systemLocale = 'zh-TW';
+  assertTrue(areCommandsSupported());
+
+  LocaleInfo.locale = 'yue-Hant-HK';
+  systemLocale = 'zh';
+  assertFalse(areCommandsSupported());
+
+  LocaleInfo.locale = 'yue-Hant-HK';
+  systemLocale = 'zh-CN';
+  assertFalse(areCommandsSupported());
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/delete_prev_sent_macro.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/delete_prev_sent_macro.js
index d703ac79..9eb0845 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/delete_prev_sent_macro.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/delete_prev_sent_macro.js
@@ -32,4 +32,9 @@
     this.inputController_.deletePrevSentence();
     return this.createRunMacroResult_(/*isSuccess=*/ true);
   }
+
+  /** @override */
+  isSmart() {
+    return true;
+  }
 }
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/hidden_macro_manager.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/hidden_macro_manager.js
index 58f3ca6..5a3ea0d 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/hidden_macro_manager.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/hidden_macro_manager.js
@@ -41,18 +41,16 @@
         new DeletePrevSentMacro(this.inputController_).runMacro();
         break;
       case MacroName.NAV_NEXT_WORD:
-        new NavNextWordMacro(/*isRTLLocale=*/ false).runMacro();
+        new NavNextWordMacro().runMacro();
         break;
       case MacroName.NAV_PREV_WORD:
-        new NavPrevWordMacro(/*isRTLLocale=*/ false).runMacro();
+        new NavPrevWordMacro().runMacro();
         break;
       case MacroName.NAV_NEXT_SENT:
-        new NavNextSentMacro(this.inputController_, /*isRTLLocale=*/ false)
-            .runMacro();
+        new NavNextSentMacro(this.inputController_).runMacro();
         break;
       case MacroName.NAV_PREV_SENT:
-        new NavPrevSentMacro(this.inputController_, /*isRTLLocale=*/ false)
-            .runMacro();
+        new NavPrevSentMacro(this.inputController_).runMacro();
         break;
       default:
         throw new Error(`Cannot run macro: ${name} for testing`);
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/macro.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/macro.js
index dd4b532..9ec23b1 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/macro.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/macro.js
@@ -132,4 +132,9 @@
   createRunMacroResult_(isSuccess, error) {
     return {isSuccess, error};
   }
+
+  /** @return {boolean} */
+  isSmart() {
+    return false;
+  }
 }
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/nav_sent_macro.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/nav_sent_macro.js
index 58d30eb5..8bc97ea 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/nav_sent_macro.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/nav_sent_macro.js
@@ -9,16 +9,11 @@
 
 /** Implements a macro that moves the text caret to the next sentence. */
 export class NavNextSentMacro extends Macro {
-  /**
-   * @param {!InputController} inputController
-   * @param {boolean} isRTLLocale
-   */
-  constructor(inputController, isRTLLocale) {
+  /** @param {!InputController} inputController */
+  constructor(inputController) {
     super(MacroName.NAV_NEXT_SENT);
     /** @private {!InputController} */
     this.inputController_ = inputController;
-    /** @private {boolean} */
-    this.isRTLLocale_ = isRTLLocale;
   }
 
   /** @override */
@@ -36,20 +31,20 @@
     this.inputController_.navNextSent();
     return this.createRunMacroResult_(/*isSuccess=*/ true);
   }
+
+  /** @override */
+  isSmart() {
+    return true;
+  }
 }
 
 /** Implements a macro that moves the text caret to the previous sentence. */
 export class NavPrevSentMacro extends Macro {
-  /**
-   * @param {!InputController} inputController
-   * @param {boolean} isRTLLocale
-   */
-  constructor(inputController, isRTLLocale) {
+  /** @param {!InputController} inputController */
+  constructor(inputController) {
     super(MacroName.NAV_PREV_SENT);
     /** @private {!InputController} */
     this.inputController_ = inputController;
-    /** @private {boolean} */
-    this.isRTLLocale_ = isRTLLocale;
   }
 
   /** @override */
@@ -67,4 +62,9 @@
     this.inputController_.navPrevSent();
     return this.createRunMacroResult_(/*isSuccess=*/ true);
   }
+
+  /** @override */
+  isSmart() {
+    return true;
+  }
 }
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/repeatable_key_press_macro.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/repeatable_key_press_macro.js
index 65c03c0e..13c5dbc4 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/repeatable_key_press_macro.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/repeatable_key_press_macro.js
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import {EventGenerator} from '../../../common/event_generator.js';
+import {LocaleInfo} from '../locale_info.js';
 
 import {Macro, MacroError} from './macro.js';
 import {MacroName} from './macro_names.js';
@@ -69,43 +70,29 @@
 
 /** Macro to navigate to the previous character. */
 export class NavPreviousCharMacro extends RepeatableKeyPressMacro {
-  /**
-   * @param {boolean} isRTLLocale Whether the Dictation speech recognition
-   *     locale is right-to-left.
-   * @param {number=} repeat The number of characters to move.
-   */
-  constructor(isRTLLocale, repeat = 1) {
+  /** @param {number=} repeat The number of characters to move. */
+  constructor(repeat = 1) {
     super(MacroName.NAV_PREV_CHAR, repeat);
-
-    /** @private {boolean} */
-    this.isRTLLocale_ = isRTLLocale;
   }
 
   /** @override */
   doKeyPress() {
     EventGenerator.sendKeyPress(
-        this.isRTLLocale_ ? KeyCode.RIGHT : KeyCode.LEFT);
+        LocaleInfo.isRTLLocale() ? KeyCode.RIGHT : KeyCode.LEFT);
   }
 }
 
 /** Macro to navigate to the next character. */
 export class NavNextCharMacro extends RepeatableKeyPressMacro {
-  /**
-   * @param {boolean} isRTLLocale Whether the Dictation speech recognition
-   *     locale is right-to-left.
-   * @param {number=} repeat The number of characters to move.
-   */
-  constructor(isRTLLocale, repeat = 1) {
+  /** @param {number=} repeat The number of characters to move. */
+  constructor(repeat = 1) {
     super(MacroName.NAV_NEXT_CHAR, repeat);
-
-    /** @private {boolean} */
-    this.isRTLLocale_ = isRTLLocale;
   }
 
   /** @override */
   doKeyPress() {
     EventGenerator.sendKeyPress(
-        this.isRTLLocale_ ? KeyCode.LEFT : KeyCode.RIGHT);
+        LocaleInfo.isRTLLocale() ? KeyCode.LEFT : KeyCode.RIGHT);
   }
 }
 
@@ -209,20 +196,14 @@
 
 /** Macro to unselect text. */
 export class UnselectTextMacro extends RepeatableKeyPressMacro {
-  /**
-   * @param {boolean} isRTLLocale Whether the Dictation speech recognition
-   *     locale is right-to-left.
-   */
-  constructor(isRTLLocale) {
+  constructor() {
     super(MacroName.UNSELECT_TEXT, /*repeat=*/ 1);
-    /** @private {boolean} */
-    this.isRTLLocale_ = isRTLLocale;
   }
 
   /** @override */
   doKeyPress() {
     EventGenerator.sendKeyPress(
-        this.isRTLLocale_ ? KeyCode.LEFT : KeyCode.RIGHT);
+        LocaleInfo.isRTLLocale() ? KeyCode.LEFT : KeyCode.RIGHT);
   }
 }
 
@@ -240,42 +221,28 @@
 
 /** Macro to navigate to the next word. */
 export class NavNextWordMacro extends RepeatableKeyPressMacro {
-  /**
-   * @param {boolean} isRTLLocale Whether the Dictation speech recognition
-   *     locale is right-to-left.
-   * @param {number=} repeat The number of words to move.
-   */
-  constructor(isRTLLocale, repeat = 1) {
+  /** @param {number=} repeat The number of words to move. */
+  constructor(repeat = 1) {
     super(MacroName.NAV_NEXT_WORD, repeat);
-
-    /** @private {boolean} */
-    this.isRTLLocale_ = isRTLLocale;
   }
 
   /** @override */
   doKeyPress() {
     EventGenerator.sendKeyPress(
-        this.isRTLLocale_ ? KeyCode.LEFT : KeyCode.RIGHT, {ctrl: true});
+        LocaleInfo.isRTLLocale() ? KeyCode.LEFT : KeyCode.RIGHT, {ctrl: true});
   }
 }
 
 /** Macro to navigate to the previous word. */
 export class NavPrevWordMacro extends RepeatableKeyPressMacro {
-  /**
-   * @param {boolean} isRTLLocale Whether the Dictation speech recognition
-   *     locale is right-to-left.
-   * @param {number=} repeat The number of words to move.
-   */
-  constructor(isRTLLocale, repeat = 1) {
+  /** @param {number=} repeat The number of words to move. */
+  constructor(repeat = 1) {
     super(MacroName.NAV_PREV_WORD, repeat);
-
-    /** @private {boolean} */
-    this.isRTLLocale_ = isRTLLocale;
   }
 
   /** @override */
   doKeyPress() {
     EventGenerator.sendKeyPress(
-        this.isRTLLocale_ ? KeyCode.RIGHT : KeyCode.LEFT, {ctrl: true});
+        LocaleInfo.isRTLLocale() ? KeyCode.RIGHT : KeyCode.LEFT, {ctrl: true});
   }
 }
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_delete_phrase_macro.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_delete_phrase_macro.js
index a6474394..ec34a092 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_delete_phrase_macro.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_delete_phrase_macro.js
@@ -36,4 +36,9 @@
     this.inputController_.deletePhrase(this.phrase_);
     return this.createRunMacroResult_(/*isSuccess=*/ true);
   }
+
+  /** @override */
+  isSmart() {
+    return true;
+  }
 }
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_insert_before_macro.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_insert_before_macro.js
index 0da4923..8439a71 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_insert_before_macro.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_insert_before_macro.js
@@ -42,4 +42,9 @@
     this.inputController_.insertBefore(this.insertPhrase_, this.beforePhrase_);
     return this.createRunMacroResult_(/*isSuccess=*/ true);
   }
+
+  /** @override */
+  isSmart() {
+    return true;
+  }
 }
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_replace_phrase_macro.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_replace_phrase_macro.js
index 1370fea..400d421 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_replace_phrase_macro.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_replace_phrase_macro.js
@@ -40,4 +40,9 @@
     this.inputController_.replacePhrase(this.deletePhrase_, this.insertPhrase_);
     return this.createRunMacroResult_(/*isSuccess=*/ true);
   }
+
+  /** @override */
+  isSmart() {
+    return true;
+  }
 }
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_select_between_macro.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_select_between_macro.js
index e5c98a2..6bcac17 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_select_between_macro.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/macros/smart_select_between_macro.js
@@ -39,4 +39,9 @@
     this.inputController_.selectBetween(this.startPhrase_, this.endPhrase_);
     return this.createRunMacroResult_(/*isSuccess=*/ true);
   }
+
+  /** @override */
+  isSmart() {
+    return true;
+  }
 }
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/dictation_parse_test.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/dictation_parse_test.js
index 15a9df35..6612d01 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/dictation_parse_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/dictation_parse_test.js
@@ -35,58 +35,87 @@
   assertNotNullNorUndefined(strategy);
   let macro = await strategy.parse('Hello world');
   assertEquals('INPUT_TEXT_VIEW', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('type delete');
   assertEquals('INPUT_TEXT_VIEW', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('delete');
   assertEquals('DELETE_PREV_CHAR', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('move to the previous character');
   assertEquals('NAV_PREV_CHAR', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('move to the next character');
   assertEquals('NAV_NEXT_CHAR', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('move to the previous line');
   assertEquals('NAV_PREV_LINE', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('move to the next line');
   assertEquals('NAV_NEXT_LINE', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('copy');
   assertEquals('COPY_SELECTED_TEXT', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('paste');
   assertEquals('PASTE_TEXT', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('cut');
   assertEquals('CUT_SELECTED_TEXT', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('undo');
   assertEquals('UNDO_TEXT_EDIT', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('redo');
   assertEquals('REDO_ACTION', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('select all');
   assertEquals('SELECT_ALL_TEXT', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('unselect');
   assertEquals('UNSELECT_TEXT', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('help');
   assertEquals('LIST_COMMANDS', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('new line');
   assertEquals('NEW_LINE', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('cancel');
   assertEquals('STOP_LISTENING', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('delete the previous word');
   assertEquals('DELETE_PREV_WORD', macro.getMacroNameString());
-  macro = await strategy.parse('delete the previous sentence');
-  assertEquals('DELETE_PREV_SENT', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('move to the next word');
   assertEquals('NAV_NEXT_WORD', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
   macro = await strategy.parse('move to the previous word');
   assertEquals('NAV_PREV_WORD', macro.getMacroNameString());
+  assertFalse(macro.isSmart());
+
+  // Smart macros.
+  macro = await strategy.parse('delete the previous sentence');
+  assertEquals('DELETE_PREV_SENT', macro.getMacroNameString());
+  assertTrue(macro.isSmart());
   macro = await strategy.parse('delete hello world');
   assertEquals('SMART_DELETE_PHRASE', macro.getMacroNameString());
+  assertTrue(macro.isSmart());
   macro = await strategy.parse('replace hello world with goodnight world');
   assertEquals('SMART_REPLACE_PHRASE', macro.getMacroNameString());
+  assertTrue(macro.isSmart());
   macro = await strategy.parse('insert hello world before goodnight world');
   assertEquals('SMART_INSERT_BEFORE', macro.getMacroNameString());
+  assertTrue(macro.isSmart());
   macro = await strategy.parse('select from hello world to goodnight world');
   assertEquals('SMART_SELECT_BTWN_INCL', macro.getMacroNameString());
+  assertTrue(macro.isSmart());
   macro = await strategy.parse('move to the next sentence');
   assertEquals('NAV_NEXT_SENT', macro.getMacroNameString());
+  assertTrue(macro.isSmart());
   macro = await strategy.parse('move to the previous sentence');
   assertEquals('NAV_PREV_SENT', macro.getMacroNameString());
+  assertTrue(macro.isSmart());
 });
 
 // TODO(crbug.com/1264544): This test fails because of a memory issues
@@ -105,30 +134,25 @@
       assertEquals('DELETE_PREV_CHAR', macro.getMacroNameString());
     });
 
-AX_TEST_F(
-    'DictationParseTest', 'AreCommandsSupportedForLocales', async function() {
-      // True if the language part of the code matches.
-      assertTrue(SpeechParser.areCommandsSupported('en-US', 'en'));
-      assertTrue(SpeechParser.areCommandsSupported('EN-US', 'en'));
-      assertTrue(SpeechParser.areCommandsSupported('en-US', 'en-US'));
-      assertTrue(SpeechParser.areCommandsSupported('en-GB', 'en-US'));
-      assertTrue(SpeechParser.areCommandsSupported('es-CR', 'es'));
-      assertTrue(SpeechParser.areCommandsSupported('es-CR', 'es-ES'));
+AX_TEST_F('DictationParseTest', 'NoSmartMacrosForRTLLocales', async function() {
+  const strategy = this.getSimpleParseStrategy();
+  assertNotNullNorUndefined(strategy);
+  await this.setPref(Dictation.DICTATION_LOCALE_PREF, 'en-US');
+  await this.getPref(Dictation.DICTATION_LOCALE_PREF);
 
-      // False if the language part of the code doesn't match, in most cases.
-      assertFalse(SpeechParser.areCommandsSupported('en-US', 'ja-JP'));
-      assertFalse(SpeechParser.areCommandsSupported('ja-JP', 'en-US'));
-      assertFalse(SpeechParser.areCommandsSupported('iw-IL', 'en-US'));
-      assertFalse(SpeechParser.areCommandsSupported('no-NO', 'es-ES'));
-      assertFalse(SpeechParser.areCommandsSupported('yue-Hant-HK', 'en-US'));
+  let macro = await strategy.parse('insert hello world before goodnight world');
+  assertNotNullNorUndefined(macro);
+  assertTrue(macro.isSmart());
+  assertEquals('SMART_INSERT_BEFORE', macro.getMacroNameString());
 
-      // Special cases: these Dictation locales can map to
-      // existing UI languages.
-      assertTrue(SpeechParser.areCommandsSupported('iw-IL', 'he'));
-      assertTrue(SpeechParser.areCommandsSupported('iw-IL', 'he-IL'));
-      assertTrue(SpeechParser.areCommandsSupported('no-NO', 'nb'));
-      assertTrue(SpeechParser.areCommandsSupported('no-NO', 'nb-NB'));
-      assertFalse(SpeechParser.areCommandsSupported('yue-Hant-HK', 'zh'));
-      assertFalse(SpeechParser.areCommandsSupported('yue-Hant-HK', 'zh-CN'));
-      assertTrue(SpeechParser.areCommandsSupported('yue-Hant-HK', 'zh-TW'));
-    });
+  // Change Dictation locale to a right-to-left locale.
+  await this.setPref(Dictation.DICTATION_LOCALE_PREF, 'ar-LB');
+  await this.getPref(Dictation.DICTATION_LOCALE_PREF);
+
+  // Smart macros are not supported in right-to-left locales. In these cases,
+  // we fall back to INPUT_TEXT_VIEW macros.
+  macro = await strategy.parse('insert hello world before goodnight world');
+  assertNotNullNorUndefined(macro);
+  assertFalse(macro.isSmart());
+  assertEquals('INPUT_TEXT_VIEW', macro.getMacroNameString());
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/input_text_strategy.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/input_text_strategy.js
index f73b25b..c43e67a1 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/input_text_strategy.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/input_text_strategy.js
@@ -11,7 +11,7 @@
 export class InputTextStrategy extends ParseStrategy {
   /** @param {!InputController} inputController */
   constructor(inputController) {
-    super(inputController, false);
+    super(inputController);
   }
 
   /** @override */
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/parse_strategy.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/parse_strategy.js
index 35022b61..301dc75 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/parse_strategy.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/parse_strategy.js
@@ -15,15 +15,10 @@
  * Macro.
  */
 export class ParseStrategy {
-  /**
-   * @param {!InputController} inputController
-   * @param {boolean} isRTLLocale
-   */
-  constructor(inputController, isRTLLocale) {
+  /** @param {!InputController} inputController */
+  constructor(inputController) {
     /** @private {!InputController} */
     this.inputController_ = inputController;
-    /** @private {boolean} */
-    this.isRTLLocale_ = isRTLLocale;
   }
 
   /** @return {!InputController} */
@@ -31,11 +26,6 @@
     return this.inputController_;
   }
 
-  /** @return {boolean} */
-  getIsRTLLocale() {
-    return this.isRTLLocale_;
-  }
-
   /**
    * Accepts text, parses it, and returns a Macro.
    * @param {string} text
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/pumpkin_parse_strategy.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/pumpkin_parse_strategy.js
index f3b59b9..07e7ecd 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/pumpkin_parse_strategy.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/pumpkin_parse_strategy.js
@@ -8,6 +8,7 @@
  */
 
 import {InputController} from '../input_controller.js';
+import {LocaleInfo} from '../locale_info.js';
 import {InputTextViewMacro} from '../macros/input_text_view_macro.js';
 import {ListCommandsMacro} from '../macros/list_commands_macro.js';
 import {Macro} from '../macros/macro.js';
@@ -24,12 +25,11 @@
 export class PumpkinParseStrategy extends ParseStrategy {
   /**
    * @param {!InputController} inputController
-   * @param {boolean} isRTLLocale
-   * @param {string} locale
    * @return {!Promise<!PumpkinParseStrategy>}
    */
-  static async create(inputController, isRTLLocale, locale) {
-    const instance = new PumpkinParseStrategy(inputController, isRTLLocale);
+  static async create(inputController) {
+    const locale = LocaleInfo.locale;
+    const instance = new PumpkinParseStrategy(inputController);
     if (PumpkinAvailability.usePumpkin(locale)) {
       await instance.initPumpkin_(PumpkinAvailability.LOCALES[locale]);
     }
@@ -39,11 +39,10 @@
 
   /**
    * @param {!InputController} inputController
-   * @param {boolean} isRTLLocale
    * @private
    */
-  constructor(inputController, isRTLLocale) {
-    super(inputController, isRTLLocale);
+  constructor(inputController) {
+    super(inputController);
 
     /** @private {speech.pumpkin.api.js.PumpkinTagger.PumpkinTagger} */
     this.pumpkinTagger_ = null;
@@ -180,11 +179,9 @@
       case MacroName.DELETE_PREV_CHAR:
         return new RepeatableKeyPressMacro.DeletePreviousCharacterMacro(repeat);
       case MacroName.NAV_PREV_CHAR:
-        return new RepeatableKeyPressMacro.NavPreviousCharMacro(
-            this.getIsRTLLocale(), repeat);
+        return new RepeatableKeyPressMacro.NavPreviousCharMacro(repeat);
       case MacroName.NAV_NEXT_CHAR:
-        return new RepeatableKeyPressMacro.NavNextCharMacro(
-            this.getIsRTLLocale(), repeat);
+        return new RepeatableKeyPressMacro.NavNextCharMacro(repeat);
       case MacroName.NAV_PREV_LINE:
         return new RepeatableKeyPressMacro.NavPreviousLineMacro(repeat);
       case MacroName.NAV_NEXT_LINE:
@@ -202,8 +199,7 @@
       case MacroName.SELECT_ALL_TEXT:
         return new RepeatableKeyPressMacro.SelectAllTextMacro();
       case MacroName.UNSELECT_TEXT:
-        return new RepeatableKeyPressMacro.UnselectTextMacro(
-            this.getIsRTLLocale());
+        return new RepeatableKeyPressMacro.UnselectTextMacro();
       case MacroName.LIST_COMMANDS:
         return new ListCommandsMacro();
       default:
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/simple_parse_strategy.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/simple_parse_strategy.js
index d43a05e..8515fd84 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/simple_parse_strategy.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/simple_parse_strategy.js
@@ -8,6 +8,7 @@
  */
 
 import {InputController} from '../input_controller.js';
+import {LocaleInfo} from '../locale_info.js';
 import {DeletePrevSentMacro} from '../macros/delete_prev_sent_macro.js';
 import {HiddenMacroManager} from '../macros/hidden_macro_manager.js';
 import {InputTextViewMacro, NewLineMacro} from '../macros/input_text_view_macro.js';
@@ -39,9 +40,8 @@
   /**
    * @param {!MacroName} macroName
    * @param {!InputController} inputController
-   * @param {boolean} isRTLLocale
    */
-  constructor(macroName, inputController, isRTLLocale) {
+  constructor(macroName, inputController) {
     if (!SimpleMacroFactory.getData_()[macroName]) {
       throw new Error(
           'Macro is not supported by SimpleMacroFactory: ' + macroName);
@@ -51,8 +51,6 @@
     this.macroName_ = macroName;
     /** @private {!InputController} */
     this.inputController_ = inputController;
-    /** @private {boolean} */
-    this.isRTLLocale_ = isRTLLocale;
 
     /** @private {RegExp} */
     this.commandRegex_ = null;
@@ -100,25 +98,16 @@
 
     const initialArgs = [];
     switch (this.macroName_) {
-      case MacroName.NAV_PREV_CHAR:
-      case MacroName.NAV_NEXT_CHAR:
-      case MacroName.UNSELECT_TEXT:
-      case MacroName.NAV_NEXT_WORD:
-      case MacroName.NAV_PREV_WORD:
-        initialArgs.push(this.isRTLLocale_);
-        break;
       case MacroName.NEW_LINE:
       case MacroName.DELETE_PREV_SENT:
+      case MacroName.NAV_NEXT_SENT:
+      case MacroName.NAV_PREV_SENT:
       case MacroName.SMART_DELETE_PHRASE:
       case MacroName.SMART_REPLACE_PHRASE:
       case MacroName.SMART_INSERT_BEFORE:
       case MacroName.SMART_SELECT_BTWN_INCL:
         initialArgs.push(this.inputController_);
         break;
-      case MacroName.NAV_NEXT_SENT:
-      case MacroName.NAV_PREV_SENT:
-        initialArgs.push(this.inputController_, this.isRTLLocale_);
-        break;
     }
 
     const result = this.commandRegex_.exec(text);
@@ -128,7 +117,12 @@
     const extractedArgs = result.slice(1);
     const finalArgs = initialArgs.concat(extractedArgs);
     const data = SimpleMacroFactory.getData_();
-    return new data[this.macroName_].build(...finalArgs);
+    const macro = new data[this.macroName_].build(...finalArgs);
+    if (macro.isSmart() && !LocaleInfo.allowSmartEditing()) {
+      return null;
+    }
+
+    return macro;
   }
 
   /**
@@ -244,12 +238,9 @@
 
 /** A parsing strategy that utilizes SimpleMacroFactory. */
 export class SimpleParseStrategy extends ParseStrategy {
-  /**
-   * @param {!InputController} inputController
-   * @param {boolean} isRTLLocale
-   */
-  constructor(inputController, isRTLLocale) {
-    super(inputController, isRTLLocale);
+  /** @param {!InputController} inputController */
+  constructor(inputController) {
+    super(inputController);
 
     /**
      * Map of macro names to a factory for that macro.
@@ -271,9 +262,7 @@
       }
 
       this.macroFactoryMap_.set(
-          name,
-          new SimpleMacroFactory(
-              name, this.getInputController(), this.getIsRTLLocale()));
+          name, new SimpleMacroFactory(name, this.getInputController()));
     }
   }
 
@@ -300,8 +289,7 @@
           macros[1] :
           macros[0];
     } else if (macros.length > 2) {
-      // Unexpected, log a warning. This will cause tests to fail.
-      console.warn('Unexpected ambiguous macros found for text: ${text}.');
+      console.warn(`Unexpected ambiguous macros found for text: ${text}.`);
       return macros[0];
     }
 
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/speech_parser.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/speech_parser.js
index ad6b36d..9b61d75 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/speech_parser.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/speech_parser.js
@@ -7,6 +7,7 @@
  */
 
 import {InputController} from '../input_controller.js';
+import {LocaleInfo} from '../locale_info.js';
 import {Macro} from '../macros/macro.js';
 
 import {InputTextStrategy} from './input_text_strategy.js';
@@ -18,9 +19,6 @@
 export class SpeechParser {
   /** @param {!InputController} inputController to interact with the IME. */
   constructor(inputController) {
-    /** @private {boolean} */
-    this.isRTLLocale_ = false;
-
     /** @private {!InputController} */
     this.inputController_ = inputController;
 
@@ -34,25 +32,18 @@
     this.pumpkinParseStrategy_ = null;
   }
 
-  /**
-   * @param {string} locale The Dictation recognition locale. Only some locales
-   *     are supported by Pumpkin.
-   * @return {!Promise}
-   */
-  async initialize(locale, commandsSupported) {
-    this.isRTLLocale_ = SpeechParser.RTLLocales.has(locale);
-
-    if (!commandsSupported) {
+  /** @return {!Promise} */
+  async refresh() {
+    if (!LocaleInfo.areCommandsSupported()) {
       this.simpleParseStrategy_ = null;
       this.pumpkinParseStrategy_ = null;
       return;
     }
 
-    // Initialize additional parsing strategies.
-    this.simpleParseStrategy_ =
-        new SimpleParseStrategy(this.inputController_, this.isRTLLocale_);
-    this.pumpkinParseStrategy_ = await PumpkinParseStrategy.create(
-        this.inputController_, this.isRTLLocale_, locale);
+    //  Initialize additional parsing strategies.
+    this.simpleParseStrategy_ = new SimpleParseStrategy(this.inputController_);
+    this.pumpkinParseStrategy_ =
+        await PumpkinParseStrategy.create(this.inputController_);
   }
 
   /**
@@ -80,43 +71,4 @@
     return await /** @type {!Promise<!Macro>} */ (
         this.inputTextStrategy_.parse(text));
   }
-
-  /**
-   * Determines whether commands are supported for this Dictation language
-   * and UI system language.
-   * @param {string} locale The Dictation locale code, like 'en-US'.
-   * @param {string} systemLocale The system language, may be like 'en' or
-   *     'en-US'.
-   * @return boolean Whether commands are supported.
-   */
-  static areCommandsSupported(locale, systemLocale) {
-    // Currently Dictation cannot support commands when the UI language
-    // doesn't match the Dictation language. See crbug.com/1340590.
-    locale = locale.toLowerCase();
-    systemLocale = systemLocale.toLowerCase();
-    const uiLanguage = systemLocale.split('-')[0];
-    if (uiLanguage !== (locale.split('-')[0])) {
-      if (SpeechParser.LocaleToUILanguagesMap.has(locale) &&
-          (SpeechParser.LocaleToUILanguagesMap.get(locale) === uiLanguage ||
-           SpeechParser.LocaleToUILanguagesMap.get(locale) === systemLocale)) {
-        return true;
-      }
-      return false;
-    }
-    return true;
-  }
 }
-
-// All RTL locales from Dictation::GetAllSupportedLocales.
-SpeechParser.RTLLocales = new Set([
-  'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IL', 'ar-IQ', 'ar-JO',
-  'ar-KW', 'ar-LB', 'ar-MA', 'ar-OM', 'ar-PS', 'ar-QA', 'ar-SA',
-  'ar-TN', 'ar-YE', 'fa-IR', 'iw-IL', 'ur-IN', 'ur-PK',
-]);
-
-// Hebrew in Dictation is 'iw-IL' but 'he' in UI languages.
-// yue-Hant-HK can map to 'zh-TW' because both are written as traditional
-// Chinese. check this Norwegian in Dictation is 'no-NO' but 'nb' in UI
-// languages.
-SpeechParser.LocaleToUILanguagesMap =
-    new Map([['iw-il', 'he'], ['yue-hant-hk', 'zh-tw'], ['no-no', 'nb']]);
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/ui_controller.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/ui_controller.js
index 654bdf8..e78f744b 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/ui_controller.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/ui_controller.js
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import {LocaleInfo} from './locale_info.js';
+
 const HintType = chrome.accessibilityPrivate.DictationBubbleHintType;
 const IconType = chrome.accessibilityPrivate.DictationBubbleIconType;
 
@@ -36,18 +38,6 @@
   constructor() {
     /** @private {?number} */
     this.showHintsTimeoutId_ = null;
-
-    /** @private {boolean} */
-    this.hintsSupported_ = true;
-  }
-
-  /**
-   * Sets whether hints are supported. If hints are not
-   * supported, they will not be shown.
-   * @param {boolean} supported
-   */
-  setHintsSupported(supported) {
-    this.hintsSupported_ = supported;
   }
 
   /**
@@ -85,7 +75,8 @@
         break;
     }
 
-    if (!context || !this.hintsSupported_) {
+    if (!context || !LocaleInfo.areCommandsSupported()) {
+      // Do not show hints if commands are not supported.
       return;
     }
 
diff --git a/chrome/browser/resources/chromeos/login/test_api/test_api.js b/chrome/browser/resources/chromeos/login/test_api/test_api.js
index 85a87cec..c83bbed 100644
--- a/chrome/browser/resources/chromeos/login/test_api/test_api.js
+++ b/chrome/browser/resources/chromeos/login/test_api/test_api.js
@@ -161,6 +161,8 @@
 class WelcomeScreenTester extends ScreenElementApi {
   constructor() {
     super('connect');
+    this.demoModeConfirmationDialog =
+        new PolymerElementApi(this, '#demoModeConfirmationDialog');
   }
 
   /** @override */
@@ -173,6 +175,9 @@
     assert(this.nextButton);
     this.nextButton.click();
   }
+  getDemoModeOkButtonName() {
+    return loadTimeData.getString('enableDemoModeDialogConfirm');
+  }
 }
 
 class NetworkScreenTester extends ScreenElementApi {
@@ -542,6 +547,29 @@
   }
 }
 
+class DemoPreferencesScreenTester extends ScreenElementApi {
+  constructor() {
+    super('demo-preferences');
+  }
+
+  getDemoPreferencesNextButtonName() {
+    return loadTimeData.getString('demoPreferencesNextButtonLabel');
+  }
+}
+
+class ArcTosScreenTester extends ScreenElementApi {
+  constructor() {
+    super('arc-tos');
+  }
+
+  // Note that the Accept Button text key is different depending on whether
+  // the device in Demo Mode setup. Key for non-demo setup is
+  // "arcTermsOfServiceAcceptButton"
+  getArcTosDemoModeAcceptButtonName() {
+    return loadTimeData.getString('arcTermsOfServiceAcceptAndContinueButton');
+  }
+}
+
 class OobeApiProvider {
   constructor() {
     this.screens = {
@@ -563,6 +591,8 @@
       GuestTosScreen: new GuestTosScreenTester(),
       ErrorScreen: new ErrorScreenTester(),
       OfflineLoginScreen: new OfflineLoginScreenTester(),
+      DemoPreferencesScreen: new DemoPreferencesScreenTester(),
+      ArcTosScreen: new ArcTosScreenTester(),
     };
 
     this.loginWithPin = function(username, pin) {
diff --git a/chrome/browser/safe_browsing/BUILD.gn b/chrome/browser/safe_browsing/BUILD.gn
index 9d43a724..b047622 100644
--- a/chrome/browser/safe_browsing/BUILD.gn
+++ b/chrome/browser/safe_browsing/BUILD.gn
@@ -2,6 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//build/config/chromeos/ui_mode.gni")
 import("//components/safe_browsing/buildflags.gni")
 import("//extensions/buildflags/buildflags.gni")
 
@@ -389,6 +390,9 @@
           "//chrome/services/file_util/public/mojom",
         ]
       }
+      if (is_chromeos_ash) {
+        deps += [ "//ash/constants:constants" ]
+      }
     } else if (safe_browsing_mode == 2) {
       if (is_android) {
         sources += [
diff --git a/chrome/browser/safe_browsing/tailored_security/chrome_tailored_security_service_unittest.cc b/chrome/browser/safe_browsing/tailored_security/chrome_tailored_security_service_unittest.cc
index f0d9a8f..5a3403f 100644
--- a/chrome/browser/safe_browsing/tailored_security/chrome_tailored_security_service_unittest.cc
+++ b/chrome/browser/safe_browsing/tailored_security/chrome_tailored_security_service_unittest.cc
@@ -39,7 +39,7 @@
 
 namespace safe_browsing {
 
-#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+#if !BUILDFLAG(IS_ANDROID)
 // Names for Tailored Security status to make the test cases clearer.
 const bool kTailoredSecurityEnabled = true;
 const bool kTailoredSecurityDisabled = false;
@@ -203,10 +203,7 @@
       chrome_tailored_security_service_;
 };
 
-#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
-// TODO(crbug/1344604): Tests will fail until we add `catalog_name` to notifier
-// id creation.
-
+#if !BUILDFLAG(IS_ANDROID)
 // Some of the test names are shorted using "Ts" for Tailored Security, "Ep"
 // for Enhanced Protection and "Sb" for Safe Browsing.
 
diff --git a/chrome/browser/safe_browsing/tailored_security/notification_handler_desktop.cc b/chrome/browser/safe_browsing/tailored_security/notification_handler_desktop.cc
index 2a27e61..af2214c 100644
--- a/chrome/browser/safe_browsing/tailored_security/notification_handler_desktop.cc
+++ b/chrome/browser/safe_browsing/tailored_security/notification_handler_desktop.cc
@@ -32,6 +32,10 @@
 #include "ui/native_theme/common_theme.h"
 #include "ui/native_theme/native_theme.h"
 
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "ash/constants/notifier_catalogs.h"
+#endif
+
 namespace safe_browsing {
 
 namespace {
@@ -73,11 +77,33 @@
       outcome);
 }
 
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+
+message_center::NotifierId GetDisabledNotifierId() {
+  return message_center::NotifierId(
+      message_center::NotifierType::SYSTEM_COMPONENT,
+      kTailoredSecurityNotifierId,
+      ash::NotificationCatalogName::kTailoredSecurityDisabled);
+}
+message_center::NotifierId GetEnabledNotifierId() {
+  return message_center::NotifierId(
+      message_center::NotifierType::SYSTEM_COMPONENT,
+      kTailoredSecurityNotifierId,
+      ash::NotificationCatalogName::kTailoredSecurityEnabled);
+}
+message_center::NotifierId GetPromotionNotifierId() {
+  return message_center::NotifierId(
+      message_center::NotifierType::SYSTEM_COMPONENT,
+      kTailoredSecurityNotifierId,
+      ash::NotificationCatalogName::kTailoredSecurityPromotion);
+}
+#else
 message_center::NotifierId GetNotifierId() {
   return message_center::NotifierId(
       message_center::NotifierType::SYSTEM_COMPONENT,
       kTailoredSecurityNotifierId);
 }
+#endif
 
 }  // namespace
 
@@ -152,6 +178,12 @@
   std::u16string title, description, primary_button, secondary_button;
   ui::ImageModel icon;
   std::string notification_id;
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+  const message_center::NotifierId notifier_id =
+      enable ? GetEnabledNotifierId() : GetDisabledNotifierId();
+#else
+  const message_center::NotifierId notifier_id = GetNotifierId();
+#endif
   if (enable) {
     notification_id = kTailoredSecurityEnableNotificationId;
     title = l10n_util::GetStringUTF16(
@@ -184,7 +216,7 @@
       message_center::NOTIFICATION_TYPE_SIMPLE, notification_id, title,
       description, icon,
       l10n_util::GetStringUTF16(IDS_TAILORED_SECURITY_DISPLAY_SOURCE),
-      GURL(kTailoredSecurityNotificationOrigin), GetNotifierId(),
+      GURL(kTailoredSecurityNotificationOrigin), notifier_id,
       message_center::RichNotificationData(),
       /*delegate=*/nullptr);
   notification.set_buttons({message_center::ButtonInfo(primary_button),
@@ -205,6 +237,12 @@
       IDS_TAILORED_SECURITY_UNCONSENTED_PROMOTION_NOTIFICATION_ACCEPT);
   const std::u16string& secondary_button =
       l10n_util::GetStringUTF16(IDS_NO_THANKS);
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+  const message_center::NotifierId notifier_id = GetPromotionNotifierId();
+#else
+  const message_center::NotifierId notifier_id = GetNotifierId();
+#endif
+
   // TODO(crbug/1257622): Confirm with UX that it's appropriate to use the
   // blue color here.
   auto icon =
@@ -215,7 +253,7 @@
       message_center::NOTIFICATION_TYPE_SIMPLE, notification_id, title,
       description, icon,
       l10n_util::GetStringUTF16(IDS_TAILORED_SECURITY_DISPLAY_SOURCE),
-      GURL(kTailoredSecurityNotificationOrigin), GetNotifierId(),
+      GURL(kTailoredSecurityNotificationOrigin), notifier_id,
       message_center::RichNotificationData(),
       /*delegate=*/nullptr);
   notification.set_buttons({message_center::ButtonInfo(primary_button),
diff --git a/chrome/browser/safety_check/android/java/src/org/chromium/chrome/browser/safety_check/SafetyCheckMediator.java b/chrome/browser/safety_check/android/java/src/org/chromium/chrome/browser/safety_check/SafetyCheckMediator.java
index 5cb94be..2792292c 100644
--- a/chrome/browser/safety_check/android/java/src/org/chromium/chrome/browser/safety_check/SafetyCheckMediator.java
+++ b/chrome/browser/safety_check/android/java/src/org/chromium/chrome/browser/safety_check/SafetyCheckMediator.java
@@ -26,8 +26,11 @@
 import org.chromium.chrome.browser.password_check.PasswordCheck;
 import org.chromium.chrome.browser.password_check.PasswordCheckFactory;
 import org.chromium.chrome.browser.password_check.PasswordCheckUIStatus;
+import org.chromium.chrome.browser.password_manager.CredentialManagerLauncher.CredentialManagerError;
 import org.chromium.chrome.browser.password_manager.ManagePasswordsReferrer;
 import org.chromium.chrome.browser.password_manager.PasswordCheckReferrer;
+import org.chromium.chrome.browser.password_manager.PasswordCheckupClientHelper.PasswordCheckBackendException;
+import org.chromium.chrome.browser.password_manager.PasswordManagerBackendSupportHelper;
 import org.chromium.chrome.browser.password_manager.PasswordManagerHelper;
 import org.chromium.chrome.browser.password_manager.PasswordStoreBridge;
 import org.chromium.chrome.browser.password_manager.PasswordStoreCredential;
@@ -245,6 +248,17 @@
             mShowSafePasswordState = false;
             mModel.set(SafetyCheckProperties.SAFE_BROWSING_STATE, SafeBrowsingState.UNCHECKED);
             mModel.set(SafetyCheckProperties.UPDATES_STATE, UpdatesState.UNCHECKED);
+
+            // If the new Password Manager backend is out of date, attempting to fetch breached
+            // credentials will expectedly fail and display an error message. This error is
+            // designed to be only shown when user explicitly runs the check (or it was ran
+            // recently). For this case, breached credential fetch is skipped.
+            if (PasswordManagerHelper.canUseUpm()
+                    && PasswordManagerBackendSupportHelper.getInstance().isUpdateNeeded()) {
+                mLoadStage = PasswordCheckLoadStage.IDLE;
+                mModel.set(SafetyCheckProperties.PASSWORDS_STATE, PasswordsState.UNCHECKED);
+                return;
+            }
         }
         mModel.set(SafetyCheckProperties.PASSWORDS_STATE, PasswordsState.CHECKING);
         mLoadStage = PasswordCheckLoadStage.INITIAL_WAIT_FOR_LOAD;
@@ -257,6 +271,7 @@
                     PasswordsStatus.SIGNED_OUT, PasswordsStatus.MAX_VALUE + 1);
             updatePasswordElementClickDestination();
         }
+
         fetchPasswordsAndBreachedCredentials();
         if (mPasswordsLoaded && mLeaksLoaded) {
             determinePasswordStateOnLoadComplete();
@@ -543,6 +558,11 @@
                 }
                 return true;
             };
+        } else if (state == PasswordsState.BACKEND_VERSION_NOT_SUPPORTED) {
+            listener = (p) -> {
+                PasswordManagerHelper.launchGmsUpdate(p.getContext());
+                return true;
+            };
         } else {
             listener = (p) -> {
                 PasswordManagerHelper.showPasswordSettings(p.getContext(),
@@ -668,13 +688,21 @@
         if (mModel == null) return;
 
         setRunnablePasswords(() -> {
-            if (mModel != null) {
-                RecordHistogram.recordEnumeratedHistogram("Settings.SafetyCheck.PasswordsResult",
-                        SafetyCheckProperties.passwordsStateToNative(PasswordsState.ERROR),
-                        PasswordsStatus.MAX_VALUE + 1);
+            if (mModel == null) return;
+
+            RecordHistogram.recordEnumeratedHistogram("Settings.SafetyCheck.PasswordsResult",
+                    SafetyCheckProperties.passwordsStateToNative(PasswordsState.ERROR),
+                    PasswordsStatus.MAX_VALUE + 1);
+            if (error instanceof PasswordCheckBackendException
+                    && ((PasswordCheckBackendException) error).errorCode
+                            == CredentialManagerError.BACKEND_VERSION_NOT_SUPPORTED) {
+                mModel.set(SafetyCheckProperties.PASSWORDS_STATE,
+                        PasswordsState.BACKEND_VERSION_NOT_SUPPORTED);
+            } else {
                 mModel.set(SafetyCheckProperties.PASSWORDS_STATE, PasswordsState.ERROR);
-                updatePasswordElementClickDestination();
             }
+
+            updatePasswordElementClickDestination();
         });
     }
 
diff --git a/chrome/browser/safety_check/android/java/src/org/chromium/chrome/browser/safety_check/SafetyCheckProperties.java b/chrome/browser/safety_check/android/java/src/org/chromium/chrome/browser/safety_check/SafetyCheckProperties.java
index cca700e..339841a 100644
--- a/chrome/browser/safety_check/android/java/src/org/chromium/chrome/browser/safety_check/SafetyCheckProperties.java
+++ b/chrome/browser/safety_check/android/java/src/org/chromium/chrome/browser/safety_check/SafetyCheckProperties.java
@@ -41,7 +41,8 @@
 
     @IntDef({PasswordsState.UNCHECKED, PasswordsState.CHECKING, PasswordsState.SAFE,
             PasswordsState.COMPROMISED_EXIST, PasswordsState.OFFLINE, PasswordsState.NO_PASSWORDS,
-            PasswordsState.SIGNED_OUT, PasswordsState.QUOTA_LIMIT, PasswordsState.ERROR})
+            PasswordsState.SIGNED_OUT, PasswordsState.QUOTA_LIMIT, PasswordsState.ERROR,
+            PasswordsState.BACKEND_VERSION_NOT_SUPPORTED})
     @Retention(RetentionPolicy.SOURCE)
     public @interface PasswordsState {
         int UNCHECKED = 0;
@@ -53,6 +54,7 @@
         int SIGNED_OUT = 6;
         int QUOTA_LIMIT = 7;
         int ERROR = 8;
+        int BACKEND_VERSION_NOT_SUPPORTED = 9;
     }
 
     static @PasswordsState int passwordsStatefromErrorState(@PasswordCheckUIStatus int state) {
@@ -82,6 +84,11 @@
                 // This is not used.
                 assert false : "PasswordsState.UNCHECKED has no native equivalent.";
                 return PasswordsStatus.ERROR;
+            case PasswordsState.BACKEND_VERSION_NOT_SUPPORTED:
+                // This is not used.
+                assert false
+                    : "PasswordsState.BACKEND_VERSION_NOT_SUPPORTED has no native equivalent.";
+                return PasswordsStatus.ERROR;
             case PasswordsState.CHECKING:
                 return PasswordsStatus.CHECKING;
             case PasswordsState.SAFE:
diff --git a/chrome/browser/safety_check/android/java/src/org/chromium/chrome/browser/safety_check/SafetyCheckViewBinder.java b/chrome/browser/safety_check/android/java/src/org/chromium/chrome/browser/safety_check/SafetyCheckViewBinder.java
index da41040..01b6f97 100644
--- a/chrome/browser/safety_check/android/java/src/org/chromium/chrome/browser/safety_check/SafetyCheckViewBinder.java
+++ b/chrome/browser/safety_check/android/java/src/org/chromium/chrome/browser/safety_check/SafetyCheckViewBinder.java
@@ -47,6 +47,8 @@
                 return context.getResources().getQuantityString(
                         R.plurals.safety_check_passwords_compromised_exist, compromised,
                         compromised);
+            case PasswordsState.BACKEND_VERSION_NOT_SUPPORTED:
+                return context.getString(R.string.safety_check_passwords_update_play_services);
             default:
                 assert false : "Unknown PasswordsState value.";
         }
@@ -68,6 +70,7 @@
             case PasswordsState.QUOTA_LIMIT:
             case PasswordsState.OFFLINE:
             case PasswordsState.ERROR:
+            case PasswordsState.BACKEND_VERSION_NOT_SUPPORTED:
                 return R.drawable.ic_info_outline_grey_24dp;
             default:
                 assert false : "Unknown PasswordsState value.";
diff --git a/chrome/browser/safety_check/android/javatests/src/org/chromium/chrome/browser/safety_check/SafetyCheckMediatorTest.java b/chrome/browser/safety_check/android/javatests/src/org/chromium/chrome/browser/safety_check/SafetyCheckMediatorTest.java
index 7336570..c2c0273 100644
--- a/chrome/browser/safety_check/android/javatests/src/org/chromium/chrome/browser/safety_check/SafetyCheckMediatorTest.java
+++ b/chrome/browser/safety_check/android/javatests/src/org/chromium/chrome/browser/safety_check/SafetyCheckMediatorTest.java
@@ -47,6 +47,7 @@
 import org.chromium.chrome.browser.password_check.PasswordCheck;
 import org.chromium.chrome.browser.password_check.PasswordCheckFactory;
 import org.chromium.chrome.browser.password_check.PasswordCheckUIStatus;
+import org.chromium.chrome.browser.password_manager.CredentialManagerLauncher.CredentialManagerError;
 import org.chromium.chrome.browser.password_manager.PasswordCheckupClientHelper;
 import org.chromium.chrome.browser.password_manager.PasswordCheckupClientHelper.PasswordCheckBackendException;
 import org.chromium.chrome.browser.password_manager.PasswordCheckupClientHelperFactory;
@@ -113,6 +114,8 @@
     private Handler mHandler;
     @Mock
     private PasswordCheck mPasswordCheck;
+
+    // TODO(crbug.com/1346235): Use existing fake instead of mocking
     @Mock
     private PasswordCheckupClientHelper mPasswordCheckupHelper;
     @Mock
@@ -121,6 +124,8 @@
     private PrefService mPrefService;
     @Mock
     private UserPrefs.Natives mUserPrefsJniMock;
+
+    // TODO(crbug.com/1346235): Use fake instead of mocking
     @Mock
     private PasswordManagerBackendSupportHelper mBackendSupportHelperMock;
 
@@ -279,6 +284,7 @@
 
         mModel = SafetyCheckProperties.createSafetyCheckModel();
         if (mUseNewApi) {
+            // TODO(crbug.com/1346235): Use existing fake instead of mocking
             PasswordCheckupClientHelperFactory mockPasswordCheckFactory =
                     mock(PasswordCheckupClientHelperFactory.class);
             when(mockPasswordCheckFactory.createHelper()).thenReturn(mPasswordCheckupHelper);
@@ -388,6 +394,19 @@
     }
 
     @Test
+    public void testPasswordsCheckBackendOutdated() {
+        if (!mUseNewApi) return;
+        captureRunPasswordCheckCallback();
+        mMediator.performSafetyCheck();
+        mRunPasswordCheckFailedCallback.onResult(new PasswordCheckBackendException(
+                "test", CredentialManagerError.BACKEND_VERSION_NOT_SUPPORTED));
+        assertEquals(PasswordsState.BACKEND_VERSION_NOT_SUPPORTED, mModel.get(PASSWORDS_STATE));
+        assertEquals(1,
+                RecordHistogram.getHistogramValueCountForTesting(
+                        SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM, PasswordsStatus.ERROR));
+    }
+
+    @Test
     public void testPasswordsCheckNoPasswords() {
         captureRunPasswordCheckCallback();
         mMediator.performSafetyCheck();
diff --git a/chrome/browser/search_resumption/BUILD.gn b/chrome/browser/search_resumption/BUILD.gn
index e5608aa2..79d81e1 100644
--- a/chrome/browser/search_resumption/BUILD.gn
+++ b/chrome/browser/search_resumption/BUILD.gn
@@ -97,5 +97,6 @@
     "//third_party/mockito:mockito_java",
     "//ui/android:ui_no_recycler_view_java",
     "//url:gurl_java",
+    "//url:gurl_junit_test_support",
   ]
 }
diff --git a/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleCoordinator.java b/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleCoordinator.java
index 9c8b6db..58ac8fb 100644
--- a/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleCoordinator.java
+++ b/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleCoordinator.java
@@ -22,8 +22,8 @@
 
     public SearchResumptionModuleCoordinator(ViewGroup parent, Tab tabToTrack, Tab currentTab,
             Profile profile, int moduleContainerStbuId) {
-        OnSuggestionClickCallback callback = tile -> {
-            currentTab.loadUrl(new LoadUrlParams(tile.getUrl()));
+        OnSuggestionClickCallback callback = (gurl) -> {
+            currentTab.loadUrl(new LoadUrlParams(gurl));
             RecordUserAction.record(SearchResumptionModuleMediator.ACTION_CLICK);
         };
         mTileBuilder = new SearchResumptionTileBuilder(callback);
diff --git a/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleMediator.java b/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleMediator.java
index 7d3159e0..e72466aa 100644
--- a/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleMediator.java
+++ b/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleMediator.java
@@ -26,6 +26,7 @@
 import org.chromium.components.omnibox.AutocompleteResult;
 import org.chromium.ui.modelutil.PropertyModel;
 import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
+import org.chromium.url.GURL;
 
 import java.util.List;
 
@@ -45,6 +46,7 @@
     private PropertyModel mModel;
 
     private @Nullable SearchResumptionModuleView mModuleLayoutView;
+    private @Nullable SearchResumptionModuleBridge mSearchResumptionModuleBridge;
 
     SearchResumptionModuleMediator(ViewStub moduleStub, Tab tabToTrack, Profile profile,
             SearchResumptionTileBuilder tileBuilder) {
@@ -78,22 +80,34 @@
     }
 
     /**
+     * Called when the search suggestions are available using the new service API.
+     * @param suggestionTexts The display texts of the suggestions.
+     * @param suggestionUrls The URLs of the suggestions.
+     */
+    void onSuggestionsAvailable(String[] suggestionTexts, GURL[] suggestionUrls) {
+        if (mModel != null || !shouldShowSuggestionModule(suggestionUrls, suggestionTexts)) {
+            return;
+        }
+        showSearchSuggestionModule(suggestionTexts, suggestionUrls);
+    }
+
+    /**
      * Inflates the search_resumption_layout and shows the suggestions on the module.
      * @param autocompleteResult The suggestions to show on the module.
      */
     void showSearchSuggestionModule(AutocompleteResult autocompleteResult) {
-        if (mModel != null) return;
-
-        mModuleLayoutView = (SearchResumptionModuleView) mStub.inflate();
-        mModel = new PropertyModel(SearchResumptionModuleProperties.ALL_KEYS);
-        PropertyModelChangeProcessor.create(
-                mModel, mModuleLayoutView, new SearchResumptionModuleViewBinder());
-
+        if (!initializeModule()) return;
         mTileBuilder.buildSuggestionTile(autocompleteResult.getSuggestionsList(),
                 mModuleLayoutView.findViewById(R.id.search_resumption_module_tiles_container));
-        mModel.set(SearchResumptionModuleProperties.EXPAND_COLLAPSE_CLICK_CALLBACK,
-                this::onExpandedOrCollapsed);
-        RecordUserAction.record(ACTION_SHOW);
+    }
+
+    /**
+     * Inflates the search_resumption_layout and shows the suggestions on the module.
+     */
+    void showSearchSuggestionModule(String[] texts, GURL[] urls) {
+        if (!initializeModule()) return;
+        mTileBuilder.buildSuggestionTile(texts, urls,
+                mModuleLayoutView.findViewById(R.id.search_resumption_module_tiles_container));
     }
 
     void destroy() {
@@ -103,6 +117,9 @@
         if (mModuleLayoutView != null) {
             mModuleLayoutView.destroy();
         }
+        if (mSearchResumptionModuleBridge != null) {
+            mSearchResumptionModuleBridge.destroy();
+        }
         TemplateUrlServiceFactory.get().removeObserver(this::onTemplateURLServiceChanged);
         mSignInManager.removeSignInStateObserver(this);
     }
@@ -120,7 +137,9 @@
             mAutoComplete.startZeroSuggest("", mTabToTrackSuggestion.getUrl().getSpec(),
                     pageClassification, mTabToTrackSuggestion.getTitle());
         } else {
-            // TODO(hanxi): Switches to use a new KeyedService instead of Autocomplete API.
+            mSearchResumptionModuleBridge = new SearchResumptionModuleBridge(profile);
+            mSearchResumptionModuleBridge.fetchSuggestions(
+                    mTabToTrackSuggestion.getUrl().getSpec(), this::onSuggestionsAvailable);
         }
     }
 
@@ -156,6 +175,45 @@
         return false;
     }
 
+    /**
+     * Returns whether to show the search resumption module. Only showing the module if at least
+     * {@link SearchResumptionTileBuilder#MAX_TILES_NUMBER} -1 suggestions are given.
+     */
+    private boolean shouldShowSuggestionModule(GURL[] urls, String[] texts) {
+        if (urls.length != texts.length
+                || urls.length < SearchResumptionTileBuilder.MAX_TILES_NUMBER - 1) {
+            return false;
+        }
+
+        int count = 0;
+        for (int i = 0; i < urls.length; i++) {
+            if (SearchResumptionTileBuilder.isSuggestionValid(texts[i])) {
+                count++;
+            }
+            if (count >= SearchResumptionTileBuilder.MAX_TILES_NUMBER - 1) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Inflates the module and initializes the property model.
+     * @return Whether the module is inflated.
+     */
+    private boolean initializeModule() {
+        if (mModel != null) return false;
+
+        mModuleLayoutView = (SearchResumptionModuleView) mStub.inflate();
+        mModel = new PropertyModel(SearchResumptionModuleProperties.ALL_KEYS);
+        PropertyModelChangeProcessor.create(
+                mModel, mModuleLayoutView, new SearchResumptionModuleViewBinder());
+        mModel.set(SearchResumptionModuleProperties.EXPAND_COLLAPSE_CLICK_CALLBACK,
+                this::onExpandedOrCollapsed);
+        RecordUserAction.record(ACTION_SHOW);
+        return true;
+    }
+
     private void setVisibility(boolean isVisible) {
         if (mModel != null) {
             mModel.set(SearchResumptionModuleProperties.IS_VISIBLE, isVisible);
diff --git a/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionTileBuilder.java b/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionTileBuilder.java
index 953c1a7a..6feca4a 100644
--- a/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionTileBuilder.java
+++ b/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionTileBuilder.java
@@ -10,6 +10,7 @@
 import org.chromium.base.TraceEvent;
 import org.chromium.chrome.browser.omnibox.OmniboxSuggestionType;
 import org.chromium.components.omnibox.AutocompleteMatch;
+import org.chromium.url.GURL;
 
 import java.util.List;
 
@@ -26,7 +27,7 @@
      *  The callback when a {@link SearchResumptionTileView} is clicked.
      */
     interface OnSuggestionClickCallback {
-        void onSuggestionClick(AutocompleteMatch tile);
+        void onSuggestionClick(GURL gurl);
     }
 
     public SearchResumptionTileBuilder(OnSuggestionClickCallback callback) {
@@ -38,11 +39,18 @@
      * OmniboxSuggestionType.SEARCH_SUGGEST}.
      */
     static boolean isSearchSuggestion(AutocompleteMatch suggestion) {
-        return !TextUtils.isEmpty(suggestion.getDisplayText())
+        return isSuggestionValid(suggestion.getDisplayText())
                 && suggestion.getType() == OmniboxSuggestionType.SEARCH_SUGGEST;
     }
 
     /**
+     * Returns Whether the given suggestion is a qualified suggestion.
+     */
+    static boolean isSuggestionValid(String text) {
+        return !TextUtils.isEmpty(text);
+    }
+
+    /**
      * Iterators the suggestions and chooses the top MAX_TILES_NUMBER ones or less depending on the
      * number of available suggestions to build on the parent ViewGroup.
      */
@@ -75,12 +83,52 @@
     }
 
     /**
+     * Iterators the suggestions and chooses the top MAX_TILES_NUMBER ones or less depending on the
+     * number of available suggestions to build on the parent ViewGroup.
+     */
+    void buildSuggestionTile(
+            String[] texts, GURL[] urls, SearchResumptionTileContainerView parent) {
+        try (TraceEvent e = TraceEvent.scoped("SearchSuggestionTileProvider.addTileSection")) {
+            assert parent.getChildCount() == 0;
+
+            int suggestionCount = urls.length;
+            int visibleTilesCount = Math.min(suggestionCount, MAX_TILES_NUMBER);
+            int tileIndex = 0;
+            int suggestionIndex = 0;
+            while (tileIndex < visibleTilesCount && suggestionIndex < urls.length) {
+                if (!isSuggestionValid(texts[suggestionIndex])) {
+                    suggestionIndex++;
+                    continue;
+                }
+                SearchResumptionTileView tileView =
+                        buildTileView(texts[suggestionIndex], urls[suggestionIndex], parent);
+                parent.addView(tileView);
+                tileIndex++;
+                suggestionIndex++;
+            }
+
+            int childSize = parent.getChildCount();
+            for (int i = 0; i < childSize; i++) {
+                ((SearchResumptionTileView) parent.getChildAt(i)).mayUpdateBackground(i, childSize);
+            }
+        }
+    }
+
+    /**
      * Builds a {@link SearchResumptionTileView} based on the given suggestion.
      */
     SearchResumptionTileView buildTileView(
             AutocompleteMatch suggestion, SearchResumptionTileContainerView parent) {
+        return buildTileView(suggestion.getDisplayText(), suggestion.getUrl(), parent);
+    }
+
+    /**
+     * Builds a {@link SearchResumptionTileView} based on the given suggestion.
+     */
+    SearchResumptionTileView buildTileView(
+            String text, GURL url, SearchResumptionTileContainerView parent) {
         SearchResumptionTileView tileView = parent.buildTileView();
-        tileView.updateSuggestionData(suggestion);
+        tileView.updateSuggestionData(url, text);
         tileView.addOnSuggestionClickCallback(mCallback);
         return tileView;
     }
diff --git a/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionTileView.java b/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionTileView.java
index 649286a7..63a5cec 100644
--- a/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionTileView.java
+++ b/chrome/browser/search_resumption/java/src/org/chromium/chrome/browser/search_resumption/SearchResumptionTileView.java
@@ -13,13 +13,13 @@
 import androidx.core.content.ContextCompat;
 
 import org.chromium.chrome.browser.search_resumption.SearchResumptionTileBuilder.OnSuggestionClickCallback;
-import org.chromium.components.omnibox.AutocompleteMatch;
+import org.chromium.url.GURL;
 
 /**
  * The view for a search suggestion tile.
  */
 public class SearchResumptionTileView extends RelativeLayout {
-    private AutocompleteMatch mSearchTile;
+    private GURL mGurl;
     private TextView mTileContent;
 
     public SearchResumptionTileView(Context context, @Nullable AttributeSet attrs) {
@@ -35,14 +35,14 @@
     /**
      * Updates the content of the tile.
      */
-    void updateSuggestionData(AutocompleteMatch suggestion) {
-        mSearchTile = suggestion;
-        mTileContent.setText(suggestion.getDisplayText());
+    void updateSuggestionData(GURL gUrl, String displayText) {
+        mGurl = gUrl;
+        mTileContent.setText(displayText);
         setContentDescription(mTileContent.getText());
     }
 
     void addOnSuggestionClickCallback(OnSuggestionClickCallback callback) {
-        setOnClickListener(v -> callback.onSuggestionClick(mSearchTile));
+        setOnClickListener(v -> callback.onSuggestionClick(mGurl));
     }
 
     /**
diff --git a/chrome/browser/search_resumption/junit/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleMediatorUnitTest.java b/chrome/browser/search_resumption/junit/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleMediatorUnitTest.java
index 9c977162..fe46238 100644
--- a/chrome/browser/search_resumption/junit/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleMediatorUnitTest.java
+++ b/chrome/browser/search_resumption/junit/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleMediatorUnitTest.java
@@ -197,6 +197,26 @@
 
     @Test
     @MediumTest
+    public void testDoNotBuildModuleWithoutEnoughSuggestions_newServiceAPI() {
+        String[] texts = {"suggestion 1"};
+        GURL[] gUrls = {createMockGurl("foo.com")};
+
+        mMediator.onSuggestionsAvailable(texts, gUrls);
+        verify(mParent, times(0)).inflate();
+    }
+    @Test
+    @MediumTest
+    public void testShowModuleWithEnoughResults_newServiceAPI() {
+        String[] texts = {"suggestion 1", "suggestion2"};
+        GURL[] gUrls = {createMockGurl("foo.com"), createMockGurl("bar.com")};
+
+        mMediator.onSuggestionsAvailable(texts, gUrls);
+        verify(mParent, times(1)).inflate();
+        Assert.assertEquals(View.VISIBLE, mSuggestionTilesContainerView.getVisibility());
+    }
+
+    @Test
+    @MediumTest
     public void testModuleVisibility() {
         testShowModuleWithEnoughResults();
         mMediator.onSignedOut();
diff --git a/chrome/browser/search_resumption/junit/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleViewUnitTest.java b/chrome/browser/search_resumption/junit/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleViewUnitTest.java
index 85801671..a2c481b 100644
--- a/chrome/browser/search_resumption/junit/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleViewUnitTest.java
+++ b/chrome/browser/search_resumption/junit/src/org/chromium/chrome/browser/search_resumption/SearchResumptionModuleViewUnitTest.java
@@ -4,7 +4,6 @@
 
 package org.chromium.chrome.browser.search_resumption;
 
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -29,9 +28,10 @@
 import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
 import org.chromium.chrome.browser.search_resumption.SearchResumptionTileBuilder.OnSuggestionClickCallback;
-import org.chromium.components.omnibox.AutocompleteMatch;
 import org.chromium.ui.modelutil.PropertyModel;
 import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
+import org.chromium.url.GURL;
+import org.chromium.url.JUnitTestGURLs;
 
 /** Tests for {@link SearchResumptionModuleView}. */
 @RunWith(BaseRobolectricTestRunner.class)
@@ -138,10 +138,9 @@
     public void testTileView() {
         SearchResumptionTileView tileView = inflateTileView();
         String text = "foo";
-        AutocompleteMatch match = Mockito.mock(AutocompleteMatch.class);
-        doReturn(text).when(match).getDisplayText();
+        GURL gUrl = JUnitTestGURLs.getGURL(JUnitTestGURLs.EXAMPLE_URL);
 
-        tileView.updateSuggestionData(match);
+        tileView.updateSuggestionData(gUrl, text);
         Assert.assertEquals(text, tileView.getTextForTesting());
         Assert.assertEquals(text, tileView.getContentDescription());
 
diff --git a/chrome/browser/search_resumption/junit/src/org/chromium/chrome/browser/search_resumption/SearchResumptionTileBuilderUnitTest.java b/chrome/browser/search_resumption/junit/src/org/chromium/chrome/browser/search_resumption/SearchResumptionTileBuilderUnitTest.java
index d35f3438..ddb11d9 100644
--- a/chrome/browser/search_resumption/junit/src/org/chromium/chrome/browser/search_resumption/SearchResumptionTileBuilderUnitTest.java
+++ b/chrome/browser/search_resumption/junit/src/org/chromium/chrome/browser/search_resumption/SearchResumptionTileBuilderUnitTest.java
@@ -118,8 +118,8 @@
     }
 
     private void createTileBuilder() {
-        OnSuggestionClickCallback callback = tile -> {
-            mTab.loadUrl(new LoadUrlParams(tile.getUrl()));
+        OnSuggestionClickCallback callback = (gUrl) -> {
+            mTab.loadUrl(new LoadUrlParams(gUrl));
             RecordUserAction.record(SearchResumptionModuleMediator.ACTION_CLICK);
         };
         mTileBuilder = new SearchResumptionTileBuilder(callback);
diff --git a/chrome/browser/ui/android/autofill/autofill_popup_view_android.cc b/chrome/browser/ui/android/autofill/autofill_popup_view_android.cc
index 7633478..b2762ad 100644
--- a/chrome/browser/ui/android/autofill/autofill_popup_view_android.cc
+++ b/chrome/browser/ui/android/autofill/autofill_popup_view_android.cc
@@ -107,7 +107,7 @@
         base::android::ConvertUTF16ToJavaString(env, suggestion.offer_label);
     Java_AutofillPopupBridge_addToAutofillSuggestionArray(
         env, data_array, i, value, label, item_tag, android_icon_id,
-        /*icon_at_start=*/false, suggestion.frontend_id, is_deletable,
+        suggestion.is_icon_at_start, suggestion.frontend_id, is_deletable,
         is_label_multiline, /*isLabelBold*/ false,
         url::GURLAndroid::FromNativeGURL(env, suggestion.custom_icon_url));
   }
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
index 809cac6..be6a443 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -1447,6 +1447,9 @@
       <message name="IDS_SAFETY_CHECK_PASSWORDS_ERROR_QUOTA_LIMIT" desc="Text to display when the password check hits the daily quota limit.">
         Chrome couldn’t check all passwords
       </message>
+      <message name="IDS_SAFETY_CHECK_PASSWORDS_UPDATE_PLAY_SERVICES" desc="Text to display when the password check is unable to run because Google Play services version is not supported.">
+        Update Google Play services to check your passwords
+      </message>
       <message name="IDS_SAFETY_CHECK_UPDATES_UPDATED" desc="Text to display when the updates check confirms that the latest version is already installed.">
         Chrome is up to date
       </message>
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_SAFETY_CHECK_PASSWORDS_UPDATE_PLAY_SERVICES.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_SAFETY_CHECK_PASSWORDS_UPDATE_PLAY_SERVICES.png.sha1
new file mode 100644
index 0000000..1776f4a
--- /dev/null
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_SAFETY_CHECK_PASSWORDS_UPDATE_PLAY_SERVICES.png.sha1
@@ -0,0 +1 @@
+409c88ac939613e3995340b7d3b8518103009465
\ No newline at end of file
diff --git a/chrome/browser/ui/app_list/chrome_app_list_model_updater.cc b/chrome/browser/ui/app_list/chrome_app_list_model_updater.cc
index f40a43ac..c2b7b8714 100644
--- a/chrome/browser/ui/app_list/chrome_app_list_model_updater.cc
+++ b/chrome/browser/ui/app_list/chrome_app_list_model_updater.cc
@@ -365,15 +365,33 @@
     const std::string& id,
     const gfx::ImageSkia& icon,
     const ash::IconColor& icon_color) {
+  if (icon.isNull())
+    return;
+
+  // TODO(https://crbug.com/1346386): it is awkward to check both `chrome_item`
+  // and `item`. Maybe remove one of them or both after investigation.
   ChromeAppListItem* chrome_item = FindItem(id);
   if (!chrome_item)
     return;
 
+  ash::AppListItem* item = model_.FindItem(id);
+  if (!item)
+    return;
+
   base::AutoReset auto_reset(&item_with_icon_update_, chrome_item->id());
 
-  std::unique_ptr<ash::AppListItemMetadata> data = chrome_item->CloneMetadata();
-  data->icon = icon;
-  data->icon_color = icon_color;
+  const bool color_change = (icon_color != item->GetDefaultIconColor());
+
+  // Two similar icons may generate the same extracted icon color value.
+  // Therefore, always update the app list item icon.
+  item->SetDefaultIconAndColor(icon, icon_color);
+
+  // Sync the icon color if the color changes. Note that the icon is not synced.
+  // Therefore, we only check whether the color changes here.
+  if (color_change)
+    OnAppListItemUpdated(item);
+
+  std::unique_ptr<ash::AppListItemMetadata> data = item->CloneMetadata();
   MaybeUpdatePositionWhenIconColorChange(data.get());
 
   model_.SetItemMetadata(id, std::move(data));
diff --git a/chrome/browser/ui/ash/projector/projector_client_impl.cc b/chrome/browser/ui/ash/projector/projector_client_impl.cc
index 10cb8cc2..952a2467 100644
--- a/chrome/browser/ui/ash/projector/projector_client_impl.cc
+++ b/chrome/browser/ui/ash/projector/projector_client_impl.cc
@@ -295,7 +295,15 @@
   if (!is_enabled)
     CloseProjectorApp();
 
-  auto* web_app_provider = web_app::WebAppProvider::GetForWebApps(profile);
-  web_app_provider->sync_bridge().SetAppIsDisabled(
-      ash::kChromeUITrustedProjectorSwaAppId, !is_enabled);
+  auto* web_app_provider = ash::SystemWebAppManager::GetWebAppProvider(profile);
+  web_app_provider->on_registry_ready().Post(
+      FROM_HERE, base::BindOnce(&ProjectorClientImpl::SetAppIsDisabled,
+                                weak_ptr_factory_.GetWeakPtr(),
+                                web_app_provider, !is_enabled));
+}
+
+void ProjectorClientImpl::SetAppIsDisabled(web_app::WebAppProvider* provider,
+                                           bool disabled) {
+  provider->sync_bridge().SetAppIsDisabled(
+      ash::kChromeUITrustedProjectorSwaAppId, disabled);
 }
diff --git a/chrome/browser/ui/ash/projector/projector_client_impl.h b/chrome/browser/ui/ash/projector/projector_client_impl.h
index 66bc785..a47666a7 100644
--- a/chrome/browser/ui/ash/projector/projector_client_impl.h
+++ b/chrome/browser/ui/ash/projector/projector_client_impl.h
@@ -26,6 +26,10 @@
 class WebView;
 }  // namespace views
 
+namespace web_app {
+class WebAppProvider;
+}  // namespace web_app
+
 class OnDeviceSpeechRecognizer;
 
 // The client implementation for the ProjectorController in ash/. This client is
@@ -103,6 +107,9 @@
   // app is enabled.
   void OnEnablementPolicyChanged();
 
+  // Called when app registry becomes ready.
+  void SetAppIsDisabled(web_app::WebAppProvider* provider, bool disabled);
+
   ash::ProjectorController* const controller_;
   ash::AnnotatorMessageHandler* message_handler_ = nullptr;
   SpeechRecognizerStatus recognizer_status_ =
diff --git a/chrome/browser/ui/ash/projector/projector_client_impl_browsertest.cc b/chrome/browser/ui/ash/projector/projector_client_impl_browsertest.cc
index e32c7e20..27b6500 100644
--- a/chrome/browser/ui/ash/projector/projector_client_impl_browsertest.cc
+++ b/chrome/browser/ui/ash/projector/projector_client_impl_browsertest.cc
@@ -384,6 +384,12 @@
   profile->GetPrefs()->SetBoolean(GetPolicy(), false);
   // The Projector app immediately closes to prevent further access.
   EXPECT_TRUE(app_browser->IsAttemptingToCloseBrowser());
+
+  auto* web_app_provider = web_app::WebAppProvider::GetForTest(profile);
+  base::RunLoop loop;
+  web_app_provider->on_registry_ready().Post(FROM_HERE, loop.QuitClosure());
+  loop.Run();
+
   // We can't uninstall the Projector SWA until the next session, but the icon
   // is greyed out and disabled.
   EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
@@ -394,6 +400,11 @@
   // The app can re-enable too if it's already installed and the policy flips to
   // true.
   profile->GetPrefs()->SetBoolean(GetPolicy(), true);
+
+  base::RunLoop loop2;
+  web_app_provider->on_registry_ready().Post(FROM_HERE, loop2.QuitClosure());
+  loop2.Run();
+
   EXPECT_EQ(apps::Readiness::kReady,
             GetAppReadiness(kChromeUITrustedProjectorSwaAppId));
   EXPECT_FALSE(apps::IconEffects::kBlocked &
diff --git a/chrome/browser/ui/ash/system_tray_tray_cast_browsertest_media_router_chromeos.cc b/chrome/browser/ui/ash/system_tray_tray_cast_browsertest_media_router_chromeos.cc
index 08de5f2..54bfc54 100644
--- a/chrome/browser/ui/ash/system_tray_tray_cast_browsertest_media_router_chromeos.cc
+++ b/chrome/browser/ui/ash/system_tray_tray_cast_browsertest_media_router_chromeos.cc
@@ -5,19 +5,38 @@
 #include <memory>
 #include <vector>
 
+#include "ash/components/login/auth/public/key.h"
+#include "ash/components/login/auth/public/user_context.h"
 #include "ash/public/cpp/ash_view_ids.h"
 #include "ash/public/cpp/cast_config_controller.h"
+#include "ash/public/cpp/system_tray_client.h"
 #include "ash/public/cpp/system_tray_test_api.h"
+#include "ash/shelf/shelf.h"
+#include "ash/shelf/shelf_widget.h"
+#include "ash/shell.h"
+#include "ash/system/cast/tray_cast.h"
+#include "ash/system/cast/unified_cast_detailed_view_controller.h"
+#include "ash/system/model/system_tray_model.h"
+#include "ash/system/status_area_widget.h"
+#include "ash/system/unified/unified_system_tray.h"
+#include "ash/system/unified/unified_system_tray_bubble.h"
+#include "ash/system/unified/unified_system_tray_view.h"
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/ash/login/login_manager_test.h"
+#include "chrome/browser/ash/login/session/user_session_manager.h"
+#include "chrome/browser/ash/login/session/user_session_manager_test_api.h"
 #include "chrome/browser/ash/login/test/login_manager_mixin.h"
+#include "chrome/browser/ash/login/ui/login_display_host_webui.h"
 #include "chrome/browser/ash/profiles/profile_helper.h"
 #include "chrome/browser/media/router/discovery/access_code/access_code_cast_feature.h"
 #include "chrome/browser/media/router/media_router_feature.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/ash/cast_config_controller_media_router.h"
 #include "chrome/browser/ui/ui_features.h"
+#include "chrome/test/base/chrome_test_utils.h"
 #include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/media_router/access_code_cast/access_code_cast_integration_browsertest.h"
+#include "components/access_code_cast/common/access_code_cast_metrics.h"
 #include "components/account_id/account_id.h"
 #include "components/media_router/browser/media_routes_observer.h"
 #include "components/media_router/browser/media_sinks_observer.h"
@@ -26,8 +45,12 @@
 #include "components/media_router/common/test/test_helper.h"
 #include "components/prefs/pref_service.h"
 #include "components/user_manager/user_manager.h"
+#include "components/vector_icons/vector_icons.h"
 #include "content/public/test/browser_test.h"
+#include "content/public/test/test_host_resolver.h"
 #include "content/public/test/test_utils.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/events/test/event_generator.h"
 #include "ui/message_center/message_center.h"
 #include "url/gurl.h"
 
@@ -219,11 +242,10 @@
   EXPECT_FALSE(IsCastingNotificationVisible());
 }
 
-class SystemTrayTrayCastAccessCodeChromeOSTest : public ash::LoginManagerTest {
+class SystemTrayTrayCastAccessCodeChromeOSTest
+    : public media_router::AccessCodeCastIntegrationBrowserTest {
  public:
-  SystemTrayTrayCastAccessCodeChromeOSTest() : LoginManagerTest() {
-    scoped_feature_list_.InitAndEnableFeature(::features::kAccessCodeCastUI);
-
+  SystemTrayTrayCastAccessCodeChromeOSTest() {
     // Use consumer emails to avoid having to fake a policy fetch.
     login_mixin_.AppendRegularUsers(2);
     account_id1_ = login_mixin_.users()[0].account_id;
@@ -237,20 +259,55 @@
 
   ~SystemTrayTrayCastAccessCodeChromeOSTest() override = default;
 
+  void PreRunTestOnMainThread() override {
+    CastConfigControllerMediaRouter::SetMediaRouterForTest(media_router_);
+    InProcessBrowserTest::PreRunTestOnMainThread();
+  }
+
   void SetUpOnMainThread() override {
-    LoginManagerTest::SetUpOnMainThread();
+    ash::LoginDisplayHostWebUI::DisableRestrictiveProxyCheckForTest();
+
+    ash::test::UserSessionManagerTestApi session_manager_test_api(
+        ash::UserSessionManager::GetInstance());
+    session_manager_test_api.SetShouldLaunchBrowserInTests(false);
+    session_manager_test_api.SetShouldObtainTokenHandleInTests(false);
+
+    AccessCodeCastIntegrationBrowserTest::SetUpOnMainThread();
+    MixinBasedInProcessBrowserTest::SetUpOnMainThread();
     tray_test_api_ = ash::SystemTrayTestApi::Create();
+
+    event_generator_ = std::make_unique<ui::test::EventGenerator>(  // IN-TEST
+        ash::Shell::GetPrimaryRootWindow());
+  }
+
+  ui::test::EventGenerator* GetEventGenerator() {
+    return event_generator_.get();
   }
 
  protected:
   void SetupUserProfile(const AccountId& account_id, bool allow_access_code) {
-    const user_manager::User* user = UserManager::Get()->FindUser(account_id);
-    Profile* profile = ProfileHelper::Get()->GetProfileByUser(user);
+    user_ = UserManager::Get()->FindUser(account_id);
+    Profile* profile = ProfileHelper::Get()->GetProfileByUser(user_);
     profile->GetPrefs()->SetBoolean(media_router::prefs::kAccessCodeCastEnabled,
                                     allow_access_code);
     content::RunAllTasksUntilIdle();
   }
 
+  ash::UserContext CreateUserContext(const AccountId& account_id,
+                                     const std::string& password) {
+    ash::UserContext user_context(user_manager::UserType::USER_TYPE_REGULAR,
+                                  account_id);
+    user_context.SetKey(ash::Key(password));
+    user_context.SetPasswordKey(ash::Key(password));
+    if (account_id.GetUserEmail() == ash::FakeGaiaMixin::kEnterpriseUser1) {
+      user_context.SetRefreshToken(ash::FakeGaiaMixin::kTestRefreshToken1);
+    } else if (account_id.GetUserEmail() ==
+               ash::FakeGaiaMixin::kEnterpriseUser2) {
+      user_context.SetRefreshToken(ash::FakeGaiaMixin::kTestRefreshToken2);
+    }
+    return user_context;
+  }
+
   void ShowBubble() { tray_test_api_->ShowBubble(); }
 
   void CloseBubble() { tray_test_api_->CloseBubble(); }
@@ -263,19 +320,57 @@
 
   bool IsTrayVisible() { return IsViewDrawn(ash::VIEW_ID_CAST_MAIN_VIEW); }
 
+  // Returns the status area widget.
+  ash::StatusAreaWidget* FindStatusAreaWidget() {
+    return ash::Shelf::ForWindow(ash::Shell::GetRootWindowForNewWindows())
+        ->shelf_widget()
+        ->status_area_widget();
+  }
+
+  // Performs a tap of the specified |view|.
+  void TapOn(const views::View* view) {
+    GetEventGenerator()->MoveTouch(view->GetBoundsInScreen().CenterPoint());
+    GetEventGenerator()->ClickLeftButton();
+  }
+
+  ash::CastDetailedView* GetCastDetailedView() {
+    return GetUnifiedCastDetailedViewController()
+        ->get_cast_detailed_view_for_testing();  // IN-TEST
+  }
+
+  ash::UnifiedSystemTrayController* GetUnifiedSystemTrayController() {
+    return FindStatusAreaWidget()
+        ->unified_system_tray()
+        ->bubble()
+        ->unified_system_tray_controller();
+  }
+
+  ash::UnifiedCastDetailedViewController*
+  GetUnifiedCastDetailedViewController() {
+    return static_cast<ash::UnifiedCastDetailedViewController*>(
+        GetUnifiedSystemTrayController()->detailed_view_controller());
+  }
+
   AccountId account_id1_;
   AccountId account_id2_;
 
- private:
   ash::LoginManagerMixin login_mixin_{&mixin_host_};
+
+  std::unique_ptr<ui::test::EventGenerator> event_generator_;
+  const user_manager::User* user_;
+
+ private:
   std::unique_ptr<ash::SystemTrayTestApi> tray_test_api_;
   base::test::ScopedFeatureList scoped_feature_list_;
 };
 
 IN_PROC_BROWSER_TEST_F(SystemTrayTrayCastAccessCodeChromeOSTest,
                        PolicyOffNoSinksNoVisibleTray) {
+  const ash::UserContext user_context =
+      CreateUserContext(account_id1_, "password");
+
   // Login a user that does not have access code casting enabled
-  LoginUser(account_id1_);
+  ASSERT_TRUE(login_mixin_.LoginAndWaitForActiveSession(user_context));
   SetupUserProfile(account_id1_, /* allow_access_code */ false);
 
   ShowBubble();
@@ -287,13 +382,88 @@
 
 IN_PROC_BROWSER_TEST_F(SystemTrayTrayCastAccessCodeChromeOSTest,
                        PolicyOnNoSinksVisibleTray) {
-  // Login a user that does not have access code casting enabled
-  LoginUser(account_id2_);
+  const ash::UserContext user_context =
+      CreateUserContext(account_id2_, "password");
+
+  // Login a user that does have access code casting enabled
+  ASSERT_TRUE(login_mixin_.LoginAndWaitForActiveSession(user_context));
   SetupUserProfile(account_id2_, /* allow_access_code */ true);
 
   ShowBubble();
 
   // Even though there are no sinks, since this user does have access code
-  // casting enabled, the tray should not be visible.
+  // casting enabled, the tray should be visible.
   EXPECT_TRUE(IsTrayVisible());
 }
+
+IN_PROC_BROWSER_TEST_F(SystemTrayTrayCastAccessCodeChromeOSTest,
+                       SimulateValidCastingWorkflow) {
+  const ash::UserContext user_context =
+      CreateUserContext(account_id2_, "password");
+
+  // Login a user that does have access code casting enabled
+  ASSERT_TRUE(login_mixin_.LoginAndWaitForActiveSession(user_context));
+  SetupUserProfile(account_id2_, /* allow_access_code */ true);
+
+  ShowBubble();
+
+  // Show the Cast detailed view menu.
+  GetUnifiedSystemTrayController()->ShowCastDetailedView();
+
+  auto* detailed_cast_view = GetCastDetailedView();
+  ASSERT_TRUE(detailed_cast_view);
+
+  auto* access_code_cast_button =
+      detailed_cast_view->get_add_access_code_device_for_testing();  // IN-TEST
+  ASSERT_TRUE(access_code_cast_button);
+  ASSERT_TRUE(access_code_cast_button->GetEnabled());
+
+  const char kEndpointResponseSuccess[] =
+      R"({
+      "device": {
+        "displayName": "test_device",
+        "id": "1234",
+        "deviceCapabilities": {
+          "videoOut": true,
+          "videoIn": true,
+          "audioOut": true,
+          "audioIn": true,
+          "devMode": true
+        },
+        "networkInfo": {
+          "hostName": "GoogleNet",
+          "port": "666",
+          "ipV4Address": "192.0.2.146",
+          "ipV6Address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
+        }
+      }
+    })";
+
+  // Mock a successful fetch from our server.
+  SetEndpointFetcherMockResponse(kEndpointResponseSuccess, net::HTTP_OK,
+                                 net::OK);
+
+  // Simulate a successful opening of the channel.
+  SetMockOpenChannelCallbackResponse(true);
+
+  SetUpPrimaryAccountWithHostedDomain(
+      signin::ConsentLevel::kSync,
+      ProfileHelper::Get()->GetProfileByUser(user_));
+
+  content::WebContentsAddedObserver observer;
+  TapOn(access_code_cast_button);
+
+  content::WebContents* dialog_contents = observer.GetWebContents();
+  ASSERT_TRUE(content::WaitForLoadStop(dialog_contents));
+
+  SetAccessCode("abcdef", dialog_contents);
+
+  // TODO(crbug.com/1291738): There is a validation process with desktop media
+  // requests which are unnecessary for the complexity of this browsertest. We
+  // are just passing in a hardcoded magic string instead.
+  ExpectStartRouteCallFromTabMirroring(
+      "cast:<1234>", "urn:x-org.chromium.media:source:desktop", nullptr,
+      base::Seconds(120), media_router_);
+
+  PressSubmitAndWaitForClose(dialog_contents);
+}
diff --git a/chrome/browser/ui/views/accessibility/caption_bubble_controller_views_browsertest.cc b/chrome/browser/ui/views/accessibility/caption_bubble_controller_views_browsertest.cc
index f466ec3..6afe1fc 100644
--- a/chrome/browser/ui/views/accessibility/caption_bubble_controller_views_browsertest.cc
+++ b/chrome/browser/ui/views/accessibility/caption_bubble_controller_views_browsertest.cc
@@ -29,6 +29,7 @@
 #include "ui/views/controls/button/image_button.h"
 #include "ui/views/controls/image_view.h"
 #include "ui/views/controls/label.h"
+#include "ui/views/test/widget_test.h"
 #include "ui/views/widget/widget.h"
 
 #if defined(USE_AURA)
@@ -1059,8 +1060,12 @@
   test_task_runner->FastForwardBy(base::Seconds(4));
   EXPECT_TRUE(IsWidgetVisible());
 
-  // TODO(crbug.com/1055150): Test that widget doesn't hide when focused. It
-  // works in app but the tests aren't working.
+  // Test that widget doesn't hide when focused.
+  views::test::WidgetActivationWaiter waiter(GetCaptionWidget(), true);
+  GetCaptionWidget()->Activate();
+  waiter.Wait();
+  test_task_runner->FastForwardBy(base::Seconds(10));
+  EXPECT_TRUE(IsWidgetVisible());
 }
 
 // TODO(https://crbug.com/1207312): Flaky test.
diff --git a/chrome/browser/ui/views/omnibox/omnibox_popup_contents_view.cc b/chrome/browser/ui/views/omnibox/omnibox_popup_contents_view.cc
index 9cc06e2..c105c05 100644
--- a/chrome/browser/ui/views/omnibox/omnibox_popup_contents_view.cc
+++ b/chrome/browser/ui/views/omnibox/omnibox_popup_contents_view.cc
@@ -355,8 +355,10 @@
     // Show the header if it's distinct from the previous match's header.
     const AutocompleteMatch& match = GetMatchAtIndex(i);
     std::u16string current_row_header =
-        edit_model_->result().GetHeaderForSuggestionGroup(
-            match.suggestion_group_id.value_or(kInvalidSuggestionGroupId));
+        match.suggestion_group_id.has_value()
+            ? edit_model_->result().GetHeaderForSuggestionGroup(
+                  match.suggestion_group_id.value())
+            : u"";
     if (!current_row_header.empty() &&
         current_row_header != previous_row_header) {
       row_view->ShowHeader(match.suggestion_group_id.value(),
diff --git a/chrome/browser/ui/views/omnibox/omnibox_row_view.cc b/chrome/browser/ui/views/omnibox/omnibox_row_view.cc
index e8c872af..9d4b2e3 100644
--- a/chrome/browser/ui/views/omnibox/omnibox_row_view.cc
+++ b/chrome/browser/ui/views/omnibox/omnibox_row_view.cc
@@ -17,6 +17,7 @@
 #include "components/omnibox/browser/omnibox_edit_model.h"
 #include "components/omnibox/browser/omnibox_popup_selection.h"
 #include "components/omnibox/browser/omnibox_prefs.h"
+#include "components/omnibox/browser/suggestion_group.h"
 #include "components/omnibox/browser/vector_icons.h"
 #include "components/prefs/pref_change_registrar.h"
 #include "components/prefs/pref_service.h"
@@ -86,7 +87,8 @@
     }
   }
 
-  void SetHeader(int suggestion_group_id, const std::u16string& header_text) {
+  void SetHeader(SuggestionGroupId suggestion_group_id,
+                 const std::u16string& header_text) {
     suggestion_group_id_ = suggestion_group_id;
     header_text_ = header_text;
 
@@ -248,7 +250,7 @@
   raw_ptr<views::ToggleImageButton> header_toggle_button_;
 
   // The group ID associated with this header.
-  int suggestion_group_id_ = 0;
+  SuggestionGroupId suggestion_group_id_ = SuggestionGroupId::kInvalid;
 
   // The unmodified header text for this header.
   std::u16string header_text_;
@@ -332,7 +334,7 @@
   result_view_ = AddChildView(std::move(result_view));
 }
 
-void OmniboxRowView::ShowHeader(int suggestion_group_id,
+void OmniboxRowView::ShowHeader(SuggestionGroupId suggestion_group_id,
                                 const std::u16string& header_text) {
   // Create the header (at index 0) if it doesn't exist.
   if (header_view_ == nullptr)
diff --git a/chrome/browser/ui/views/omnibox/omnibox_row_view.h b/chrome/browser/ui/views/omnibox/omnibox_row_view.h
index dce7b82..189c46234 100644
--- a/chrome/browser/ui/views/omnibox/omnibox_row_view.h
+++ b/chrome/browser/ui/views/omnibox/omnibox_row_view.h
@@ -14,6 +14,7 @@
 class OmniboxEditModel;
 class OmniboxResultView;
 class PrefService;
+enum class SuggestionGroupId;
 
 // The View that's a direct child of the OmniboxPopupContentsView, one per row.
 // This, in turn, has a child OmniboxResultView and an optional header that is
@@ -31,7 +32,8 @@
                  PrefService* pref_service);
 
   // Sets the header that appears above this row. Also shows the header.
-  void ShowHeader(int suggestion_group_id, const std::u16string& header_text);
+  void ShowHeader(SuggestionGroupId suggestion_group_id,
+                  const std::u16string& header_text);
 
   // Hides the header.
   void HideHeader();
diff --git a/chrome/browser/ui/views/side_search/side_search_icon_view.cc b/chrome/browser/ui/views/side_search/side_search_icon_view.cc
index 9ec7fb9..d82f0976 100644
--- a/chrome/browser/ui/views/side_search/side_search_icon_view.cc
+++ b/chrome/browser/ui/views/side_search/side_search_icon_view.cc
@@ -146,10 +146,9 @@
     return;
 
   if (base::FeatureList::IsEnabled(features::kUnifiedSidePanel)) {
-    content::WebContents* active_contents =
-        browser_view->GetActiveWebContents();
-    UnifiedSideSearchController::FromWebContents(active_contents)
-        ->OpenSidePanel();
+    browser_view->side_panel_coordinator()->Show(
+        SidePanelEntry::Id::kSideSearch,
+        SidePanelUtil::SidePanelOpenTrigger::kSideSearchPageAction);
   } else {
     browser_view->side_search_controller()->ToggleSidePanel();
   }
diff --git a/chrome/browser/ui/views/side_search/unified_side_search_controller.cc b/chrome/browser/ui/views/side_search/unified_side_search_controller.cc
index dffdbe91..0acb2ace 100644
--- a/chrome/browser/ui/views/side_search/unified_side_search_controller.cc
+++ b/chrome/browser/ui/views/side_search/unified_side_search_controller.cc
@@ -89,6 +89,13 @@
 
 void UnifiedSideSearchController::OnEntryShown(SidePanelEntry* entry) {
   UpdateSidePanel();
+  auto* active_contents = GetBrowserView()->GetActiveWebContents();
+  if (active_contents) {
+    auto* helper =
+        SideSearchTabContentsHelper::FromWebContents(active_contents);
+    if (helper)
+      helper->MaybeRecordDurationSidePanelAvailableToFirstOpen();
+  }
 }
 
 void UnifiedSideSearchController::OnEntryHidden(SidePanelEntry* entry) {
@@ -151,13 +158,6 @@
   if (browser_view) {
     browser_view->side_panel_coordinator()->Show(
         SidePanelEntry::Id::kSideSearch);
-    auto* active_contents = browser_view->GetActiveWebContents();
-    if (active_contents) {
-      auto* helper =
-          SideSearchTabContentsHelper::FromWebContents(active_contents);
-      if (helper)
-        helper->MaybeRecordDurationSidePanelAvailableToFirstOpen();
-    }
   }
 }
 
diff --git a/chrome/browser/ui/views/status_bubble_views.cc b/chrome/browser/ui/views/status_bubble_views.cc
index 8c3a7cba..a48193c6 100644
--- a/chrome/browser/ui/views/status_bubble_views.cc
+++ b/chrome/browser/ui/views/status_bubble_views.cc
@@ -706,7 +706,11 @@
     DCHECK(!expand_view_);
     popup_ = std::make_unique<views::Widget>();
 
+#if BUILDFLAG(IS_MAC)
+    views::Widget::InitParams params(views::Widget::InitParams::TYPE_TOOLTIP);
+#else
     views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
+#endif
 #if BUILDFLAG(IS_WIN)
     // On Windows use the software compositor to ensure that we don't block
     // the UI thread blocking issue during command buffer creation. We can
diff --git a/chrome/browser/ui/views/tabs/tab_drag_controller.cc b/chrome/browser/ui/views/tabs/tab_drag_controller.cc
index 9336243b..22325cc 100644
--- a/chrome/browser/ui/views/tabs/tab_drag_controller.cc
+++ b/chrome/browser/ui/views/tabs/tab_drag_controller.cc
@@ -2338,28 +2338,18 @@
 
 gfx::Point TabDragController::GetCursorScreenPoint() {
 #if BUILDFLAG(IS_CHROMEOS_ASH)
-  if (event_source_ == EVENT_SOURCE_TOUCH &&
-      aura::Env::GetInstance()->is_touch_down()) {
-    views::Widget* widget = GetAttachedBrowserWidget();
-    DCHECK(widget);
-    aura::Window* widget_window = widget->GetNativeWindow();
-    DCHECK(widget_window->GetRootWindow());
-    gfx::PointF touch_point_f;
-    bool got_touch_point =
-        widget->GetGestureRecognizer()->GetLastTouchPointForTarget(
-            widget_window, &touch_point_f);
-    if (got_touch_point) {
-      gfx::Point touch_point = gfx::ToFlooredPoint(touch_point_f);
-      wm::ConvertPointToScreen(widget_window->GetRootWindow(), &touch_point);
-      return touch_point;
-    }
-
-    // Fallback when touch state is lost. See http://crbug.com/1162541
-    return last_point_in_screen_;
-  }
-#endif
-
+  views::Widget* widget = GetAttachedBrowserWidget();
+  DCHECK(widget);
+  aura::Window* widget_window = widget->GetNativeWindow();
+  DCHECK(widget_window->GetRootWindow());
+  auto event_source = event_source_ == EVENT_SOURCE_MOUSE
+                          ? ui::mojom::DragEventSource::kMouse
+                          : ui::mojom::DragEventSource::kTouch;
+  return aura::Env::GetInstance()->GetLastPointerPoint(
+      event_source, widget_window, /*fallback=*/last_point_in_screen_);
+#else
   return display::Screen::GetScreen()->GetCursorScreenPoint();
+#endif
 }
 
 gfx::Vector2d TabDragController::GetWindowOffset(
diff --git a/chrome/browser/ui/webui/access_code_cast/access_code_cast_handler_browsertest.cc b/chrome/browser/ui/webui/access_code_cast/access_code_cast_handler_browsertest.cc
index 84b8a1a4..e62f725 100644
--- a/chrome/browser/ui/webui/access_code_cast/access_code_cast_handler_browsertest.cc
+++ b/chrome/browser/ui/webui/access_code_cast/access_code_cast_handler_browsertest.cc
@@ -8,6 +8,7 @@
 #include "base/win/windows_version.h"
 #endif
 #include "chrome/browser/media/router/discovery/access_code/access_code_cast_constants.h"
+#include "components/sessions/content/session_tab_helper.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace media_router {
@@ -29,7 +30,8 @@
 
   // This tests that if the network is not present (we are not connected to the
   // internet), we will see a server error in the access code dialog box.
-  SetUpPrimaryAccountWithHostedDomain(signin::ConsentLevel::kSync);
+  SetUpPrimaryAccountWithHostedDomain(signin::ConsentLevel::kSync,
+                                      browser()->profile());
 
   auto* dialog_contents = ShowDialog();
   SetAccessCode("abcdef", dialog_contents);
@@ -79,11 +81,17 @@
 
   EnableAccessCodeCasting();
 
-  SetUpPrimaryAccountWithHostedDomain(signin::ConsentLevel::kSync);
+  SetUpPrimaryAccountWithHostedDomain(signin::ConsentLevel::kSync,
+                                      browser()->profile());
 
   auto* dialog_contents = ShowDialog();
   SetAccessCode("abcdef", dialog_contents);
-  ExpectStartRouteCallFromTabMirroring("cast:<1234>");
+  ExpectStartRouteCallFromTabMirroring(
+      "cast:<1234>",
+      MediaSource::ForTab(
+          sessions::SessionTabHelper::IdForTab(web_contents()).id())
+          .id(),
+      web_contents());
 
   PressSubmitAndWaitForClose(dialog_contents);
 }
@@ -100,7 +108,8 @@
 
   // This tests that an account that does not have Sync enabled will throw a
   // generic error.
-  SetUpPrimaryAccountWithHostedDomain(signin::ConsentLevel::kSignin);
+  SetUpPrimaryAccountWithHostedDomain(signin::ConsentLevel::kSignin,
+                                      browser()->profile());
 
   auto* dialog_contents = ShowDialog();
   SetAccessCode("abcdef", dialog_contents);
diff --git a/chrome/browser/ui/webui/realbox/realbox_handler.cc b/chrome/browser/ui/webui/realbox/realbox_handler.cc
index f7b1d0c2..515b20f 100644
--- a/chrome/browser/ui/webui/realbox/realbox_handler.cc
+++ b/chrome/browser/ui/webui/realbox/realbox_handler.cc
@@ -137,7 +137,8 @@
     suggestion_group->hide_group_a11y_label = l10n_util::GetStringFUTF16(
         IDS_ACC_HEADER_HIDE_SUGGESTIONS_BUTTON, suggestion_group->header);
 
-    result_map.emplace(pair.first, std::move(suggestion_group));
+    result_map.emplace(static_cast<int>(pair.first),
+                       std::move(suggestion_group));
   }
   return result_map;
 }
@@ -212,8 +213,8 @@
                                                      description_class.style));
     }
     mojom_match->destination_url = match.destination_url;
-    mojom_match->suggestion_group_id =
-        match.suggestion_group_id.value_or(kInvalidSuggestionGroupId);
+    mojom_match->suggestion_group_id = static_cast<int>(
+        match.suggestion_group_id.value_or(SuggestionGroupId::kInvalid));
     const bool is_bookmarked =
         bookmark_model->IsBookmarked(match.destination_url);
     mojom_match->icon_url =
@@ -689,9 +690,14 @@
   if (!autocomplete_controller_)
     return;
 
+  // It should be safe to cast |suggestion_group_id| to SuggestionGroupId type,
+  // since the group ID was originally passed to the page by the browser.
+  // TODO(crbug.com/1343512): Investigate migrating this enum to a proto enum
+  // to take advantage of its safe built-in conversion logic.
   omnibox::SuggestionGroupVisibility new_value =
       autocomplete_controller_->result().IsSuggestionGroupHidden(
-          profile_->GetPrefs(), suggestion_group_id)
+          profile_->GetPrefs(),
+          static_cast<SuggestionGroupId>(suggestion_group_id))
           ? omnibox::SuggestionGroupVisibility::SHOWN
           : omnibox::SuggestionGroupVisibility::HIDDEN;
   omnibox::SetSuggestionGroupVisibility(profile_->GetPrefs(),
diff --git a/chrome/browser/web_applications/commands/install_isolated_app_command.cc b/chrome/browser/web_applications/commands/install_isolated_app_command.cc
index 3d014737..96a6ced 100644
--- a/chrome/browser/web_applications/commands/install_isolated_app_command.cc
+++ b/chrome/browser/web_applications/commands/install_isolated_app_command.cc
@@ -16,6 +16,9 @@
 #include "chrome/browser/web_applications/commands/web_app_command.h"
 #include "chrome/browser/web_applications/web_app_data_retriever.h"
 #include "chrome/browser/web_applications/web_app_url_loader.h"
+#include "third_party/blink/public/common/manifest/manifest_util.h"
+#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
+#include "url/gurl.h"
 
 namespace web_app {
 
@@ -63,14 +66,58 @@
   url_loader_.LoadUrl(
       url, shared_web_contents(),
       WebAppUrlLoader::UrlComparison::kIgnoreQueryParamsAndRef,
+      base::BindOnce(&InstallIsolatedAppCommand::OnLoadUrl, weak_this_));
+}
+
+void InstallIsolatedAppCommand::OnLoadUrl(WebAppUrlLoaderResult result) {
+  if (!IsUrlLoadingResultSuccess(result)) {
+    ReportFailure();
+    return;
+  }
+
+  data_retriever_->CheckInstallabilityAndRetrieveManifest(
+      shared_web_contents(),
+      /*bypass_service_worker_check=*/false,
       base::BindOnce(
-          [](base::WeakPtr<InstallIsolatedAppCommand> self,
-             WebAppUrlLoader::Result url_loading_result) {
-            self->Report(IsUrlLoadingResultSuccess(url_loading_result));
-          },
+          &InstallIsolatedAppCommand::OnCheckInstallabilityAndRetrieveManifest,
           weak_this_));
 }
 
+void InstallIsolatedAppCommand::OnCheckInstallabilityAndRetrieveManifest(
+    blink::mojom::ManifestPtr opt_manifest,
+    const GURL& manifest_url,
+    bool valid_manifest_for_web_app,
+    bool is_installable) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  if (!is_installable) {
+    ReportFailure();
+    return;
+  }
+
+  // See |WebAppDataRetriever::CheckInstallabilityCallback| documentation for
+  // details.
+  DCHECK(valid_manifest_for_web_app)
+      << "must be true when |is_installable| is true.";
+
+  if (!opt_manifest) {
+    ReportFailure();
+    return;
+  }
+
+  // See |WebAppDataRetriever::CheckInstallabilityCallback| documentation for
+  // details.
+  DCHECK(!blink::IsEmptyManifest(opt_manifest))
+      << "must not be empty when manifest is present.";
+
+  // See |WebAppDataRetriever::CheckInstallabilityCallback| documentation for
+  // details.
+  DCHECK(!manifest_url.is_empty())
+      << "must not be empty if manifest is not empty.";
+
+  Report(/*success=*/true);
+}
+
 void InstallIsolatedAppCommand::OnSyncSourceRemoved() {
   ReportFailure();
 }
diff --git a/chrome/browser/web_applications/commands/install_isolated_app_command.h b/chrome/browser/web_applications/commands/install_isolated_app_command.h
index 526aa27..67f9880 100644
--- a/chrome/browser/web_applications/commands/install_isolated_app_command.h
+++ b/chrome/browser/web_applications/commands/install_isolated_app_command.h
@@ -13,11 +13,16 @@
 #include "base/strings/string_piece_forward.h"
 #include "base/values.h"
 #include "chrome/browser/web_applications/commands/web_app_command.h"
+#include "third_party/blink/public/mojom/manifest/manifest.mojom-forward.h"
+
+class GURL;
 
 namespace web_app {
 class WebAppUrlLoader;
 class WebAppDataRetriever;
 
+enum class WebAppUrlLoaderResult;
+
 enum class InstallIsolatedAppCommandResult {
   kOk,
   kUnknownError,
@@ -44,6 +49,13 @@
   void ReportFailure();
   void Report(bool success);
 
+  void OnLoadUrl(WebAppUrlLoaderResult result);
+  void OnCheckInstallabilityAndRetrieveManifest(
+      blink::mojom::ManifestPtr opt_manifest,
+      const GURL& manifest_url,
+      bool valid_manifest_for_web_app,
+      bool is_installable);
+
   SEQUENCE_CHECKER(sequence_checker_);
 
   std::string url_;
diff --git a/chrome/browser/web_applications/commands/install_isolated_app_command_unittest.cc b/chrome/browser/web_applications/commands/install_isolated_app_command_unittest.cc
index ea93c70..f9dd3e5 100644
--- a/chrome/browser/web_applications/commands/install_isolated_app_command_unittest.cc
+++ b/chrome/browser/web_applications/commands/install_isolated_app_command_unittest.cc
@@ -84,13 +84,36 @@
       std::unique_ptr<WebAppDataRetriever> data_retriever = nullptr) {
     base::test::TestFuture<InstallIsolatedAppCommandResult> test_future;
     auto command = CreateCommand(url, test_future.GetCallback());
-    if (data_retriever != nullptr) {
-      command->SetDataRetrieverForTesting(std::move(data_retriever));
-    }
+    command->SetDataRetrieverForTesting(data_retriever != nullptr
+                                            ? std::move(data_retriever)
+                                            : CreateDefaultDataRetriever());
     ScheduleCommand(std::move(command));
     return test_future.Get();
   }
 
+  static std::unique_ptr<FakeDataRetriever> CreateDefaultDataRetriever() {
+    std::unique_ptr<FakeDataRetriever> fake_data_retriever =
+        std::make_unique<FakeDataRetriever>();
+
+    fake_data_retriever->SetManifest(
+        /*manifest=*/CreateDefaultManifest(), /*is_installable=*/true,
+        /*manifest_url=*/CreateDefaultManifestURL());
+    return fake_data_retriever;
+  }
+
+  static blink::mojom::ManifestPtr CreateDefaultManifest() {
+    auto manifest = blink::mojom::Manifest::New();
+    manifest->start_url = GURL{"http://test.com/"},
+    manifest->scope = GURL{"http://test.com/scope"},
+    manifest->display = DisplayMode::kStandalone;
+    manifest->short_name = u"Manifest Name";
+    return manifest;
+  }
+
+  static GURL CreateDefaultManifestURL() {
+    return GURL{"http://defaul-non-empty-url.com/manifest.json"};
+  }
+
   TestingProfile* profile() const { return profile_.get(); }
 
  private:
@@ -171,11 +194,54 @@
 
   std::unique_ptr<FakeDataRetriever> fake_data_retriever =
       std::make_unique<FakeDataRetriever>();
+  fake_data_retriever->SetManifest(
+      /*manifest=*/CreateDefaultManifest(), /*is_installable=*/true,
+      /*manifest_url=*/CreateDefaultManifestURL());
 
   EXPECT_THAT(ExecuteCommand("http://test-url-example.com",
                              std::move(fake_data_retriever)),
               IsInstallationOk());
 }
 
+TEST_F(InstallIsolatedAppCommandTest,
+       InstallationFailsWhenAppIsNotInstallable) {
+  SetPrepareForLoadResultLoaded();
+
+  ExpectLoadedForURL("http://test-url-example.com");
+
+  std::unique_ptr<FakeDataRetriever> fake_data_retriever =
+      std::make_unique<FakeDataRetriever>();
+
+  auto manifest = blink::mojom::Manifest::New();
+  fake_data_retriever->SetManifest(
+      std::move(manifest),
+      /*is_installable=*/false,
+      /*manifest_url=*/
+      GURL{"http://test-url-example.com/manifest.json"});
+
+  EXPECT_THAT(ExecuteCommand("http://test-url-example.com",
+                             std::move(fake_data_retriever)),
+              Not(IsInstallationOk()));
+}
+
+TEST_F(InstallIsolatedAppCommandTest,
+       InstallationFailsWhenAppIsInstallableButManifestIsNull) {
+  SetPrepareForLoadResultLoaded();
+
+  ExpectLoadedForURL("http://test-url-example.com");
+
+  std::unique_ptr<FakeDataRetriever> fake_data_retriever =
+      std::make_unique<FakeDataRetriever>();
+
+  fake_data_retriever->SetManifest(
+      /*manifest=*/nullptr,
+      /*is_installable=*/true,
+      /*manifest_url=*/CreateDefaultManifestURL());
+
+  EXPECT_THAT(ExecuteCommand("http://test-url-example.com",
+                             std::move(fake_data_retriever)),
+              Not(IsInstallationOk()));
+}
+
 }  // namespace
 }  // namespace web_app
diff --git a/chrome/browser/webauthn/chrome_webauthn_browsertest.cc b/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
index 0f90f1e..82f8194 100644
--- a/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
+++ b/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
@@ -98,8 +98,8 @@
 
   static constexpr char kPageFile[] = "page.html";
 
-  std::vector<base::Value> resources;
-  resources.emplace_back(std::string(kPageFile));
+  base::Value::List resources;
+  resources.Append(std::string(kPageFile));
   static constexpr char kContents[] = R"(
 <html>
   <head>
diff --git a/chrome/browser/xsurface/android/java/src/org/chromium/chrome/browser/xsurface/SurfaceActionsHandler.java b/chrome/browser/xsurface/android/java/src/org/chromium/chrome/browser/xsurface/SurfaceActionsHandler.java
index 4089466..eb85d98 100644
--- a/chrome/browser/xsurface/android/java/src/org/chromium/chrome/browser/xsurface/SurfaceActionsHandler.java
+++ b/chrome/browser/xsurface/android/java/src/org/chromium/chrome/browser/xsurface/SurfaceActionsHandler.java
@@ -114,4 +114,11 @@
      * Attempts to follow or unfollow a WebFeed.
      */
     default void updateWebFeedFollowState(WebFeedFollowUpdate update) {}
+
+    /**
+     * Navigates a new tab in group to a particular URL.
+     * @param url The url for which to navigate.
+     * @param actionSourceView The View from which the user tap originated. May be null.
+     */
+    default void navigateNewTabInGroup(String url, View actionSourceView) {}
 }
diff --git a/chrome/build/linux.pgo.txt b/chrome/build/linux.pgo.txt
index 7f18884..a471cb2 100644
--- a/chrome/build/linux.pgo.txt
+++ b/chrome/build/linux.pgo.txt
@@ -1 +1 @@
-chrome-linux-main-1658404777-1ea1be63e174d409a7b616996078d592f388c70f.profdata
+chrome-linux-main-1658425924-e2c6280a51ccc9e0ee83ef2933893b13ff7fcb7c.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 086ccbbc..5b73041 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1658404777-d39fb39b600e72bc0bcac09f6cf18a5f1419f1af.profdata
+chrome-win32-main-1658425924-778ab27c2d5aa269a6113ae799d48237a5b5d381.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 3090acc..384962c 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1658415573-cbf63ca3eb3683d159c0865bd1049a6ee0f2a2f1.profdata
+chrome-win64-main-1658425924-23d20021b0a4b09689ed0da2e2eccc4accf2fc53.profdata
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index fdce773..3bb624d1 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -4183,6 +4183,7 @@
         "//chromeos/ui/frame",
         "//chromeos/ui/frame:test_support",
         "//chromeos/ui/wm",
+        "//components/access_code_cast/common",
         "//components/app_constants",
         "//components/app_restore",
         "//components/arc:arc_test_support",
@@ -9285,6 +9286,7 @@
       "//ui/base:test_support",
       "//ui/base/clipboard:clipboard_test_support",
       "//ui/compositor:test_support",
+      "//ui/display:display_interactive_ui_tests",
       "//ui/display:test_support",
       "//ui/events:events_interactive_ui_tests",
       "//ui/events:gesture_detection",
diff --git a/chrome/test/chromedriver/session_commands_unittest.cc b/chrome/test/chromedriver/session_commands_unittest.cc
index 2f000d5..e4525a6 100644
--- a/chrome/test/chromedriver/session_commands_unittest.cc
+++ b/chrome/test/chromedriver/session_commands_unittest.cc
@@ -192,14 +192,14 @@
   ASSERT_TRUE(result.DictEmpty());
 
   // Invalid entry
-  base::DictionaryValue* entry_ptr;
-  ASSERT_TRUE(list_ptr->GetDictionary(0, &entry_ptr));
-  entry_ptr->GetDict().Set("pageLoadStrategy", "invalid");
+  base::Value::Dict* entry_ptr = list_ptr->GetList()[0].GetIfDict();
+  ASSERT_TRUE(entry_ptr);
+  entry_ptr->Set("pageLoadStrategy", "invalid");
   status = ProcessCapabilities(params, &result);
   ASSERT_EQ(kInvalidArgument, status.code());
 
   // Valid entry
-  entry_ptr->GetDict().Set("pageLoadStrategy", "eager");
+  entry_ptr->Set("pageLoadStrategy", "eager");
   status = ProcessCapabilities(params, &result);
   ASSERT_EQ(kOk, status.code()) << status.message();
   ASSERT_EQ(result.DictSize(), 1u);
@@ -209,9 +209,10 @@
 
   // Multiple entries, the first one should be selected.
   list_ptr->Append(base::DictionaryValue());
-  ASSERT_TRUE(list_ptr->GetDictionary(1, &entry_ptr));
-  entry_ptr->GetDict().Set("pageLoadStrategy", "normal");
-  entry_ptr->GetDict().Set("browserName", "chrome");
+  entry_ptr = list_ptr->GetList()[1].GetIfDict();
+  ASSERT_TRUE(entry_ptr);
+  entry_ptr->Set("pageLoadStrategy", "normal");
+  entry_ptr->Set("browserName", "chrome");
   status = ProcessCapabilities(params, &result);
   ASSERT_EQ(kOk, status.code()) << status.message();
   ASSERT_EQ(result.DictSize(), 1u);
diff --git a/chrome/test/data/webui/chromeos/os_feedback_ui/file_attachment_test.js b/chrome/test/data/webui/chromeos/os_feedback_ui/file_attachment_test.js
index 725f1633..35d4b8b 100644
--- a/chrome/test/data/webui/chromeos/os_feedback_ui/file_attachment_test.js
+++ b/chrome/test/data/webui/chromeos/os_feedback_ui/file_attachment_test.js
@@ -266,21 +266,42 @@
         return new Uint8Array(fakeData).buffer;
       },
     });
-    // getImageUrl_ should return an url when file is image type.
-    const imageUrl = await page.getImageUrl_(fakeImageFile);
+
+    page.setSelectedFileForTesting(fakeImageFile);
+    await flushTasks();
+
+    // The selected file name is set properly.
+    assertEquals('fake.png', getElementContent('#selectedFileName'));
+
+    // The selectedFileImage should have an url when file is image type.
+    const imageUrl = getElement('#selectedFileImage').src;
     assertTrue(imageUrl.length > 0);
     // There should be a preview image.
     page.selectedImageUrl_ = imageUrl;
     const selectedImage = getElement('#selectedFileImage');
     assertTrue(!!selectedImage.src);
     assertEquals(imageUrl, selectedImage.src);
+    assertEquals(
+        'Preview fake.png', getElement('#selectedImageButton').ariaLabel);
   });
 
-
   // Test that clicking the image will open preview dialog and set the
   // focus on the close dialog icon button.
   test('selectedImagePreviewDialog', async () => {
     await initializePage();
+    const fakeData = [12, 11, 99];
+
+    /** @type {!File} */
+    const fakeImageFile = /** @type {!File} */ ({
+      name: 'fake.png',
+      type: 'image/png',
+      size: MAX_ATTACH_FILE_SIZE,
+      arrayBuffer: async () => {
+        return new Uint8Array(fakeData).buffer;
+      },
+    });
+
+    page.setSelectedFileForTesting(fakeImageFile);
     page.selectedImageUrl_ = fakeImageUrl;
     assertEquals(fakeImageUrl, getElement('#selectedFileImage').src);
 
@@ -295,6 +316,9 @@
     imageButton.click();
     await imageClickPromise;
 
+    // The preview dialog's title should be set properly.
+    assertEquals('fake.png', getElementContent('#modalDialogTitleText'));
+
     // The preview dialog's close icon button is visible now.
     assertTrue(isVisible(closeDialogButton));
     // The preview dialog's close icon button is focused.
diff --git a/chrome/test/data/webui/chromeos/os_feedback_ui/share_data_page_test.js b/chrome/test/data/webui/chromeos/os_feedback_ui/share_data_page_test.js
index e3164829..c4c33dce 100644
--- a/chrome/test/data/webui/chromeos/os_feedback_ui/share_data_page_test.js
+++ b/chrome/test/data/webui/chromeos/os_feedback_ui/share_data_page_test.js
@@ -134,7 +134,10 @@
 
     assertTrue(page.i18nExists('attachScreenshotLabel'));
     assertEquals('Screenshot', getElementContent('#screenshotCheckLabel'));
+
     assertTrue(!!getElement('#screenshotImage'));
+    assertEquals('Preview Screenshot', getElement('#imageButton').ariaLabel);
+    assertTrue(page.i18nExists('previewImageAriaLabel'));
 
     // Add file attachment element.
     assertTrue(!!getElement('file-attachment'));
diff --git a/chrome/test/media_router/access_code_cast/access_code_cast_integration_browsertest.cc b/chrome/test/media_router/access_code_cast/access_code_cast_integration_browsertest.cc
index 51c8c6e..8ee6bf1c 100644
--- a/chrome/test/media_router/access_code_cast/access_code_cast_integration_browsertest.cc
+++ b/chrome/test/media_router/access_code_cast/access_code_cast_integration_browsertest.cc
@@ -183,24 +183,17 @@
 }
 
 void AccessCodeCastIntegrationBrowserTest::SetUpPrimaryAccountWithHostedDomain(
-    signin::ConsentLevel consent_level) {
+    signin::ConsentLevel consent_level,
+    Profile* profile) {
+  ASSERT_TRUE(identity_test_environment_);
   // Ensure that the stub user is signed in.
-  CoreAccountInfo account_info =
-      identity_test_environment_->MakePrimaryAccountAvailable(
-          user_manager::kStubUserEmail, consent_level);
+  identity_test_environment_->MakePrimaryAccountAvailable(
+      user_manager::kStubUserEmail, consent_level);
 
   identity_test_environment_->SetAutomaticIssueOfAccessTokens(true);
 
-  ASSERT_EQ(account_info.email, user_manager::kStubUserEmail);
-
-  identity_test_environment_->SimulateSuccessfulFetchOfAccountInfo(
-      account_info.account_id, account_info.email, account_info.gaia,
-      "foo_school.com", "full_name", "given_name", "locale",
-      "http://picture.example.com/picture.jpg");
-
-  ASSERT_TRUE(
-      AccessCodeCastSinkServiceFactory::GetForProfile(browser()->profile()));
-  AccessCodeCastSinkServiceFactory::GetForProfile(browser()->profile())
+  ASSERT_TRUE(AccessCodeCastSinkServiceFactory::GetForProfile(profile));
+  AccessCodeCastSinkServiceFactory::GetForProfile(profile)
       ->SetIdentityManagerForTesting(
           identity_test_environment_->identity_manager());
 
@@ -423,15 +416,19 @@
 }
 
 void AccessCodeCastIntegrationBrowserTest::ExpectStartRouteCallFromTabMirroring(
-    const std::string& sink_name) {
-  MediaSource media_source = MediaSource::ForTab(
-      sessions::SessionTabHelper::IdForTab(web_contents()).id());
-  auto* router = static_cast<TestMediaRouter*>(
-      media_router::MediaRouterFactory::GetInstance()->GetApiForBrowserContext(
-          browser()->profile()));
-  EXPECT_CALL(*router,
-              CreateRouteInternal(media_source.id(), sink_name, _,
-                                  web_contents(), _, base::Seconds(60), false));
+    const std::string& sink_name,
+    const std::string& media_source_id,
+    content::WebContents* web_contents,
+    base::TimeDelta timeout,
+    media_router::MockMediaRouter* media_router) {
+  if (!media_router) {
+    media_router = static_cast<TestMediaRouter*>(
+        media_router::MediaRouterFactory::GetInstance()
+            ->GetApiForBrowserContext(browser()->profile()));
+  }
+  EXPECT_CALL(*media_router,
+              CreateRouteInternal(media_source_id, sink_name, _, web_contents,
+                                  _, timeout, false));
 }
 
 }  // namespace media_router
diff --git a/chrome/test/media_router/access_code_cast/access_code_cast_integration_browsertest.h b/chrome/test/media_router/access_code_cast/access_code_cast_integration_browsertest.h
index 88141f4..7d9b6b46 100644
--- a/chrome/test/media_router/access_code_cast/access_code_cast_integration_browsertest.h
+++ b/chrome/test/media_router/access_code_cast/access_code_cast_integration_browsertest.h
@@ -20,6 +20,7 @@
 #include "chrome/common/webui_url_constants.h"
 #include "chrome/test/base/chrome_test_utils.h"
 #include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/mixin_based_in_process_browser_test.h"
 #include "chrome/test/base/mojo_web_ui_browser_test.h"
 #include "components/keyed_service/content/browser_context_dependency_manager.h"
 #include "components/media_router/browser/media_router.h"
@@ -39,7 +40,8 @@
 
 // Base class that generates an access code cast dialog and all objects that are
 // required for interacting with the dialog.
-class AccessCodeCastIntegrationBrowserTest : public InProcessBrowserTest {
+class AccessCodeCastIntegrationBrowserTest
+    : public MixinBasedInProcessBrowserTest {
  public:
   AccessCodeCastIntegrationBrowserTest();
   ~AccessCodeCastIntegrationBrowserTest() override;
@@ -51,7 +53,8 @@
 
   // Makes user signed-in with the stub account's email and sets the
   // |consent_level| for that account.
-  void SetUpPrimaryAccountWithHostedDomain(signin::ConsentLevel consent_level);
+  void SetUpPrimaryAccountWithHostedDomain(signin::ConsentLevel consent_level,
+                                           Profile* profile);
 
   void EnableAccessCodeCasting();
 
@@ -96,7 +99,12 @@
   void UpdateSinks(const std::vector<MediaSink>& sinks,
                    const std::vector<url::Origin>& origins);
 
-  void ExpectStartRouteCallFromTabMirroring(const std::string& sink_name);
+  void ExpectStartRouteCallFromTabMirroring(
+      const std::string& sink_name,
+      const std::string& media_source_id,
+      content::WebContents* web_contents,
+      base::TimeDelta timeout = base::Seconds(60),
+      media_router::MockMediaRouter* media_router = nullptr);
   void ValidateAndReleaseCastMediaSinkServiceImpl();
 
  protected:
diff --git a/chromecast/media/cma/backend/volume_map.cc b/chromecast/media/cma/backend/volume_map.cc
index da361cd..70e6227 100644
--- a/chromecast/media/cma/backend/volume_map.cc
+++ b/chromecast/media/cma/backend/volume_map.cc
@@ -66,18 +66,18 @@
 
   double prev_level = -1.0;
   std::vector<LevelToDb> new_map;
-  for (size_t i = 0; i < volume_map_list->GetListDeprecated().size(); ++i) {
-    const base::DictionaryValue* volume_map_entry;
-    CHECK(volume_map_list->GetDictionary(i, &volume_map_entry));
 
-    absl::optional<double> level = volume_map_entry->FindDoubleKey(kKeyLevel);
+  for (const auto& value : volume_map_list->GetList()) {
+    const base::Value::Dict& volume_map_entry = value.GetDict();
+
+    absl::optional<double> level = volume_map_entry.FindDouble(kKeyLevel);
     CHECK(level);
     CHECK_GE(*level, 0.0);
     CHECK_LE(*level, 1.0);
     CHECK_GT(*level, prev_level);
     prev_level = *level;
 
-    absl::optional<double> db = volume_map_entry->FindDoubleKey(kKeyDb);
+    absl::optional<double> db = volume_map_entry.FindDouble(kKeyDb);
     CHECK(db);
     CHECK_LE(*db, 0.0);
 
diff --git a/chromeos/chromeos_strings.grd b/chromeos/chromeos_strings.grd
index b8b512f..465a6c2 100644
--- a/chromeos/chromeos_strings.grd
+++ b/chromeos/chromeos_strings.grd
@@ -3449,6 +3449,9 @@
       <message name="IDS_FEEDBACK_TOOL_ATTACH_SCREENSHOT_CHECKBOX_ARIA_LABEL" desc="Aria Label for the checkbox to select the screenshot">
         Attach screenshot
       </message>
+      <message name="IDS_FEEDBACK_TOOL_PREVIEW_IMAGE_ARIA_LABEL" desc="Aria Label for the thumbnail of a screenshot or an image file">
+        Preview <ph name="PREVIEW_OBJECT">$1<ex>Screenshot</ex></ph>
+      </message>
       <message name="IDS_FEEDBACK_TOOL_USER_CONSENT_LABEL" desc="Label for checkbox indicating whether the user agrees to be contacted back.">
         We may email you for more information or updates
       </message>
diff --git a/chromeos/chromeos_strings_grd/IDS_FEEDBACK_TOOL_PREVIEW_IMAGE_ARIA_LABEL.png.sha1 b/chromeos/chromeos_strings_grd/IDS_FEEDBACK_TOOL_PREVIEW_IMAGE_ARIA_LABEL.png.sha1
new file mode 100644
index 0000000..1bdd032
--- /dev/null
+++ b/chromeos/chromeos_strings_grd/IDS_FEEDBACK_TOOL_PREVIEW_IMAGE_ARIA_LABEL.png.sha1
@@ -0,0 +1 @@
+c3ac7a6f7875ab6f9705b1a46fbd7a3fd47e97ad
\ No newline at end of file
diff --git a/chromeos/components/quick_answers/BUILD.gn b/chromeos/components/quick_answers/BUILD.gn
index f8680da..4a85be8 100644
--- a/chromeos/components/quick_answers/BUILD.gn
+++ b/chromeos/components/quick_answers/BUILD.gn
@@ -59,6 +59,7 @@
     "//chromeos/services/machine_learning/public/mojom",
     "//chromeos/strings:strings_grit",
     "//components/account_id",
+    "//components/language/core/browser",
     "//components/prefs:prefs",
     "//components/signin/public/base",
     "//components/signin/public/identity_manager",
diff --git a/chromeos/components/quick_answers/understanding/intent_generator.cc b/chromeos/components/quick_answers/understanding/intent_generator.cc
index f113f0d..c64cd105 100644
--- a/chromeos/components/quick_answers/understanding/intent_generator.cc
+++ b/chromeos/components/quick_answers/understanding/intent_generator.cc
@@ -41,6 +41,8 @@
 // Set of invalid characters for definition annonations.
 constexpr char kInvalidCharactersSet[] = "()[]{}<>_&|!";
 
+constexpr char kEnglishLanguage[] = "en";
+
 const std::map<std::string, IntentType>& GetIntentTypeMap() {
   static base::NoDestructor<std::map<std::string, IntentType>> kIntentTypeMap(
       {{"unit", IntentType::kUnit}, {"dictionary", IntentType::kDictionary}});
@@ -93,10 +95,31 @@
   return intent;
 }
 
+bool IsPreferredLanguage(const std::string& detected_language) {
+  auto preferred_languages_list =
+      base::SplitString(QuickAnswersState::Get()->preferred_languages(), ",",
+                        base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
+
+  for (const std::string& locale : preferred_languages_list) {
+    if (l10n_util::GetLanguage(locale) == detected_language)
+      return true;
+  }
+  return false;
+}
+
 // TODO(b/169370175): There is an issue with text classifier that
 // concatenated words are annotated as definitions. Before we switch to v2
 // model, skip such kind of queries for definition annotation for now.
 bool ShouldSkipDefinition(const std::string& text) {
+  // Skip definition annotations if English is not device language or user
+  // preferred language (Currently the text classifier only works with English
+  // words).
+  auto device_language =
+      l10n_util::GetLanguage(QuickAnswersState::Get()->application_locale());
+  if (device_language != kEnglishLanguage &&
+      !IsPreferredLanguage(kEnglishLanguage))
+    return true;
+
   DCHECK(text.length());
   // Skip the query for definition annotation if the selected text contains
   // capitalized characters in the middle and not all capitalized.
@@ -115,18 +138,6 @@
   return false;
 }
 
-bool IsPreferredLanguage(const std::string& detected_language) {
-  auto preferred_languages_list =
-      base::SplitString(QuickAnswersState::Get()->preferred_languages(), ",",
-                        base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
-
-  for (const std::string& locale : preferred_languages_list) {
-    if (l10n_util::GetLanguage(locale) == detected_language)
-      return true;
-  }
-  return false;
-}
-
 bool HasDigits(const std::string& word) {
   for (const auto& character : word) {
     if (std::isdigit(character))
@@ -214,8 +225,9 @@
                         QuickAnswersState::Get()->application_locale(),
                         language));
 
-    // Record intent source type for dictionary intent.
+    // Record intent source type and language for dictionary intent.
     RecordDictionaryIntentSource(DictionaryIntentSource::kHunspell);
+    RecordDictionaryIntentLanguage(language);
     return;
   }
 
@@ -283,9 +295,13 @@
               RewriteIntent(request.selected_text, entity_str, it->second),
               QuickAnswersState::Get()->application_locale()));
 
-      // Record intent source type for dictionary intent.
-      if (it->second == IntentType::kDictionary)
+      // Record intent source type and language for dictionary intent.
+      if (it->second == IntentType::kDictionary) {
         RecordDictionaryIntentSource(DictionaryIntentSource::kTextClassifier);
+        // Record the English language since currently the text classifier only
+        // works with English words.
+        RecordDictionaryIntentLanguage(kEnglishLanguage);
+      }
       return;
     }
   }
diff --git a/chromeos/components/quick_answers/understanding/intent_generator_unittest.cc b/chromeos/components/quick_answers/understanding/intent_generator_unittest.cc
index 581090b..00fc9c5 100644
--- a/chromeos/components/quick_answers/understanding/intent_generator_unittest.cc
+++ b/chromeos/components/quick_answers/understanding/intent_generator_unittest.cc
@@ -38,6 +38,12 @@
   return TextLanguage::New("en", /* confidence */ 1);
 }
 
+std::vector<TextLanguagePtr> DefaultLanguages() {
+  std::vector<TextLanguagePtr> languages;
+  languages.push_back(DefaultLanguage());
+  return languages;
+}
+
 class FakeSpellChecker : public SpellChecker {
  public:
   FakeSpellChecker(
@@ -83,7 +89,9 @@
         base::BindOnce(&IntentGeneratorTest::IntentGeneratorTestCallback,
                        base::Unretained(this)));
 
-    QuickAnswersState::Get()->set_use_text_annotator_for_testing();
+    fake_quick_answers_state()->set_use_text_annotator_for_testing();
+    fake_quick_answers_state()->set_application_locale("en");
+    fake_quick_answers_state()->set_preferred_languages("en");
   }
 
   void TearDown() override {
@@ -106,8 +114,7 @@
   void UseFakeServiceConnection(
       const std::vector<TextAnnotationPtr>& annotations =
           std::vector<TextAnnotationPtr>(),
-      const std::vector<TextLanguagePtr>& languages =
-          std::vector<TextLanguagePtr>()) {
+      const std::vector<TextLanguagePtr>& languages = DefaultLanguages()) {
     chromeos::machine_learning::ServiceConnection::
         UseFakeServiceConnectionForTesting(&fake_service_connection_);
     chromeos::machine_learning::ServiceConnection::GetInstance()->Initialize();
@@ -253,9 +260,7 @@
 
 TEST_F(IntentGeneratorTest, TranslationIntentWithAnnotation) {
   QuickAnswersRequest request;
-  request.selected_text = "unfathomable";
-  fake_quick_answers_state()->set_application_locale("es");
-  fake_quick_answers_state()->set_preferred_languages("es");
+  request.selected_text = "prueba";
 
   // Create the test annotations.
   std::vector<TextEntityPtr> entities;
@@ -264,14 +269,14 @@
       1.0,                                     // Confidence score.
       TextEntityData::NewNumericValue(0.0)));  // Data extracted.
 
-  auto dictionary_annotation = TextAnnotation::New(0,   // Start offset.
-                                                   12,  // End offset.
+  auto dictionary_annotation = TextAnnotation::New(0,  // Start offset.
+                                                   6,  // End offset.
                                                    std::move(entities));
 
   std::vector<TextAnnotationPtr> annotations;
   annotations.push_back(dictionary_annotation->Clone());
   std::vector<TextLanguagePtr> languages;
-  languages.push_back(DefaultLanguage());
+  languages.push_back(TextLanguage::New("es", /* confidence */ 1));
   UseFakeServiceConnection(annotations, languages);
 
   intent_generator_->GenerateIntent(request);
@@ -281,7 +286,7 @@
   // Should generate dictionary intent which is prioritized against
   // translation.
   EXPECT_EQ(IntentType::kDictionary, intent_info_.intent_type);
-  EXPECT_EQ("unfathomable", intent_info_.intent_text);
+  EXPECT_EQ("prueba", intent_info_.intent_text);
 }
 
 TEST_F(IntentGeneratorTest, TranslationIntentDeviceLanguageNotSet) {
@@ -486,6 +491,41 @@
   EXPECT_EQ("23 cm", intent_info_.intent_text);
 }
 
+TEST_F(IntentGeneratorTest, TextAnnotationNonEnglishLanguage) {
+  fake_quick_answers_state()->set_application_locale("es");
+  fake_quick_answers_state()->set_preferred_languages("es");
+
+  std::unique_ptr<QuickAnswersRequest> quick_answers_request =
+      std::make_unique<QuickAnswersRequest>();
+  quick_answers_request->selected_text = "unfathomable";
+
+  // Create the test annotations.
+  std::vector<TextEntityPtr> entities;
+  entities.emplace_back(TextEntity::New(
+      "dictionary",                            // Entity name.
+      1.0,                                     // Confidence score.
+      TextEntityData::NewNumericValue(0.0)));  // Data extracted.
+
+  auto dictionary_annotation = TextAnnotation::New(0,   // Start offset.
+                                                   12,  // End offset.
+                                                   std::move(entities));
+
+  std::vector<TextAnnotationPtr> annotations;
+  annotations.push_back(dictionary_annotation->Clone());
+  std::vector<TextLanguagePtr> languages;
+  languages.push_back(TextLanguage::New("en", /* confidence */ 1));
+  UseFakeServiceConnection(annotations, languages);
+
+  intent_generator_->GenerateIntent(*quick_answers_request);
+
+  FlushForTesting();
+
+  // Should not generate dictionary intent since English is not device language
+  // or preferred language. Should fallback to translation intent.
+  EXPECT_EQ(IntentType::kTranslation, intent_info_.intent_type);
+  EXPECT_EQ("unfathomable", intent_info_.intent_text);
+}
+
 TEST_F(IntentGeneratorTest, TextAnnotationIntentNoAnnotation) {
   std::unique_ptr<QuickAnswersRequest> quick_answers_request =
       std::make_unique<QuickAnswersRequest>();
diff --git a/chromeos/components/quick_answers/utils/quick_answers_metrics.cc b/chromeos/components/quick_answers/utils/quick_answers_metrics.cc
index a96bf2e..0c6ebbe3 100644
--- a/chromeos/components/quick_answers/utils/quick_answers_metrics.cc
+++ b/chromeos/components/quick_answers/utils/quick_answers_metrics.cc
@@ -8,6 +8,7 @@
 #include "base/notreached.h"
 #include "base/strings/stringprintf.h"
 #include "base/time/time.h"
+#include "components/language/core/browser/language_usage_metrics.h"
 
 namespace quick_answers {
 
@@ -26,6 +27,8 @@
     "QuickAnswers.TextToSpeech.EngineEvent";
 const char kQuickAnswersDictionaryIntentSource[] =
     "QuickAnswers.DictionaryIntent.Source";
+const char kQuickAnswersDictionaryIntentLanguage[] =
+    "QuickAnswers.DictionaryIntent.Language";
 const char kQuickAnswersNetworkError[] = "QuickAnswers.NetworkError.IntentType";
 const char kQuickAnswersNetworkResponseCode[] =
     "QuickAnswers.NetworkError.ResponseCode";
@@ -153,4 +156,10 @@
   base::UmaHistogramEnumeration(kQuickAnswersDictionaryIntentSource, source);
 }
 
+void RecordDictionaryIntentLanguage(const std::string& language) {
+  base::UmaHistogramSparse(
+      kQuickAnswersDictionaryIntentLanguage,
+      language::LanguageUsageMetrics::ToLanguageCodeHash(language));
+}
+
 }  // namespace quick_answers
diff --git a/chromeos/components/quick_answers/utils/quick_answers_metrics.h b/chromeos/components/quick_answers/utils/quick_answers_metrics.h
index eb65090..bcbfe80 100644
--- a/chromeos/components/quick_answers/utils/quick_answers_metrics.h
+++ b/chromeos/components/quick_answers/utils/quick_answers_metrics.h
@@ -75,6 +75,9 @@
 // Record the source type of dictionary intent.
 void RecordDictionaryIntentSource(DictionaryIntentSource source);
 
+// Record the query language of dictionary intent.
+void RecordDictionaryIntentLanguage(const std::string& language);
+
 }  // namespace quick_answers
 
 #endif  // CHROMEOS_COMPONENTS_QUICK_ANSWERS_UTILS_QUICK_ANSWERS_METRICS_H_
diff --git a/chromeos/dbus/BUILD.gn b/chromeos/dbus/BUILD.gn
index 9a158c3..e606507 100644
--- a/chromeos/dbus/BUILD.gn
+++ b/chromeos/dbus/BUILD.gn
@@ -12,6 +12,8 @@
   public_deps = [
     "//chromeos/dbus/common",
     "//chromeos/dbus/constants",
+
+    # TODO(jamescook): Remove this and fix all build targets that need it.
     "//chromeos/dbus/debug_daemon",
     "//chromeos/dbus/init",
     "//chromeos/dbus/shill",
@@ -19,18 +21,9 @@
   ]
   deps = [
     "//base",
-    "//chromeos/dbus/easy_unlock",
-    "//chromeos/dbus/fwupd",
     "//chromeos/dbus/util",
-    "//components/account_id",
-    "//components/device_event_log",
-    "//components/policy/proto",
-    "//net",
-    "//url",
   ]
   sources = [
-    "dbus_clients_browser.cc",
-    "dbus_clients_browser.h",
     "dbus_thread_manager.cc",
     "dbus_thread_manager.h",
   ]
diff --git a/chromeos/dbus/dbus_clients_browser.cc b/chromeos/dbus/dbus_clients_browser.cc
deleted file mode 100644
index afbf65b..0000000
--- a/chromeos/dbus/dbus_clients_browser.cc
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "chromeos/dbus/dbus_clients_browser.h"
-
-#include "base/check.h"
-#include "chromeos/dbus/dbus_thread_manager.h"
-
-namespace chromeos {
-
-DBusClientsBrowser::DBusClientsBrowser(bool use_real_clients) {}
-
-DBusClientsBrowser::~DBusClientsBrowser() = default;
-
-void DBusClientsBrowser::Initialize(dbus::Bus* system_bus) {
-  DCHECK(DBusThreadManager::IsInitialized());
-}
-
-}  // namespace chromeos
diff --git a/chromeos/dbus/dbus_clients_browser.h b/chromeos/dbus/dbus_clients_browser.h
deleted file mode 100644
index ffc1c00..0000000
--- a/chromeos/dbus/dbus_clients_browser.h
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROMEOS_DBUS_DBUS_CLIENTS_BROWSER_H_
-#define CHROMEOS_DBUS_DBUS_CLIENTS_BROWSER_H_
-
-#include "base/component_export.h"
-
-namespace dbus {
-class Bus;
-}
-
-namespace chromeos {
-
-// TODO(jamescook): Delete this class. http://crbug.com/647367
-class COMPONENT_EXPORT(CHROMEOS_DBUS) DBusClientsBrowser {
- public:
-  // Creates real implementations if |use_real_clients| is true and fakes
-  // otherwise. Fakes are used when running on Linux desktop and in tests.
-  explicit DBusClientsBrowser(bool use_real_clients);
-
-  DBusClientsBrowser(const DBusClientsBrowser&) = delete;
-  DBusClientsBrowser& operator=(const DBusClientsBrowser&) = delete;
-
-  ~DBusClientsBrowser();
-
-  void Initialize(dbus::Bus* system_bus);
-};
-
-}  // namespace chromeos
-
-#endif  // CHROMEOS_DBUS_DBUS_CLIENTS_BROWSER_H_
diff --git a/chromeos/dbus/dbus_thread_manager.cc b/chromeos/dbus/dbus_thread_manager.cc
index 5725aa7..48c56b0b 100644
--- a/chromeos/dbus/dbus_thread_manager.cc
+++ b/chromeos/dbus/dbus_thread_manager.cc
@@ -8,24 +8,16 @@
 #include <utility>
 
 #include "base/logging.h"
-#include "base/memory/ptr_util.h"
-#include "base/message_loop/message_pump_type.h"
 #include "chromeos/dbus/common/dbus_client.h"
-#include "chromeos/dbus/dbus_clients_browser.h"
 #include "chromeos/dbus/shill/shill_clients.h"
 
 namespace chromeos {
 
 static DBusThreadManager* g_dbus_thread_manager = nullptr;
 
-DBusThreadManager::DBusThreadManager()
-    : clients_browser_(
-          std::make_unique<DBusClientsBrowser>(use_real_clients_)) {}
+DBusThreadManager::DBusThreadManager() = default;
 
-DBusThreadManager::~DBusThreadManager() {
-  // Delete all D-Bus clients before shutting down the system bus.
-  clients_browser_.reset();
-}
+DBusThreadManager::~DBusThreadManager() = default;
 
 void DBusThreadManager::InitializeClients() {
   // Some clients call DBusThreadManager::Get() during initialization.
@@ -35,10 +27,7 @@
   // that require Shill clients. https://crbug.com/948390.
   shill_clients::Initialize(GetSystemBus());
 
-  if (clients_browser_)
-    clients_browser_->Initialize(GetSystemBus());
-
-  if (use_real_clients_)
+  if (!IsUsingFakes())
     VLOG(1) << "DBusThreadManager initialized for ChromeOS";
   else
     VLOG(1) << "DBusThreadManager created for testing";
diff --git a/chromeos/dbus/dbus_thread_manager.h b/chromeos/dbus/dbus_thread_manager.h
index 45f5315..0c90633c 100644
--- a/chromeos/dbus/dbus_thread_manager.h
+++ b/chromeos/dbus/dbus_thread_manager.h
@@ -5,21 +5,11 @@
 #ifndef CHROMEOS_DBUS_DBUS_THREAD_MANAGER_H_
 #define CHROMEOS_DBUS_DBUS_THREAD_MANAGER_H_
 
-#include <memory>
-#include <string>
-
-#include "base/callback.h"
 #include "base/component_export.h"
 #include "chromeos/dbus/init/dbus_thread_manager_base.h"
 
 namespace chromeos {
 
-// Style Note: Clients are sorted by names.
-class DBusClientsBrowser;
-
-// THIS CLASS IS BEING DEPRECATED. See README.md for guidelines and
-// https://crbug.com/647367 for details.
-//
 // Ash implementation of DBusThreadManagerBase.
 class COMPONENT_EXPORT(CHROMEOS_DBUS) DBusThreadManager
     : public DBusThreadManagerBase {
@@ -27,8 +17,6 @@
   // Sets the global instance. Must be called before any calls to Get().
   // We explicitly initialize and shut down the global object, rather than
   // making it a Singleton, to ensure clean startup and shutdown.
-  // This will initialize real or fake DBusClients depending on command-line
-  // arguments and whether this process runs in a ChromeOS environment.
   static void Initialize();
 
   // Returns true if DBusThreadManager has been initialized. Call this to
@@ -47,12 +35,10 @@
   const DBusThreadManager& operator=(const DBusThreadManager&) = delete;
   ~DBusThreadManager() override;
 
-  // Initializes all currently stored DBusClients with the system bus and
-  // performs additional setup.
+  // Performs additional setup.
+  // TODO(https://crbug.com/948390): Remove after shill client initialization
+  // moves into chrome/browser.
   void InitializeClients();
-
-  // Owns the clients.
-  std::unique_ptr<DBusClientsBrowser> clients_browser_;
 };
 
 }  // namespace chromeos
diff --git a/chromeos/dbus/init/dbus_thread_manager_base.h b/chromeos/dbus/init/dbus_thread_manager_base.h
index 7819aa2..462663e 100644
--- a/chromeos/dbus/init/dbus_thread_manager_base.h
+++ b/chromeos/dbus/init/dbus_thread_manager_base.h
@@ -41,6 +41,7 @@
   const DBusThreadManagerBase& operator=(const DBusThreadManagerBase&) = delete;
   virtual ~DBusThreadManagerBase();
 
+ private:
   // Whether to use real or fake dbus clients.
   const bool use_real_clients_;
 
diff --git a/chromeos/tast_control.gni b/chromeos/tast_control.gni
index 7b8e1902..fe74a2c 100644
--- a/chromeos/tast_control.gni
+++ b/chromeos/tast_control.gni
@@ -257,9 +257,6 @@
   # http://crbug.com/1335213
   "arc.WindowState.clamshell",
 
-  # http://crbug.com/1338201
-  "ui.ChromeCrashLoggedIn.gpu_process_breakpad",
-
   # http://crbug.com/1309278
   "policy.ChromeOsLockOnIdleSuspend",
 
diff --git a/components/autofill/android/java/res/layout/autofill_dropdown_item_refresh.xml b/components/autofill/android/java/res/layout/autofill_dropdown_item_refresh.xml
index 64457f5..9acf6959 100644
--- a/components/autofill/android/java/res/layout/autofill_dropdown_item_refresh.xml
+++ b/components/autofill/android/java/res/layout/autofill_dropdown_item_refresh.xml
@@ -16,6 +16,15 @@
     android:orientation="horizontal"
     tools:ignore="UnusedResources" >
 
+    <ImageView
+        android:id="@+id/start_dropdown_icon"
+        android:layout_width="@dimen/autofill_dropdown_refresh_icon_width"
+        android:layout_height="@dimen/autofill_dropdown_refresh_icon_height"
+        android:scaleType="centerInside"
+        android:layout_marginStart="0dp"
+        android:layout_marginEnd="@dimen/autofill_dropdown_refresh_icon_margin"
+        tools:ignore="ContentDescription" />
+
     <!-- TODO(crbug.com/903554): Remove the fixed layout_height in favor of an equivalent line
          height and padding. -->
     <LinearLayout
diff --git a/components/autofill/android/java/src/org/chromium/components/autofill/AutofillDropdownAdapter.java b/components/autofill/android/java/src/org/chromium/components/autofill/AutofillDropdownAdapter.java
index 2ff01d38..59f8a93 100644
--- a/components/autofill/android/java/src/org/chromium/components/autofill/AutofillDropdownAdapter.java
+++ b/components/autofill/android/java/src/org/chromium/components/autofill/AutofillDropdownAdapter.java
@@ -90,8 +90,16 @@
             // For refreshed layout, ignore the return value as we don't need to adjust the height
             // of the view.
             populateItemTagView(item, layout);
+            // Set the visibility of the start/end icons.
+            layout.findViewById(R.id.start_dropdown_icon)
+                    .setVisibility(item.isIconAtStart() ? View.VISIBLE : View.GONE);
+            layout.findViewById(R.id.end_dropdown_icon)
+                    .setVisibility(item.isIconAtStart() ? View.GONE : View.VISIBLE);
             ImageView iconView =
-                    populateIconView((ImageView) layout.findViewById(R.id.end_dropdown_icon), item);
+                    populateIconView((ImageView) (item.isIconAtStart()
+                                                     ? layout.findViewById(R.id.start_dropdown_icon)
+                                                     : layout.findViewById(R.id.end_dropdown_icon)),
+                            item);
             if (iconView != null) {
                 iconView.setLayoutParams(getSizeParamsForIconView(iconView, item));
             }
@@ -188,8 +196,10 @@
         ImageView iconViewEnd = (ImageView) layout.findViewById(R.id.end_dropdown_icon);
         if (item.isIconAtStart()) {
             iconViewEnd.setVisibility(View.GONE);
+            iconViewStart.setVisibility(View.VISIBLE);
         } else {
             iconViewStart.setVisibility(View.GONE);
+            iconViewEnd.setVisibility(View.VISIBLE);
         }
 
         ImageView iconView =
diff --git a/components/autofill/core/browser/autofill_form_test_utils.cc b/components/autofill/core/browser/autofill_form_test_utils.cc
index 3acab8e9..c58f17d 100644
--- a/components/autofill/core/browser/autofill_form_test_utils.cc
+++ b/components/autofill/core/browser/autofill_form_test_utils.cc
@@ -115,6 +115,7 @@
     ff.is_autofilled = dd.is_autofilled.value_or(false);
     ff.origin = dd.origin.value_or(f.main_frame_origin);
     ff.should_autocomplete = dd.should_autocomplete;
+    ff.properties_mask = dd.properties_mask;
     f.fields.push_back(ff);
   }
   return f;
diff --git a/components/autofill/core/browser/autofill_form_test_utils.h b/components/autofill/core/browser/autofill_form_test_utils.h
index 8da4714..498bfdf 100644
--- a/components/autofill/core/browser/autofill_form_test_utils.h
+++ b/components/autofill/core/browser/autofill_form_test_utils.h
@@ -47,6 +47,7 @@
   absl::optional<bool> is_autofilled;
   absl::optional<url::Origin> origin;
   std::vector<SelectOption> select_options = {};
+  FieldPropertiesMask properties_mask = 0;
 };
 
 // Attributes provided to the test form.
diff --git a/components/autofill/core/browser/autofill_suggestion_generator.cc b/components/autofill/core/browser/autofill_suggestion_generator.cc
index 248118f..d409d25 100644
--- a/components/autofill/core/browser/autofill_suggestion_generator.cc
+++ b/components/autofill/core/browser/autofill_suggestion_generator.cc
@@ -409,7 +409,10 @@
     if (image)
       suggestion.custom_icon = *image;
   }
-
+#if BUILDFLAG(IS_ANDROID)
+  // The card art icon should always be shown at the start of the suggestion.
+  suggestion.is_icon_at_start = true;
+#endif  // BUILDFLAG(IS_ANDROID)
   return suggestion;
 }
 
diff --git a/components/autofill/core/browser/browser_autofill_manager.cc b/components/autofill/core/browser/browser_autofill_manager.cc
index 1df3392..8d9e0641 100644
--- a/components/autofill/core/browser/browser_autofill_manager.cc
+++ b/components/autofill/core/browser/browser_autofill_manager.cc
@@ -695,8 +695,12 @@
       form_for_autocomplete.fields[i].should_autocomplete = false;
     }
 
+    // If the field was edited by the user and there existed an autofillable
+    // value for the field, log whether the value on submission is same as the
+    // autofillable value.
     if (submitted_form->field(i)
-            ->value_not_autofilled_over_existing_value_hash()) {
+            ->value_not_autofilled_over_existing_value_hash() &&
+        (submitted_form->field(i)->properties_mask & kUserTyped)) {
       // Compare and record if the currently filled value is same as the
       // non-empty value that was to be autofilled in the field.
       AutofillMetrics::
@@ -1922,6 +1926,8 @@
     // case.
     if (base::FeatureList::IsEnabled(
             features::kAutofillPreventOverridingPrefilledValues)) {
+      form_structure->field(i)
+          ->set_value_not_autofilled_over_existing_value_hash(absl::nullopt);
       if (form.fields[i].form_control_type != "select-one" &&
           !form.fields[i].value.empty() && !has_override &&
           !FormFieldData::DeepEqual(form.fields[i], field)) {
@@ -1933,18 +1939,16 @@
             optional_cvc ? *optional_cvc : kEmptyCvc, action,
             &unused_failure_to_fill);
         if (action == mojom::RendererFormDataAction::kFill &&
-            !fill_value.empty() && fill_value != form.fields[i].value) {
-          // Save the value that was supposed to be autofilled for this field.
+            !fill_value.empty() && fill_value != form.fields[i].value &&
+            !form_structure->field(i)->value.empty()) {
+          // Save the value that was supposed to be autofilled for this field if
+          // the field contained an initial value.
           form_structure->field(i)
               ->set_value_not_autofilled_over_existing_value_hash(
                   base::FastHash(base::UTF16ToUTF8(fill_value)));
         }
         continue;
       }
-
-      // Clear out the value in case the autofill happens for the field.
-      form_structure->field(i)
-          ->set_value_not_autofilled_over_existing_value_hash(absl::nullopt);
     }
 
     // Do not fill fields that have been edited by the user, except if the field
diff --git a/components/autofill/core/browser/data_model/autofill_profile_comparator_unittest.cc b/components/autofill/core/browser/data_model/autofill_profile_comparator_unittest.cc
index 129254b..5f547ac 100644
--- a/components/autofill/core/browser/data_model/autofill_profile_comparator_unittest.cc
+++ b/components/autofill/core/browser/data_model/autofill_profile_comparator_unittest.cc
@@ -1386,20 +1386,30 @@
 // Checks for various scenarios for determining mergeability of profiles w.r.t.
 // the state.
 TEST_P(AutofillProfileComparatorTest, CheckStatesMergeability) {
+  // |kAutofillEnableSupportForMoreStructureInAddresses| is not compatible with
+  // AlternativeStateNameMap merging logic.
+  if (structured_addresses_enabled_)
+    return;
+
   base::test::ScopedFeatureList feature;
   feature.InitAndEnableFeature(
       autofill::features::kAutofillUseAlternativeStateNameMap);
 
   autofill::test::ClearAlternativeStateNameMapForTesting();
-  autofill::test::PopulateAlternativeStateNameMapForTesting();
+  autofill::test::PopulateAlternativeStateNameMapForTesting(
+      "DE", "RandomState",
+      {{.canonical_name = "RandomState",
+        .abbreviations = {"RS"},
+        .alternative_names = {"AlternateRandomState"}}});
 
   AutofillProfile empty = CreateProfileWithAddress("", "", "", "", "", "DE");
-  AutofillProfile p1 = CreateProfileWithAddress("", "", "", "Bayern", "", "DE");
+  AutofillProfile p1 =
+      CreateProfileWithAddress("", "", "", "RandomState", "", "DE");
   AutofillProfile p2 = CreateProfileWithAddress("", "", "", "Random", "", "DE");
-  AutofillProfile p3 =
-      CreateProfileWithAddress("", "", "", "Bayern - BY - Bavaria", "", "DE");
+  AutofillProfile p3 = CreateProfileWithAddress(
+      "", "", "", "RandomState - RS - AlternateRandomState", "", "DE");
   AutofillProfile p4 =
-      CreateProfileWithAddress("", "", "", "Bavaria", "", "DE");
+      CreateProfileWithAddress("", "", "", "AlternateRandomState", "", "DE");
 
   EXPECT_TRUE(comparator_.HaveMergeableAddresses(empty, empty));
   EXPECT_TRUE(comparator_.HaveMergeableAddresses(p1, empty));
diff --git a/components/autofill/core/browser/metrics/autofill_metrics.cc b/components/autofill/core/browser/metrics/autofill_metrics.cc
index a02cffe..ef58aa9 100644
--- a/components/autofill/core/browser/metrics/autofill_metrics.cc
+++ b/components/autofill/core/browser/metrics/autofill_metrics.cc
@@ -3598,7 +3598,7 @@
 void AutofillMetrics::
     LogIsValueNotAutofilledOverExistingValueSameAsSubmittedValue(bool is_same) {
   base::UmaHistogramBoolean(
-      "Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue",
+      "Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue2",
       is_same);
 }
 
diff --git a/components/autofill/core/browser/metrics/autofill_metrics_unittest.cc b/components/autofill/core/browser/metrics/autofill_metrics_unittest.cc
index 0ec29043..d4e0825 100644
--- a/components/autofill/core/browser/metrics/autofill_metrics_unittest.cc
+++ b/components/autofill/core/browser/metrics/autofill_metrics_unittest.cc
@@ -11863,22 +11863,22 @@
 
 // Tests the following 4 cases when |kAutofillPreventOverridingPrefilledValues|
 // is enabled:
-// 1. The field is not autofilled since it has a prefilled value but the value
+// 1. The field is not autofilled since it has an initial value but the value
 //    is edited before the form submission and is same as the value that was
 //    to be autofilled in the field.
-//    |Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue|
+//    |Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue2|
 //    should emit true for this case.
-// 2. The field is not autofilled since it has a prefilled value but the value
+// 2. The field is not autofilled since it has an initial value but the value
 //    is edited before the form submission and is different than the value that
 //    was to be autofilled in the field.
-//    |Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue|
+//    |Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue2|
 //    should emit false for this case.
-// 3. The field had a prefilled value that was similar to the value to be
+// 3. The field had an initial value that was similar to the value to be
 //    autofilled in the field.
-//    |Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue|
+//    |Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue2|
 //    should not record anything in this case.
 // 4. Selection fields are always overridden by Autofill.
-//    |Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue|
+//    |Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue2|
 //    should not record anything in this case.
 TEST_F(AutofillMetricsTest,
        IsValueNotAutofilledOverExistingValueSameAsSubmittedValue) {
@@ -11889,20 +11889,23 @@
 
   FormData form = test::GetFormData(
       {.description_for_logging = "AutofilledStateFieldSource",
-       .fields = {{.role = ServerFieldType::NAME_FULL},
-                  {.role = ServerFieldType::ADDRESS_HOME_CITY,
-                   .value = u"Sacremento"},  // Case #1
-                  {.role = ServerFieldType::ADDRESS_HOME_STATE,
-                   .value = u"CA",
-                   .form_control_type = "select-one",
-                   .select_options = {{u"TN", u"Tennesse"},
-                                      {u"CA", u"California"},
-                                      {u"WA", u"Washington DC"}}},  // Case #4
-                  {.role = ServerFieldType::ADDRESS_HOME_ZIP,
-                   .value = u"00000"},  // Case #2
-                  {.role = ServerFieldType::PHONE_HOME_WHOLE_NUMBER,
-                   .value = u"12345678901"},  // Case #3
-                  {.role = ServerFieldType::ADDRESS_HOME_COUNTRY}}});
+       .fields = {
+           {.role = ServerFieldType::NAME_FULL},
+           {.role = ServerFieldType::ADDRESS_HOME_CITY,
+            .value = u"Sacremento",
+            .properties_mask = FieldPropertiesFlags::kUserTyped},  // Case #1
+           {.role = ServerFieldType::ADDRESS_HOME_STATE,
+            .value = u"CA",
+            .form_control_type = "select-one",
+            .select_options = {{u"TN", u"Tennesse"},
+                               {u"CA", u"California"},
+                               {u"WA", u"Washington DC"}}},  // Case #4
+           {.role = ServerFieldType::ADDRESS_HOME_ZIP,
+            .value = u"00000",
+            .properties_mask = FieldPropertiesFlags::kUserTyped},  // Case #2
+           {.role = ServerFieldType::PHONE_HOME_WHOLE_NUMBER,
+            .value = u"12345678901"},  // Case #3
+           {.role = ServerFieldType::ADDRESS_HOME_COUNTRY}}});
 
   std::vector<ServerFieldType> heuristic_types = {
       NAME_FULL,        ADDRESS_HOME_CITY,       ADDRESS_HOME_STATE,
@@ -11912,7 +11915,8 @@
       ADDRESS_HOME_ZIP, PHONE_HOME_WHOLE_NUMBER, ADDRESS_HOME_COUNTRY};
 
   // Simulate having seen this form on page load.
-  autofill_manager().AddSeenForm(form, heuristic_types, server_types);
+  autofill_manager().AddSeenForm(form, heuristic_types, server_types,
+                                 /*preserve_values_in_form_structure=*/true);
 
   autofill_manager().OnAskForValuesToFillTest(form, form.fields[0]);
   autofill_manager().DidShowSuggestions(
@@ -11942,10 +11946,10 @@
                                      SubmissionSource::FORM_SUBMISSION);
 
   histogram_tester.ExpectBucketCount(
-      "Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue",
+      "Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue2",
       true, 1);
   histogram_tester.ExpectBucketCount(
-      "Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue",
+      "Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue2",
       false, 1);
 }
 
diff --git a/components/autofill/core/browser/test_browser_autofill_manager.cc b/components/autofill/core/browser/test_browser_autofill_manager.cc
index 92cf2871..da912c1 100644
--- a/components/autofill/core/browser/test_browser_autofill_manager.cc
+++ b/components/autofill/core/browser/test_browser_autofill_manager.cc
@@ -107,7 +107,8 @@
 void TestBrowserAutofillManager::AddSeenForm(
     const FormData& form,
     const std::vector<ServerFieldType>& heuristic_types,
-    const std::vector<ServerFieldType>& server_types) {
+    const std::vector<ServerFieldType>& server_types,
+    bool preserve_values_in_form_structure) {
   std::vector<std::vector<std::pair<PatternSource, ServerFieldType>>>
       all_heuristic_types;
 
@@ -118,21 +119,24 @@
         return {{GetActivePatternSource(), type}};
       });
 
-  AddSeenForm(form, all_heuristic_types, server_types);
+  AddSeenForm(form, all_heuristic_types, server_types,
+              preserve_values_in_form_structure);
 }
 
 void TestBrowserAutofillManager::AddSeenForm(
     const FormData& form,
     const std::vector<std::vector<std::pair<PatternSource, ServerFieldType>>>&
         heuristic_types,
-    const std::vector<ServerFieldType>& server_types) {
-  FormData empty_form = form;
-  for (auto& field : empty_form.fields) {
+    const std::vector<ServerFieldType>& server_types,
+    bool preserve_values_in_form_structure) {
+  FormData form_with_empty_fields = form;
+  for (auto& field : form_with_empty_fields.fields) {
     field.value = std::u16string();
   }
 
   std::unique_ptr<TestFormStructure> form_structure =
-      std::make_unique<TestFormStructure>(empty_form);
+      std::make_unique<TestFormStructure>(
+          preserve_values_in_form_structure ? form : form_with_empty_fields);
   form_structure->SetFieldTypes(heuristic_types, server_types);
   form_structure->identify_sections_for_testing();
   AddSeenFormStructure(std::move(form_structure));
diff --git a/components/autofill/core/browser/test_browser_autofill_manager.h b/components/autofill/core/browser/test_browser_autofill_manager.h
index 422bf0e..d9078bf 100644
--- a/components/autofill/core/browser/test_browser_autofill_manager.h
+++ b/components/autofill/core/browser/test_browser_autofill_manager.h
@@ -58,13 +58,15 @@
 
   void AddSeenForm(const FormData& form,
                    const std::vector<ServerFieldType>& heuristic_types,
-                   const std::vector<ServerFieldType>& server_types);
+                   const std::vector<ServerFieldType>& server_types,
+                   bool preserve_values_in_form_structure = false);
 
   void AddSeenForm(
       const FormData& form,
       const std::vector<std::vector<std::pair<PatternSource, ServerFieldType>>>&
           heuristic_types,
-      const std::vector<ServerFieldType>& server_types);
+      const std::vector<ServerFieldType>& server_types,
+      bool preserve_values_in_form_structure);
 
   void AddSeenFormStructure(std::unique_ptr<FormStructure> form_structure);
 
diff --git a/components/autofill/core/browser/ui/suggestion.h b/components/autofill/core/browser/ui/suggestion.h
index 7216790b..1368f10a 100644
--- a/components/autofill/core/browser/ui/suggestion.h
+++ b/components/autofill/core/browser/ui/suggestion.h
@@ -131,6 +131,10 @@
   // The url for the custom icon. This is used by android to fetch the image as
   // android does not support gfx::Image directly.
   GURL custom_icon_url;
+
+  // On Android, the icon can be at the start of the suggestion before the label
+  // or at the end of the label.
+  bool is_icon_at_start = false;
 #endif  // BUILDFLAG(IS_ANDROID)
 
   // TODO(crbug.com/1019660): Identify icons with enum instead of strings.
diff --git a/components/autofill_assistant/browser/BUILD.gn b/components/autofill_assistant/browser/BUILD.gn
index ef62dac..a378876 100644
--- a/components/autofill_assistant/browser/BUILD.gn
+++ b/components/autofill_assistant/browser/BUILD.gn
@@ -266,6 +266,11 @@
     "starter.h",
     "starter_heuristic.cc",
     "starter_heuristic.h",
+    "starter_heuristic_configs/finch_starter_heuristic_config.cc",
+    "starter_heuristic_configs/finch_starter_heuristic_config.h",
+    "starter_heuristic_configs/legacy_starter_heuristic_config.cc",
+    "starter_heuristic_configs/legacy_starter_heuristic_config.h",
+    "starter_heuristic_configs/starter_heuristic_config.h",
     "starter_platform_delegate.h",
     "startup_util.cc",
     "startup_util.h",
@@ -550,6 +555,8 @@
     "service/service_impl_unittest.cc",
     "service/service_request_sender_impl_unittest.cc",
     "service/service_request_sender_local_impl_unittest.cc",
+    "starter_heuristic_configs/finch_starter_heuristic_config_unittest.cc",
+    "starter_heuristic_configs/legacy_starter_heuristic_config_unittest.cc",
     "starter_heuristic_unittest.cc",
     "starter_unittest.cc",
     "startup_util_unittest.cc",
diff --git a/components/autofill_assistant/browser/features.cc b/components/autofill_assistant/browser/features.cc
index 22ef1476..48dafce9 100644
--- a/components/autofill_assistant/browser/features.cc
+++ b/components/autofill_assistant/browser/features.cc
@@ -105,7 +105,19 @@
 const base::Feature kAutofillAssistantProactiveHelp{
     "AutofillAssistantProactiveHelp", base::FEATURE_ENABLED_BY_DEFAULT};
 
-// Used to configure the start heuristics for
+// Used to configure URL heuristics for upcoming new features.
+extern const base::Feature kAutofillAssistantUrlHeuristic1{
+    "AutofillAssistantUrlHeuristic1", base::FEATURE_DISABLED_BY_DEFAULT};
+extern const base::Feature kAutofillAssistantUrlHeuristic2{
+    "AutofillAssistantUrlHeuristic2", base::FEATURE_DISABLED_BY_DEFAULT};
+extern const base::Feature kAutofillAssistantUrlHeuristic3{
+    "AutofillAssistantUrlHeuristic3", base::FEATURE_DISABLED_BY_DEFAULT};
+extern const base::Feature kAutofillAssistantUrlHeuristic4{
+    "AutofillAssistantUrlHeuristic4", base::FEATURE_DISABLED_BY_DEFAULT};
+extern const base::Feature kAutofillAssistantUrlHeuristic5{
+    "AutofillAssistantUrlHeuristic5", base::FEATURE_DISABLED_BY_DEFAULT};
+
+// Legacy URL heuristics. Used to configure the start heuristics for
 // |kAutofillAssistantInCctTriggering| and/or
 // |kAutofillAssistantInTabTriggering|.
 const base::Feature kAutofillAssistantUrlHeuristics{
diff --git a/components/autofill_assistant/browser/features.h b/components/autofill_assistant/browser/features.h
index 6ae675f..1a5f159 100644
--- a/components/autofill_assistant/browser/features.h
+++ b/components/autofill_assistant/browser/features.h
@@ -30,6 +30,11 @@
 extern const base::Feature kAutofillAssistantLoadDFMForTriggerScripts;
 extern const base::Feature kAutofillAssistantProactiveHelp;
 extern const base::Feature kAutofillAssistantSignGetActionsRequests;
+extern const base::Feature kAutofillAssistantUrlHeuristic1;
+extern const base::Feature kAutofillAssistantUrlHeuristic2;
+extern const base::Feature kAutofillAssistantUrlHeuristic3;
+extern const base::Feature kAutofillAssistantUrlHeuristic4;
+extern const base::Feature kAutofillAssistantUrlHeuristic5;
 extern const base::Feature kAutofillAssistantUrlHeuristics;
 extern const base::Feature kAutofillAssistantVerifyGetActionsResponses;
 
diff --git a/components/autofill_assistant/browser/script_parameters.cc b/components/autofill_assistant/browser/script_parameters.cc
index e2c9baf..c9272204 100644
--- a/components/autofill_assistant/browser/script_parameters.cc
+++ b/components/autofill_assistant/browser/script_parameters.cc
@@ -115,6 +115,7 @@
 
 ScriptParameters::ScriptParameters() = default;
 ScriptParameters::~ScriptParameters() = default;
+ScriptParameters& ScriptParameters::operator=(ScriptParameters&&) = default;
 
 void ScriptParameters::MergeWith(const ScriptParameters& another) {
   for (const auto& param : another.parameters_) {
diff --git a/components/autofill_assistant/browser/script_parameters.h b/components/autofill_assistant/browser/script_parameters.h
index a6112be..9d357adf 100644
--- a/components/autofill_assistant/browser/script_parameters.h
+++ b/components/autofill_assistant/browser/script_parameters.h
@@ -24,6 +24,7 @@
   ~ScriptParameters();
   ScriptParameters(const ScriptParameters&) = delete;
   ScriptParameters& operator=(const ScriptParameters&) = delete;
+  ScriptParameters& operator=(ScriptParameters&&);
 
   // Merges |another| into this. Does not overwrite existing values.
   void MergeWith(const ScriptParameters& another);
diff --git a/components/autofill_assistant/browser/starter.cc b/components/autofill_assistant/browser/starter.cc
index 10bc943..2b97e5a 100644
--- a/components/autofill_assistant/browser/starter.cc
+++ b/components/autofill_assistant/browser/starter.cc
@@ -26,6 +26,7 @@
 #include "components/autofill_assistant/browser/service/service_request_sender_impl.h"
 #include "components/autofill_assistant/browser/service/service_request_sender_local_impl.h"
 #include "components/autofill_assistant/browser/service/simple_url_loader_factory.h"
+#include "components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config.h"
 #include "components/autofill_assistant/browser/switches.h"
 #include "components/autofill_assistant/browser/trigger_scripts/dynamic_trigger_conditions.h"
 #include "components/autofill_assistant/browser/trigger_scripts/static_trigger_conditions.h"
@@ -65,6 +66,20 @@
 const char kEnabledGroupName[] = "Enabled";
 const char kExperimentsSyntheticTrial[] = "AutofillAssistantExperimentsTrial";
 
+// String parameter containing the JSON-encoded parameter dictionary.
+const char kUrlHeuristicParametersKey[] = "json_parameters";
+// The 5 URL heuristics features that are defined for future use cases.
+constexpr base::FeatureParam<std::string> kUrlHeuristicParams1{
+    &features::kAutofillAssistantUrlHeuristic1, kUrlHeuristicParametersKey, ""};
+constexpr base::FeatureParam<std::string> kUrlHeuristicParams2{
+    &features::kAutofillAssistantUrlHeuristic2, kUrlHeuristicParametersKey, ""};
+constexpr base::FeatureParam<std::string> kUrlHeuristicParams3{
+    &features::kAutofillAssistantUrlHeuristic3, kUrlHeuristicParametersKey, ""};
+constexpr base::FeatureParam<std::string> kUrlHeuristicParams4{
+    &features::kAutofillAssistantUrlHeuristic4, kUrlHeuristicParametersKey, ""};
+constexpr base::FeatureParam<std::string> kUrlHeuristicParams5{
+    &features::kAutofillAssistantUrlHeuristic5, kUrlHeuristicParametersKey, ""};
+
 // Creates a service request sender that communicates with a remote endpoint.
 std::unique_ptr<ServiceRequestSender> CreateRpcTriggerScriptRequestSender(
     content::BrowserContext* browser_context,
@@ -84,15 +99,6 @@
       .value_or(false);
 }
 
-// The heuristic is shared across all instances and initialized on first use. As
-// such, we do not support updating the heuristic while Chrome is running.
-const scoped_refptr<StarterHeuristic> GetOrCreateStarterHeuristic() {
-  static const base::NoDestructor<scoped_refptr<StarterHeuristic>>
-      starter_heuristic(
-          [] { return base::MakeRefCounted<StarterHeuristic>(); }());
-  return *starter_heuristic;
-}
-
 // The cache of failed trigger script fetches is shared across all instances and
 // initialized on first use.
 base::HashingLRUCache<std::string, base::TimeTicks>*
@@ -174,8 +180,21 @@
       platform_delegate_(platform_delegate),
       ukm_recorder_(ukm_recorder),
       runtime_manager_(runtime_manager),
-      starter_heuristic_(GetOrCreateStarterHeuristic()),
-      tick_clock_(tick_clock) {}
+      starter_heuristic_(base::MakeRefCounted<StarterHeuristic>()),
+      tick_clock_(tick_clock) {
+  heuristic_configs_.emplace_back(
+      std::make_unique<LegacyStarterHeuristicConfig>());
+  heuristic_configs_.emplace_back(
+      std::make_unique<FinchStarterHeuristicConfig>(kUrlHeuristicParams1));
+  heuristic_configs_.emplace_back(
+      std::make_unique<FinchStarterHeuristicConfig>(kUrlHeuristicParams2));
+  heuristic_configs_.emplace_back(
+      std::make_unique<FinchStarterHeuristicConfig>(kUrlHeuristicParams3));
+  heuristic_configs_.emplace_back(
+      std::make_unique<FinchStarterHeuristicConfig>(kUrlHeuristicParams4));
+  heuristic_configs_.emplace_back(
+      std::make_unique<FinchStarterHeuristicConfig>(kUrlHeuristicParams5));
+}
 
 Starter::~Starter() = default;
 
@@ -372,18 +391,10 @@
       platform_delegate_->GetFeatureModuleInstalled();
   bool prev_fetch_trigger_scripts_on_navigation =
       fetch_trigger_scripts_on_navigation_;
-  // Note: the feature flag must be the last thing tested in this if-statement,
-  // to avoid tagging tabs that otherwise don't qualify for in-cct triggering,
-  // which leads to pollution of our metrics.
-  fetch_trigger_scripts_on_navigation_ =
-      proactive_help_setting_enabled && msbb_setting_enabled &&
-      (!platform_delegate_->GetIsWebLayer() ||
-       platform_delegate_->GetIsLoggedIn()) &&
-      ((is_custom_tab_ && platform_delegate_->GetIsTabCreatedByGSA() &&
-        base::FeatureList::IsEnabled(
-            features::kAutofillAssistantInCCTTriggering)) ||
-       (!is_custom_tab_ && base::FeatureList::IsEnabled(
-                               features::kAutofillAssistantInTabTriggering)));
+
+  starter_heuristic_->InitFromHeuristicConfigs(heuristic_configs_,
+                                               platform_delegate_.get());
+  fetch_trigger_scripts_on_navigation_ = starter_heuristic_->HasConditionSets();
 
   // If there is a pending startup, re-check that the settings are still
   // allowing the startup to proceed. If not, cancel the startup.
diff --git a/components/autofill_assistant/browser/starter.h b/components/autofill_assistant/browser/starter.h
index 795821e..7926515e 100644
--- a/components/autofill_assistant/browser/starter.h
+++ b/components/autofill_assistant/browser/starter.h
@@ -6,6 +6,7 @@
 #define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_STARTER_H_
 
 #include <memory>
+#include <vector>
 
 #include "base/containers/flat_set.h"
 #include "base/containers/lru_cache.h"
@@ -21,6 +22,8 @@
 #include "components/autofill_assistant/browser/public/runtime_manager.h"
 #include "components/autofill_assistant/browser/service.pb.h"
 #include "components/autofill_assistant/browser/starter_heuristic.h"
+#include "components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config.h"
+#include "components/autofill_assistant/browser/starter_heuristic_configs/starter_heuristic_config.h"
 #include "components/autofill_assistant/browser/starter_platform_delegate.h"
 #include "components/autofill_assistant/browser/startup_util.h"
 #include "components/autofill_assistant/browser/trigger_scripts/trigger_script_coordinator.h"
@@ -197,6 +200,7 @@
   // from the command line and intended only for debugging and testing.
   ImplicitTriggeringDebugParametersProto implicit_triggering_debug_parameters_;
 
+  std::vector<std::unique_ptr<StarterHeuristicConfig>> heuristic_configs_;
   bool waiting_for_onboarding_ = false;
   bool waiting_for_deeplink_navigation_ = false;
   bool is_custom_tab_ = false;
@@ -206,7 +210,7 @@
   bool fetch_trigger_scripts_on_navigation_ = false;
   std::unique_ptr<TriggerContext> pending_trigger_context_;
   std::unique_ptr<TriggerScriptCoordinator> trigger_script_coordinator_;
-  const scoped_refptr<StarterHeuristic> starter_heuristic_;
+  scoped_refptr<StarterHeuristic> starter_heuristic_;
   const raw_ptr<const base::TickClock> tick_clock_;
   base::OnceCallback<void(bool success,
                           absl::optional<GURL> url,
diff --git a/components/autofill_assistant/browser/starter_heuristic.cc b/components/autofill_assistant/browser/starter_heuristic.cc
index d848a68..1166ce0 100644
--- a/components/autofill_assistant/browser/starter_heuristic.cc
+++ b/components/autofill_assistant/browser/starter_heuristic.cc
@@ -24,124 +24,87 @@
 
 namespace autofill_assistant {
 
-// String parameter containing the JSON-encoded parameter dictionary.
-const char kJsonParameterDictKey[] = "json_parameters";
-
-constexpr base::FeatureParam<std::string> kFieldTrialParams{
-    &features::kAutofillAssistantUrlHeuristics, kJsonParameterDictKey, ""};
-
-// Array of strings, containing the list of globally denylisted domains.
-const char kDenylistedDomainsKey[] = "denylistedDomains";
-// Array of heuristics, each with its own intent and conditions.
-const char kHeuristicsKey[] = "heuristics";
-// String. The intent associated with a specific heuristic.
-const char kHeuristicIntentKey[] = "intent";
 // UrlFilter dictionary. The URL condition set defining a specific intent's
-// heuristic. See also components/url_matcher/url_matcher_factory.h
+// URL filter. See also components/url_matcher/url_matcher_factory.h
 const char kHeuristicUrlConditionSetKey[] = "conditionSet";
 
-StarterHeuristic::StarterHeuristic() {
-  InitFromTrialParams();
-}
-
+StarterHeuristic::StarterHeuristic() = default;
 StarterHeuristic::~StarterHeuristic() = default;
 
-void StarterHeuristic::InitFromTrialParams() {
-  DCHECK(matcher_id_to_intent_map_.empty())
-      << "Called after already initialized";
+void StarterHeuristic::InitFromHeuristicConfigs(
+    const std::vector<std::unique_ptr<StarterHeuristicConfig>>& configs,
+    StarterPlatformDelegate* platform_delegate) {
+  url_matcher_ = std::make_unique<url_matcher::URLMatcher>();
+  matcher_id_to_config_map_.clear();
 
-  std::string parameters = kFieldTrialParams.Get();
-  if (parameters.empty()) {
-    VLOG(2) << "Field trial parameter not set";
-    return;
-  }
-  auto dict = base::JSONReader::ReadAndReturnValueWithError(parameters);
-  if (!dict.has_value() || !dict->is_dict()) {
-    VLOG(1) << "Failed to parse field trial params as JSON object: "
-            << parameters;
-    if (VLOG_IS_ON(1)) {
-      if (dict.has_value())
-        VLOG(1) << "Expecting a dictionary";
-      else
-        VLOG(1) << dict.error().message << ", line: " << dict.error().line
-                << ", col: " << dict.error().column;
-    }
-    return;
-  }
-
-  // Read mandatory list of heuristics.
-  const base::Value::List* heuristics =
-      dict->GetDict().FindList(kHeuristicsKey);
-  if (heuristics == nullptr) {
-    VLOG(1) << "Field trial params did not contain heuristics";
-    return;
-  }
   url_matcher::URLMatcherConditionSet::Vector condition_sets;
-  base::flat_map<base::MatcherStringPattern::ID, std::string> mapping;
+  base::flat_map<base::MatcherStringPattern::ID, HeuristicConfigEntry> mapping;
   base::MatcherStringPattern::ID next_condition_set_id = 0;
-  for (const auto& heuristic : *heuristics) {
-    auto* intent =
-        heuristic.FindKeyOfType(kHeuristicIntentKey, base::Value::Type::STRING);
-    auto* url_conditions = heuristic.FindKeyOfType(
-        kHeuristicUrlConditionSetKey, base::Value::Type::DICTIONARY);
-    if (!intent || !url_conditions) {
-      VLOG(1) << "Heuristic did not contain intent or url_conditions";
-      return;
-    }
-
-    std::string error;
-    condition_sets.emplace_back(
-        url_matcher::URLMatcherFactory::CreateFromURLFilterDictionary(
-            url_matcher_.condition_factory(), url_conditions->GetDict(),
-            next_condition_set_id, &error));
-    if (!error.empty()) {
-      VLOG(1) << "Error pasing url conditions: " << error;
-      return;
-    }
-    mapping[next_condition_set_id++] = *intent->GetIfString();
-  }
-
-  // Read optional list of denylisted domains.
-  const base::Value::List* denylisted_domains_value =
-      dict->GetDict().FindList(kDenylistedDomainsKey);
-  base::flat_set<std::string> denylisted_domains;
-  if (denylisted_domains_value != nullptr) {
-    for (const auto& domain : *denylisted_domains_value) {
-      if (!domain.is_string()) {
-        VLOG(1) << "Invalid type for denylisted domain";
+  for (const auto& config : configs) {
+    for (const auto& condition_set :
+         config->GetConditionSetsForClientState(platform_delegate)) {
+      if (!condition_set.is_dict()) {
+        LOG(ERROR) << "Invalid heuristic config: expected a dictionary for "
+                      "each condition set, but got "
+                   << base::Value::GetTypeName(condition_set.type());
         return;
       }
-      denylisted_domains.insert(*domain.GetIfString());
+      auto* url_conditions = condition_set.FindKeyOfType(
+          kHeuristicUrlConditionSetKey, base::Value::Type::DICTIONARY);
+      if (!url_conditions) {
+        VLOG(1) << "Condition dict did not contain a value for 'conditionSet'";
+        return;
+      }
+
+      std::string error;
+      condition_sets.emplace_back(
+          url_matcher::URLMatcherFactory::CreateFromURLFilterDictionary(
+              url_matcher_->condition_factory(), url_conditions->GetDict(),
+              next_condition_set_id, &error));
+      if (!error.empty()) {
+        VLOG(1) << "Error pasing url conditions: " << error;
+        return;
+      }
+      mapping.insert(
+          std::make_pair(next_condition_set_id++,
+                         HeuristicConfigEntry(config->GetIntent(),
+                                              config->GetDenylistedDomains())));
     }
   }
 
-  VLOG(2) << "Read " << condition_sets.size() << " condition sets and "
-          << denylisted_domains.size() << " denylisted domains.";
-  denylisted_domains_ = std::move(denylisted_domains);
-  url_matcher_.AddConditionSets(condition_sets);
-  matcher_id_to_intent_map_ = std::move(mapping);
+  VLOG(2) << "Read " << condition_sets.size() << " condition sets from "
+          << configs.size() << " configs.";
+  url_matcher_->AddConditionSets(condition_sets);
+  matcher_id_to_config_map_ = std::move(mapping);
+}
+
+bool StarterHeuristic::HasConditionSets() const {
+  return !matcher_id_to_config_map_.empty();
 }
 
 base::flat_set<std::string> StarterHeuristic::IsHeuristicMatch(
-    const GURL& url) const {
+    const GURL& url,
+    base::flat_map<base::MatcherStringPattern::ID, HeuristicConfigEntry>
+        copied_matcher_id_to_config_map) const {
   base::flat_set<std::string> matching_intents;
-  if (matcher_id_to_intent_map_.empty() || !url.is_valid()) {
+  if (copied_matcher_id_to_config_map.empty() || !url.is_valid()) {
     return matching_intents;
   }
 
-  if (denylisted_domains_.count(
-          url_utils::GetOrganizationIdentifyingDomain(url)) > 0) {
-    return matching_intents;
-  }
-
-  std::set<base::MatcherStringPattern::ID> matches = url_matcher_.MatchURL(url);
+  std::set<base::MatcherStringPattern::ID> matches =
+      url_matcher_->MatchURL(url);
   for (const auto& match : matches) {
-    auto intent = matcher_id_to_intent_map_.find(match);
-    if (intent == matcher_id_to_intent_map_.end()) {
+    auto config = copied_matcher_id_to_config_map.find(match);
+    if (config == copied_matcher_id_to_config_map.end()) {
       DCHECK(false);
       continue;
     }
-    matching_intents.emplace(intent->second);
+    // Skip matches if they are in the denylist of that config.
+    if (config->second.denylisted_domains.count(
+            url_utils::GetOrganizationIdentifyingDomain(url)) > 0) {
+      continue;
+    }
+    matching_intents.emplace(config->second.intent);
   }
   return matching_intents;
 }
@@ -153,8 +116,16 @@
   base::ThreadPool::PostTaskAndReplyWithResult(
       FROM_HERE, {base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
       base::BindOnce(&StarterHeuristic::IsHeuristicMatch,
-                     base::RetainedRef(this), url),
+                     base::RetainedRef(this), url, matcher_id_to_config_map_),
       std::move(callback));
 }
 
+StarterHeuristic::HeuristicConfigEntry::HeuristicConfigEntry(
+    const std::string& _intent,
+    const base::flat_set<std::string>& _denylisted_domains)
+    : intent(_intent), denylisted_domains(_denylisted_domains) {}
+StarterHeuristic::HeuristicConfigEntry::HeuristicConfigEntry(
+    const HeuristicConfigEntry&) = default;
+StarterHeuristic::HeuristicConfigEntry::~HeuristicConfigEntry() = default;
+
 }  // namespace autofill_assistant
diff --git a/components/autofill_assistant/browser/starter_heuristic.h b/components/autofill_assistant/browser/starter_heuristic.h
index aa0e602..cacf68c 100644
--- a/components/autofill_assistant/browser/starter_heuristic.h
+++ b/components/autofill_assistant/browser/starter_heuristic.h
@@ -11,6 +11,8 @@
 #include "base/containers/flat_map.h"
 #include "base/containers/flat_set.h"
 #include "base/memory/ref_counted.h"
+#include "components/autofill_assistant/browser/starter_heuristic_configs/starter_heuristic_config.h"
+#include "components/autofill_assistant/browser/starter_platform_delegate.h"
 #include "components/url_matcher/url_matcher.h"
 #include "components/url_matcher/url_matcher_factory.h"
 #include "url/gurl.h"
@@ -27,6 +29,16 @@
   StarterHeuristic(const StarterHeuristic&) = delete;
   StarterHeuristic& operator=(const StarterHeuristic&) = delete;
 
+  // (Re-)initializes this starter heuristic from the given set of configs and
+  // the current client state.
+  void InitFromHeuristicConfigs(
+      const std::vector<std::unique_ptr<StarterHeuristicConfig>>& configs,
+      StarterPlatformDelegate* platform_delegate);
+
+  // Returns true if at least one condition set is available. There is no point
+  // in running the heuristic otherwise.
+  bool HasConditionSets() const;
+
   // Runs the heuristic against |url| and invokes the callback with all matching
   // intents.
   //
@@ -40,6 +52,19 @@
  private:
   friend class base::RefCountedThreadSafe<StarterHeuristic>;
   friend class StarterHeuristicTest;
+
+  // Corresponds to a particular heuristic config. Used to map URL matcher IDs
+  // to the originating heuristic config without having to take ownership of
+  // or otherwise directly interacting with those configs.
+  struct HeuristicConfigEntry {
+    HeuristicConfigEntry(const std::string& intent,
+                         const base::flat_set<std::string>& denylisted_domains);
+    HeuristicConfigEntry(const HeuristicConfigEntry&);
+    ~HeuristicConfigEntry();
+    std::string intent;
+    base::flat_set<std::string> denylisted_domains;
+  };
+
   ~StarterHeuristic();
 
   // Initializes the heuristic from the heuristic trial parameters. If there is
@@ -50,20 +75,17 @@
   void InitFromTrialParams();
 
   // Runs the heuristic against |url|. Returns all matching intents.
-  base::flat_set<std::string> IsHeuristicMatch(const GURL& url) const;
-
-  // The set of denylisted domains that will always return false before
-  // considering any of the intent heuristics.
-  base::flat_set<std::string> denylisted_domains_;
+  base::flat_set<std::string> IsHeuristicMatch(
+      const GURL& url,
+      base::flat_map<base::MatcherStringPattern::ID, HeuristicConfigEntry>
+          copied_matcher_id_to_config_map) const;
 
   // The URL matcher containing one URLMatcherConditionSet per supported intent.
-  url_matcher::URLMatcher url_matcher_;
+  std::unique_ptr<url_matcher::URLMatcher> url_matcher_;
 
-  // Arbitrary mapping of matcher IDs to intent strings. This mapping is built
-  // dynamically to allow the heuristic to work on intents that are otherwise
-  // unknown to the client.
-  base::flat_map<base::MatcherStringPattern::ID, std::string>
-      matcher_id_to_intent_map_;
+  // Arbitrary mapping of matcher IDs to heuristic configs.
+  base::flat_map<base::MatcherStringPattern::ID, HeuristicConfigEntry>
+      matcher_id_to_config_map_;
 };
 
 }  // namespace autofill_assistant
diff --git a/components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config.cc b/components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config.cc
new file mode 100644
index 0000000..3e6998105
--- /dev/null
+++ b/components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config.cc
@@ -0,0 +1,143 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config.h"
+#include "base/json/json_reader.h"
+
+namespace autofill_assistant {
+
+FinchStarterHeuristicConfig::FinchStarterHeuristicConfig(
+    const base::FeatureParam<std::string>& trial_parameter) {
+  InitFromTrialParams(trial_parameter);
+}
+
+FinchStarterHeuristicConfig::~FinchStarterHeuristicConfig() = default;
+
+const std::string& FinchStarterHeuristicConfig::GetIntent() const {
+  return intent_;
+}
+
+const base::Value::List&
+FinchStarterHeuristicConfig::GetConditionSetsForClientState(
+    StarterPlatformDelegate* platform_delegate) const {
+  if (platform_delegate->GetIsSupervisedUser()) {
+    return empty_condition_sets_.GetList();
+  }
+
+  if (!platform_delegate->GetProactiveHelpSettingEnabled()) {
+    return empty_condition_sets_.GetList();
+  }
+
+  if (platform_delegate->GetIsCustomTab() &&
+      (!platform_delegate->GetIsTabCreatedByGSA() ||
+       !enabled_in_custom_tabs_)) {
+    return empty_condition_sets_.GetList();
+  }
+
+  if (!platform_delegate->GetIsCustomTab() &&
+      !platform_delegate->GetIsWebLayer() && !enabled_in_regular_tabs_) {
+    return empty_condition_sets_.GetList();
+  }
+
+  if (platform_delegate->GetIsWebLayer() && !enabled_in_weblayer_) {
+    return empty_condition_sets_.GetList();
+  }
+
+  if (!platform_delegate->GetIsLoggedIn() && !enabled_for_signed_out_users_) {
+    return empty_condition_sets_.GetList();
+  }
+
+  if (!platform_delegate->GetMakeSearchesAndBrowsingBetterEnabled() &&
+      !enabled_without_msbb_) {
+    return empty_condition_sets_.GetList();
+  }
+
+  return condition_sets_.GetList();
+}
+
+const base::flat_set<std::string>&
+FinchStarterHeuristicConfig::GetDenylistedDomains() const {
+  return denylisted_domains_;
+}
+
+absl::optional<base::flat_set<std::string>>
+FinchStarterHeuristicConfig::ReadDenylistedDomains(
+    const base::Value::Dict& dict) const {
+  const base::Value::List* denylisted_domains_value =
+      dict.FindList(kDenylistedDomainsKey);
+  if (!denylisted_domains_value) {
+    return base::flat_set<std::string>{};
+  }
+
+  base::flat_set<std::string> denylisted_domains;
+  for (const auto& domain : *denylisted_domains_value) {
+    if (!domain.is_string()) {
+      VLOG(1) << "Invalid type for denylisted domain";
+      return absl::nullopt;
+    }
+    denylisted_domains.insert(*domain.GetIfString());
+  }
+  return denylisted_domains;
+}
+
+void FinchStarterHeuristicConfig::InitFromTrialParams(
+    const base::FeatureParam<std::string>& trial_parameter) {
+  std::string parameters = trial_parameter.Get();
+  if (parameters.empty()) {
+    VLOG(2) << "Field trial parameter not set";
+    return;
+  }
+  auto dict = base::JSONReader::ReadAndReturnValueWithError(parameters);
+  if (!dict.has_value() || !dict->is_dict()) {
+    VLOG(1) << "Failed to parse field trial params as JSON object: "
+            << parameters;
+    if (VLOG_IS_ON(1)) {
+      if (dict.has_value())
+        VLOG(1) << "Expecting a dictionary";
+      else
+        VLOG(1) << dict.error().message << ", line: " << dict.error().line
+                << ", col: " << dict.error().column;
+    }
+    return;
+  }
+
+  // Read the mandatory intent.
+  auto* intent = dict->GetDict().FindString(kIntentKey);
+  if (!intent) {
+    VLOG(1) << "Dictionary did not contain the intent parameter";
+    return;
+  }
+
+  // Read optional list of denylisted domains.
+  absl::optional<base::flat_set<std::string>> denylisted_domains =
+      ReadDenylistedDomains(dict->GetDict());
+  if (!denylisted_domains) {
+    return;
+  }
+
+  // Read condition sets.
+  base::Value::List* heuristics = dict->GetDict().FindList(kHeuristicsKey);
+  if (heuristics == nullptr) {
+    VLOG(1) << "Field trial params did not contain heuristics";
+    return;
+  }
+
+  // Read optional additional filters.
+  enabled_in_custom_tabs_ =
+      dict->GetDict().FindBool(kEnabledInCustomTabsKey).value_or(false);
+  enabled_in_regular_tabs_ =
+      dict->GetDict().FindBool(kEnabledInRegularTabsKey).value_or(false);
+  enabled_in_weblayer_ =
+      dict->GetDict().FindBool(kEnabledInWeblayerKey).value_or(false);
+  enabled_for_signed_out_users_ =
+      dict->GetDict().FindBool(kEnabledForSignedOutUsers).value_or(false);
+  enabled_without_msbb_ =
+      dict->GetDict().FindBool(kEnabledWithoutMsbb).value_or(false);
+
+  denylisted_domains_ = std::move(*denylisted_domains);
+  condition_sets_ = base::Value(std::move(*heuristics));
+  intent_.assign(*intent);
+}
+
+}  // namespace autofill_assistant
diff --git a/components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config.h b/components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config.h
new file mode 100644
index 0000000..c714a60
--- /dev/null
+++ b/components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config.h
@@ -0,0 +1,90 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_STARTER_HEURISTIC_CONFIGS_FINCH_STARTER_HEURISTIC_CONFIG_H_
+#define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_STARTER_HEURISTIC_CONFIGS_FINCH_STARTER_HEURISTIC_CONFIG_H_
+
+#include "base/metrics/field_trial_params.h"
+#include "components/autofill_assistant/browser/starter_heuristic_configs/starter_heuristic_config.h"
+
+namespace autofill_assistant {
+
+// Dictionary key for the list of denylisted domains.
+const char kDenylistedDomainsKey[] = "denylistedDomains";
+// Dictionary key for the list of heuristics.
+const char kHeuristicsKey[] = "heuristics";
+// Dictionary key for the intent.
+const char kIntentKey[] = "intent";
+// Dictionary keys for filters that can't be directly enforced via finch. If not
+// specified, these default to false, so at least some of them must be set. In
+// addition to the conditions here, supervised accounts are never supported, and
+// the proactive setting must be enabled as well.
+// Note that only custom tabs created by GSA are supported.
+const char kEnabledInCustomTabsKey[] = "enabledInCustomTabs";
+const char kEnabledInRegularTabsKey[] = "enabledInRegularTabs";
+const char kEnabledInWeblayerKey[] = "enabledInWeblayer";
+// Note: signed-in users default to true and need not be configured.
+const char kEnabledForSignedOutUsers[] = "enabledForSignedOutUsers";
+// Whether 'make searches and browsing better' is required or not. By default,
+// MSBB must be enabled.
+const char kEnabledWithoutMsbb[] = "enabledWithoutMsbb";
+
+// A heuristic config that is originating from a finch feature parameter.
+// The trial parameter must be a JSON object of the following format:
+/*
+  {
+    "intent": "SOME_INTENT",
+    "denylistedDomains": ["example.com", ...],
+    "heuristics": [
+      {"conditionSet": {...}},
+      {"conditionSet": {...}},
+      ...
+    ],
+    "enabledInCustomTabs":true,
+    "enabledInRegularTabs":false,
+    "enabledInWeblayer":false,
+    "enabledForSignedOutUsers":true
+    "enabledWithoutMsbb":false
+  }
+*/
+// The 'intent' parameter is mandatory. All other parameters are optional, but
+// at least one conditionSet and one enabled* flag must be set for the config
+// to be meaningful.
+class FinchStarterHeuristicConfig : public StarterHeuristicConfig {
+ public:
+  explicit FinchStarterHeuristicConfig(
+      const base::FeatureParam<std::string>& trial_parameter);
+  ~FinchStarterHeuristicConfig() override;
+
+  // Overrides HeuristicConfig:
+  const std::string& GetIntent() const override;
+  const base::Value::List& GetConditionSetsForClientState(
+      StarterPlatformDelegate* platform_delegate) const override;
+  const base::flat_set<std::string>& GetDenylistedDomains() const override;
+
+ private:
+  void InitFromTrialParams(
+      const base::FeatureParam<std::string>& trial_parameter);
+
+  // Returns the list of denylisted domains in |dict|. Returns the empty list
+  // if the relevant key does not exist in |dict|. Returns absl::nullopt if the
+  // format of the encountered denylist was invalid.
+  absl::optional<base::flat_set<std::string>> ReadDenylistedDomains(
+      const base::Value::Dict& dict) const;
+
+  bool enabled_in_custom_tabs_ = false;
+  bool enabled_in_regular_tabs_ = false;
+  bool enabled_in_weblayer_ = false;
+  bool enabled_for_signed_out_users_ = false;
+  bool enabled_without_msbb_ = false;
+  const base::Value empty_condition_sets_ =
+      base::Value(base::Value::Type::LIST);
+  std::string intent_;
+  base::Value condition_sets_ = base::Value(base::Value::Type::LIST);
+  base::flat_set<std::string> denylisted_domains_;
+};
+
+}  // namespace autofill_assistant
+
+#endif  // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_STARTER_HEURISTIC_CONFIGS_FINCH_STARTER_HEURISTIC_CONFIG_H_
diff --git a/components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config_unittest.cc b/components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config_unittest.cc
new file mode 100644
index 0000000..57eb20a5
--- /dev/null
+++ b/components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config_unittest.cc
@@ -0,0 +1,388 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config.h"
+#include "base/json/json_reader.h"
+#include "base/test/scoped_feature_list.h"
+#include "components/autofill_assistant/browser/fake_starter_platform_delegate.h"
+#include "components/autofill_assistant/browser/features.h"
+
+#include "testing/gmock/include/gmock/gmock.h"
+
+namespace autofill_assistant {
+
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::IsEmpty;
+using ::testing::SizeIs;
+using ::testing::UnorderedElementsAreArray;
+
+class FinchStarterHeuristicConfigTest : public testing::Test {
+ public:
+  FinchStarterHeuristicConfigTest() = default;
+  ~FinchStarterHeuristicConfigTest() override = default;
+
+ protected:
+  // Convenience for tests where the contents of the heuristic don't matter.
+  void InitDefaultHeuristic(const base::Feature& feature,
+                            const std::string& parameter_key) {
+    scoped_feature_list_ = std::make_unique<base::test::ScopedFeatureList>();
+    scoped_feature_list_->InitWithFeaturesAndParameters(
+        {{feature, {{parameter_key, R"(
+        {
+          "intent":"FAKE_INTENT",
+          "denylistedDomains":["example.com"],
+          "enabledInCustomTabs":true,
+          "enabledInRegularTabs":true,
+          "enabledInWeblayer": true,
+          "enabledForSignedOutUsers": true,
+          "heuristics":[
+            {
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            }
+          ]
+        }
+        )"}}}},
+        /* disabled_features = */ {});
+  }
+
+  FakeStarterPlatformDelegate fake_platform_delegate_;
+
+ private:
+  std::unique_ptr<base::test::ScopedFeatureList> scoped_feature_list_;
+};
+
+TEST_F(FinchStarterHeuristicConfigTest, SmokeTest) {
+  InitDefaultHeuristic(features::kAutofillAssistantUrlHeuristic1, "some_key");
+
+  FinchStarterHeuristicConfig config_enabled(base::FeatureParam<std::string>{
+      &features::kAutofillAssistantUrlHeuristic1, "some_key", ""});
+  EXPECT_THAT(
+      config_enabled.GetConditionSetsForClientState(&fake_platform_delegate_),
+      SizeIs(1));
+
+  // UrlHeuristic2 was not enabled, so this should return the empty list.
+  FinchStarterHeuristicConfig config_default_disabled(
+      base::FeatureParam<std::string>{
+          &features::kAutofillAssistantUrlHeuristic2, "some_key", ""});
+  EXPECT_THAT(config_default_disabled.GetConditionSetsForClientState(
+                  &fake_platform_delegate_),
+              IsEmpty());
+}
+
+TEST_F(FinchStarterHeuristicConfigTest, DefaultHeuristicParsedCorrectly) {
+  InitDefaultHeuristic(features::kAutofillAssistantUrlHeuristic1, "some_key");
+  FinchStarterHeuristicConfig config(base::FeatureParam<std::string>{
+      &features::kAutofillAssistantUrlHeuristic1, "some_key", ""});
+
+  EXPECT_THAT(config.GetIntent(), Eq("FAKE_INTENT"));
+  EXPECT_THAT(
+      config.GetDenylistedDomains(),
+      UnorderedElementsAreArray(std::vector<std::string>{"example.com"}));
+  EXPECT_EQ(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+            base::JSONReader::Read(R"([
+            {
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            }
+          ])")
+                ->GetList());
+}
+
+TEST_F(FinchStarterHeuristicConfigTest, DisabledForSupervisedUsers) {
+  InitDefaultHeuristic(features::kAutofillAssistantUrlHeuristic1, "some_key");
+  FinchStarterHeuristicConfig config(base::FeatureParam<std::string>{
+      &features::kAutofillAssistantUrlHeuristic1, "some_key", ""});
+
+  fake_platform_delegate_.is_supervised_user_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+
+  fake_platform_delegate_.is_supervised_user_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+}
+
+TEST_F(FinchStarterHeuristicConfigTest, DisabledIfProactiveHelpSettingOff) {
+  InitDefaultHeuristic(features::kAutofillAssistantUrlHeuristic1, "some_key");
+  FinchStarterHeuristicConfig config(base::FeatureParam<std::string>{
+      &features::kAutofillAssistantUrlHeuristic1, "some_key", ""});
+
+  fake_platform_delegate_.proactive_help_enabled_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+
+  fake_platform_delegate_.proactive_help_enabled_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+}
+
+TEST_F(FinchStarterHeuristicConfigTest, FlagsDefaultToFalse) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristic1, {{"some_key", R"(
+        {
+          "intent":"FAKE_INTENT",
+          "denylistedDomains":["example.com"],
+          "heuristics":[
+            {
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            }
+          ]
+        }
+        )"}}}},
+      /* disabled_features = */ {});
+
+  FinchStarterHeuristicConfig config(base::FeatureParam<std::string>{
+      &features::kAutofillAssistantUrlHeuristic1, "some_key", ""});
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+}
+
+TEST_F(FinchStarterHeuristicConfigTest, EnabledInCustomTabsOnly) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristic1, {{"some_key", R"(
+        {
+          "intent":"FAKE_INTENT",
+          "denylistedDomains":["example.com"],
+          "heuristics":[
+            {
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            }
+          ],
+          "enabledInCustomTabs":true
+        }
+        )"}}}},
+      /* disabled_features = */ {});
+
+  FinchStarterHeuristicConfig config(base::FeatureParam<std::string>{
+      &features::kAutofillAssistantUrlHeuristic1, "some_key", ""});
+
+  fake_platform_delegate_.is_web_layer_ = false;
+  fake_platform_delegate_.is_tab_created_by_gsa_ = false;
+  fake_platform_delegate_.is_custom_tab_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+
+  fake_platform_delegate_.is_tab_created_by_gsa_ = true;
+  fake_platform_delegate_.is_custom_tab_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+
+  fake_platform_delegate_.is_custom_tab_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+
+  // In reality, these two flags should be mutually exclusive.
+  fake_platform_delegate_.is_custom_tab_ = true;
+  fake_platform_delegate_.is_web_layer_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+}
+
+TEST_F(FinchStarterHeuristicConfigTest, EnabledInRegularTabsOnly) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristic1, {{"some_key", R"(
+        {
+          "intent":"FAKE_INTENT",
+          "denylistedDomains":["example.com"],
+          "heuristics":[
+            {
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            }
+          ],
+          "enabledInRegularTabs":true
+        }
+        )"}}}},
+      /* disabled_features = */ {});
+
+  FinchStarterHeuristicConfig config(base::FeatureParam<std::string>{
+      &features::kAutofillAssistantUrlHeuristic1, "some_key", ""});
+
+  fake_platform_delegate_.is_custom_tab_ = false;
+  fake_platform_delegate_.is_web_layer_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+
+  fake_platform_delegate_.is_custom_tab_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+
+  fake_platform_delegate_.is_custom_tab_ = false;
+  fake_platform_delegate_.is_web_layer_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+}
+
+TEST_F(FinchStarterHeuristicConfigTest, EnabledInWeblayerOnly) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristic1, {{"some_key", R"(
+        {
+          "intent":"FAKE_INTENT",
+          "denylistedDomains":["example.com"],
+          "heuristics":[
+            {
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            }
+          ],
+          "enabledInWeblayer":true
+        }
+        )"}}}},
+      /* disabled_features = */ {});
+
+  FinchStarterHeuristicConfig config(base::FeatureParam<std::string>{
+      &features::kAutofillAssistantUrlHeuristic1, "some_key", ""});
+
+  fake_platform_delegate_.is_custom_tab_ = false;
+  fake_platform_delegate_.is_web_layer_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+
+  fake_platform_delegate_.is_web_layer_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+}
+
+TEST_F(FinchStarterHeuristicConfigTest, EnabledForSignedOutUsers) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristic1, {{"some_key", R"(
+        {
+          "intent":"FAKE_INTENT",
+          "denylistedDomains":["example.com"],
+          "heuristics":[
+            {
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            }
+          ],
+          "enabledForSignedOutUsers":true,
+          "enabledInCustomTabs":true
+        }
+        )"}}}},
+      /* disabled_features = */ {});
+
+  FinchStarterHeuristicConfig config(base::FeatureParam<std::string>{
+      &features::kAutofillAssistantUrlHeuristic1, "some_key", ""});
+
+  fake_platform_delegate_.is_web_layer_ = false;
+  fake_platform_delegate_.is_custom_tab_ = true;
+  fake_platform_delegate_.is_logged_in_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+
+  fake_platform_delegate_.is_custom_tab_ = true;
+  fake_platform_delegate_.is_logged_in_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+
+  fake_platform_delegate_.is_custom_tab_ = false;
+  fake_platform_delegate_.is_logged_in_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+
+  fake_platform_delegate_.is_custom_tab_ = false;
+  fake_platform_delegate_.is_logged_in_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+}
+
+TEST_F(FinchStarterHeuristicConfigTest, EnabledWithoutMsbb) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristic1, {{"some_key", R"(
+        {
+          "intent":"FAKE_INTENT",
+          "denylistedDomains":["example.com"],
+          "heuristics":[
+            {
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            }
+          ],
+          "enabledWithoutMsbb":true,
+          "enabledInCustomTabs":true
+        }
+        )"}}}},
+      /* disabled_features = */ {});
+
+  FinchStarterHeuristicConfig config(base::FeatureParam<std::string>{
+      &features::kAutofillAssistantUrlHeuristic1, "some_key", ""});
+
+  fake_platform_delegate_.is_web_layer_ = false;
+  fake_platform_delegate_.is_custom_tab_ = true;
+  fake_platform_delegate_.msbb_enabled_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+
+  fake_platform_delegate_.is_custom_tab_ = true;
+  fake_platform_delegate_.msbb_enabled_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+
+  fake_platform_delegate_.is_custom_tab_ = false;
+  fake_platform_delegate_.msbb_enabled_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+
+  fake_platform_delegate_.is_custom_tab_ = false;
+  fake_platform_delegate_.msbb_enabled_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+}
+
+TEST_F(FinchStarterHeuristicConfigTest, MultipleConditionSets) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristic1, {{"some_key", R"(
+        {
+          "intent":"FAKE_INTENT",
+          "denylistedDomains":["example.com"],
+          "heuristics":[
+            {
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            },
+            {
+              "conditionSet":{
+                "urlContains":"different"
+              }
+            }
+          ],
+          "enabledInCustomTabs":true
+        }
+        )"}}}},
+      /* disabled_features = */ {});
+
+  FinchStarterHeuristicConfig config(base::FeatureParam<std::string>{
+      &features::kAutofillAssistantUrlHeuristic1, "some_key", ""});
+
+  fake_platform_delegate_.is_web_layer_ = false;
+  fake_platform_delegate_.is_custom_tab_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(2));
+
+  fake_platform_delegate_.is_custom_tab_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+}
+
+}  // namespace autofill_assistant
diff --git a/components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config.cc b/components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config.cc
new file mode 100644
index 0000000..562ae9f
--- /dev/null
+++ b/components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config.cc
@@ -0,0 +1,160 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config.h"
+#include "base/json/json_reader.h"
+#include "base/metrics/field_trial_params.h"
+#include "components/autofill_assistant/browser/features.h"
+#include "components/autofill_assistant/browser/starter_heuristic_configs/finch_starter_heuristic_config.h"
+
+namespace autofill_assistant {
+
+const char kJsonParameterDictKey[] = "json_parameters";
+constexpr base::FeatureParam<std::string> kLegacyFieldTrialParams{
+    &features::kAutofillAssistantUrlHeuristics, kJsonParameterDictKey, ""};
+
+LegacyStarterHeuristicConfig::LegacyStarterHeuristicConfig() {
+  InitFromTrialParams();
+}
+
+LegacyStarterHeuristicConfig::~LegacyStarterHeuristicConfig() = default;
+
+const std::string& LegacyStarterHeuristicConfig::GetIntent() const {
+  return intent_;
+}
+
+const base::Value::List&
+LegacyStarterHeuristicConfig::GetConditionSetsForClientState(
+    StarterPlatformDelegate* platform_delegate) const {
+  if (platform_delegate->GetIsSupervisedUser()) {
+    return empty_condition_sets_.GetList();
+  }
+
+  if (!platform_delegate->GetProactiveHelpSettingEnabled()) {
+    return empty_condition_sets_.GetList();
+  }
+
+  if (platform_delegate->GetIsCustomTab() &&
+      !platform_delegate->GetIsTabCreatedByGSA()) {
+    return empty_condition_sets_.GetList();
+  }
+
+  // The legacy config used a separate finch feature to gate CCT vs. non-CCT
+  // support. In new configs, these can be specified directly in the params.
+  if (platform_delegate->GetIsCustomTab() &&
+      !base::FeatureList::IsEnabled(
+          features::kAutofillAssistantInCCTTriggering)) {
+    return empty_condition_sets_.GetList();
+  }
+
+  if (!platform_delegate->GetIsCustomTab() &&
+      !base::FeatureList::IsEnabled(
+          features::kAutofillAssistantInTabTriggering)) {
+    return empty_condition_sets_.GetList();
+  }
+
+  // The legacy config used to only be available for signed-in users in
+  // weblayer.
+  if (platform_delegate->GetIsWebLayer() &&
+      !platform_delegate->GetIsLoggedIn()) {
+    return empty_condition_sets_.GetList();
+  }
+
+  return condition_sets_.GetList();
+}
+
+const base::flat_set<std::string>&
+LegacyStarterHeuristicConfig::GetDenylistedDomains() const {
+  return denylisted_domains_;
+}
+
+absl::optional<base::flat_set<std::string>>
+LegacyStarterHeuristicConfig::ReadDenylistedDomains(
+    const base::Value::Dict& dict) const {
+  const base::Value::List* denylisted_domains_value =
+      dict.FindList(kDenylistedDomainsKey);
+  if (!denylisted_domains_value) {
+    return base::flat_set<std::string>{};
+  }
+
+  base::flat_set<std::string> denylisted_domains;
+  for (const auto& domain : *denylisted_domains_value) {
+    if (!domain.is_string()) {
+      VLOG(1) << "Invalid type for denylisted domain";
+      return absl::nullopt;
+    }
+    denylisted_domains.insert(*domain.GetIfString());
+  }
+  return denylisted_domains;
+}
+
+absl::optional<std::pair<base::Value, std::string>>
+LegacyStarterHeuristicConfig::ReadConditionSetsAndIntent(
+    const base::Value::Dict& dict) const {
+  const base::Value::List* condition_sets_list = dict.FindList(kHeuristicsKey);
+  if (!condition_sets_list) {
+    VLOG(1) << "Field trial params did not contain condition sets";
+    return absl::nullopt;
+  }
+
+  // In this legacy config, the INTENT script parameter was specified as part of
+  // each individual heuristic entry (and not one overall). Thus, it was
+  // technically possible to supply different INTENTS per heuristic. This was
+  // never actually used. For legacy treatment, we simply take the first
+  // specified INTENT here.
+  std::string intent;
+  if (!condition_sets_list->empty()) {
+    auto* intent_param = condition_sets_list->front().FindKeyOfType(
+        kIntentKey, base::Value::Type::STRING);
+    if (!intent_param) {
+      VLOG(1) << "Heuristic did not contain the intent parameter";
+      return absl::nullopt;
+    }
+    intent.assign(*intent_param->GetIfString());
+  }
+
+  return std::make_pair(base::Value(condition_sets_list->Clone()), intent);
+}
+
+void LegacyStarterHeuristicConfig::InitFromTrialParams() {
+  std::string parameters = kLegacyFieldTrialParams.Get();
+  if (parameters.empty()) {
+    VLOG(2) << "Field trial parameter not set";
+    return;
+  }
+  auto dict = base::JSONReader::ReadAndReturnValueWithError(parameters);
+  if (!dict.has_value() || !dict->is_dict()) {
+    VLOG(1) << "Failed to parse field trial params as JSON object: "
+            << parameters;
+    if (VLOG_IS_ON(1)) {
+      if (dict.has_value()) {
+        VLOG(1) << "Expecting a dictionary";
+      } else {
+        VLOG(1) << dict.error().message << ", line: " << dict.error().line
+                << ", col: " << dict.error().column;
+      }
+    }
+    return;
+  }
+
+  // Read optional list of denylisted domains.
+  absl::optional<base::flat_set<std::string>> denylisted_domains =
+      ReadDenylistedDomains(dict->GetDict());
+  if (!denylisted_domains) {
+    return;
+  }
+
+  // Read condition sets and intent.
+  absl::optional<std::pair<base::Value, std::string>>
+      condition_sets_and_intent = ReadConditionSetsAndIntent(dict->GetDict());
+  if (!condition_sets_and_intent) {
+    return;
+  }
+
+  denylisted_domains_ = std::move(*denylisted_domains);
+  condition_sets_ = base::Value(std::move(condition_sets_and_intent->first));
+  intent_.assign(condition_sets_and_intent->second);
+}
+
+}  // namespace autofill_assistant
diff --git a/components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config.h b/components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config.h
new file mode 100644
index 0000000..aaa7390
--- /dev/null
+++ b/components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config.h
@@ -0,0 +1,54 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_STARTER_HEURISTIC_CONFIGS_LEGACY_STARTER_HEURISTIC_CONFIG_H_
+#define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_STARTER_HEURISTIC_CONFIGS_LEGACY_STARTER_HEURISTIC_CONFIG_H_
+
+#include <string>
+
+#include "base/containers/flat_map.h"
+#include "components/autofill_assistant/browser/script_parameters.h"
+#include "components/autofill_assistant/browser/starter_heuristic_configs/starter_heuristic_config.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace autofill_assistant {
+
+// The legacy config. Some smaller changes have been made to the format of field
+// trial parameters since then, so this class provides a legacy layer for the
+// old trial until we can phase it out.
+class LegacyStarterHeuristicConfig : public StarterHeuristicConfig {
+ public:
+  LegacyStarterHeuristicConfig();
+  ~LegacyStarterHeuristicConfig() override;
+
+  // Overrides HeuristicConfig:
+  const std::string& GetIntent() const override;
+  const base::Value::List& GetConditionSetsForClientState(
+      StarterPlatformDelegate* platform_delegate) const override;
+  const base::flat_set<std::string>& GetDenylistedDomains() const override;
+
+ private:
+  void InitFromTrialParams();
+
+  // Returns the list of denylisted domains in |dict|. Returns the empty list
+  // if the relevant key does not exist in |dict|. Returns absl::nullopt if the
+  // format of the encountered denylist was invalid.
+  absl::optional<base::flat_set<std::string>> ReadDenylistedDomains(
+      const base::Value::Dict& dict) const;
+
+  // Reads the condition sets and intent in |dict|. Returns absl::nullopt if
+  // either of these parameters is invalid.
+  absl::optional<std::pair<base::Value, std::string>>
+  ReadConditionSetsAndIntent(const base::Value::Dict& dict) const;
+
+  const base::Value empty_condition_sets_ =
+      base::Value(base::Value::Type::LIST);
+  std::string intent_;
+  base::Value condition_sets_ = base::Value(base::Value::Type::LIST);
+  base::flat_set<std::string> denylisted_domains_;
+};
+
+}  // namespace autofill_assistant
+
+#endif  // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_STARTER_HEURISTIC_CONFIGS_LEGACY_STARTER_HEURISTIC_CONFIG_H_
diff --git a/components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config_unittest.cc b/components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config_unittest.cc
new file mode 100644
index 0000000..3924864c
--- /dev/null
+++ b/components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config_unittest.cc
@@ -0,0 +1,379 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config.h"
+#include "base/json/json_reader.h"
+#include "base/test/scoped_feature_list.h"
+#include "components/autofill_assistant/browser/fake_starter_platform_delegate.h"
+#include "components/autofill_assistant/browser/features.h"
+
+#include "testing/gmock/include/gmock/gmock.h"
+
+namespace autofill_assistant {
+
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::IsEmpty;
+using ::testing::SizeIs;
+using ::testing::UnorderedElementsAreArray;
+
+class LegacyStarterHeuristicConfigTest : public testing::Test {
+ public:
+  LegacyStarterHeuristicConfigTest() = default;
+  ~LegacyStarterHeuristicConfigTest() override = default;
+
+ protected:
+  // Convenience for tests where the contents of the heuristic don't matter.
+  void InitDefaultHeuristic() {
+    scoped_feature_list_ = std::make_unique<base::test::ScopedFeatureList>();
+    scoped_feature_list_->InitWithFeaturesAndParameters(
+        {{features::kAutofillAssistantUrlHeuristics, {{"json_parameters", R"(
+        {
+          "heuristics":[
+            {
+              "intent":"FAKE_INTENT",
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            }
+          ]
+        }
+        )"}}}},
+        /* disabled_features = */ {});
+  }
+
+  FakeStarterPlatformDelegate fake_platform_delegate_;
+
+ private:
+  std::unique_ptr<base::test::ScopedFeatureList> scoped_feature_list_;
+};
+
+TEST_F(LegacyStarterHeuristicConfigTest, DisabledIfNoFeatureParam) {
+  LegacyStarterHeuristicConfig config;
+  EXPECT_THAT(config.GetDenylistedDomains(), IsEmpty());
+  EXPECT_THAT(config.GetIntent(), IsEmpty());
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, DenylistedDomains) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristics, {{"json_parameters", R"(
+        {
+          "denylistedDomains":["example.com", "different.com"],
+          "heuristics":[
+            {
+              "intent":"FAKE_INTENT_A",
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            }
+          ]
+        }
+        )"}}},
+       {features::kAutofillAssistantInCCTTriggering, {}}},
+      /* disabled_features = */ {});
+
+  LegacyStarterHeuristicConfig config;
+  EXPECT_THAT(config.GetDenylistedDomains(),
+              UnorderedElementsAreArray(
+                  std::vector<std::string>{"example.com", "different.com"}));
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, MultipleConditionSets) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristics, {{"json_parameters", R"(
+        {
+          "heuristics":[
+            {
+              "intent":"FAKE_INTENT_A",
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            },
+            {
+              "intent":"FAKE_INTENT_B",
+              "conditionSet":{
+                "urlContains":"different"
+              }
+            }
+          ]
+        }
+        )"}}},
+       {features::kAutofillAssistantInCCTTriggering, {}}},
+      /* disabled_features = */ {});
+
+  LegacyStarterHeuristicConfig config;
+  fake_platform_delegate_.is_custom_tab_ = true;
+  fake_platform_delegate_.is_tab_created_by_gsa_ = true;
+  fake_platform_delegate_.is_web_layer_ = false;
+  EXPECT_EQ(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+            base::JSONReader::Read(R"([
+            {
+              "intent":"FAKE_INTENT_A",
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            },
+            {
+              "intent":"FAKE_INTENT_B",
+              "conditionSet":{
+                "urlContains":"different"
+              }
+            }
+          ])")
+                ->GetList());
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, UseFirstIntentIfMultipleSpecified) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristics, {{"json_parameters", R"(
+        {
+          "heuristics":[
+            {
+              "intent":"FAKE_INTENT_A",
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            },
+            {
+              "intent":"FAKE_INTENT_B",
+              "conditionSet":{
+                "urlContains":"different"
+              }
+            }
+          ]
+        }
+        )"}}},
+       {features::kAutofillAssistantInCCTTriggering, {}}},
+      /* disabled_features = */ {});
+
+  LegacyStarterHeuristicConfig config;
+  EXPECT_THAT(config.GetIntent(), Eq("FAKE_INTENT_A"));
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, OnlyEnabledInCustomTab) {
+  InitDefaultHeuristic();
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeatures(
+      /* enabled_features = */ {features::kAutofillAssistantInCCTTriggering},
+      /* disabled_features = */ {features::kAutofillAssistantInTabTriggering});
+  LegacyStarterHeuristicConfig config;
+
+  // Allowed: custom tabs created by GSA.
+  fake_platform_delegate_.is_custom_tab_ = true;
+  fake_platform_delegate_.is_tab_created_by_gsa_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+
+  // Not a custom tab.
+  fake_platform_delegate_.is_custom_tab_ = false;
+  fake_platform_delegate_.is_tab_created_by_gsa_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+
+  // CCT not created by GSA.
+  fake_platform_delegate_.is_custom_tab_ = true;
+  fake_platform_delegate_.is_tab_created_by_gsa_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, OnlyEnabledInRegularTab) {
+  InitDefaultHeuristic();
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeatures(
+      /* enabled_features = */ {features::kAutofillAssistantInTabTriggering},
+      /* disabled_features = */ {features::kAutofillAssistantInCCTTriggering});
+  LegacyStarterHeuristicConfig config;
+
+  // Allowed: regular tabs.
+  fake_platform_delegate_.is_custom_tab_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+
+  // Not a regular tab.
+  fake_platform_delegate_.is_custom_tab_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, EnabledInAllTabs) {
+  InitDefaultHeuristic();
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeatures(
+      /* enabled_features = */ {features::kAutofillAssistantInTabTriggering,
+                                features::kAutofillAssistantInCCTTriggering},
+      /* disabled_features = */ {});
+  LegacyStarterHeuristicConfig config;
+
+  // Allowed: regular tabs.
+  fake_platform_delegate_.is_custom_tab_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+
+  // Allowed: custom tabs.
+  fake_platform_delegate_.is_custom_tab_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, OnlySignedInUsersInWeblayer) {
+  InitDefaultHeuristic();
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeatures(
+      /* enabled_features = */ {features::kAutofillAssistantInCCTTriggering},
+      /* disabled_features = */ {features::kAutofillAssistantInTabTriggering});
+  LegacyStarterHeuristicConfig config;
+
+  // For weblayer, only signed in users are allowed.
+  fake_platform_delegate_.is_custom_tab_ = true;
+  fake_platform_delegate_.is_tab_created_by_gsa_ = true;
+  fake_platform_delegate_.is_web_layer_ = true;
+  fake_platform_delegate_.is_logged_in_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+
+  fake_platform_delegate_.is_custom_tab_ = false;
+  fake_platform_delegate_.is_tab_created_by_gsa_ = true;
+  fake_platform_delegate_.is_web_layer_ = true;
+  fake_platform_delegate_.is_logged_in_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, InvalidDenylistedDomains) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristics, {{"json_parameters", R"(
+        {
+          "denylistedDomains":[{"invalid_nested_in_object"}],
+          "heuristics":[
+            {
+              "intent":"FAKE_INTENT_A",
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            }
+          ]
+        }
+        )"}}},
+       {features::kAutofillAssistantInTabTriggering, {}},
+       {features::kAutofillAssistantInCCTTriggering, {}}},
+      /* disabled_features = */ {});
+
+  LegacyStarterHeuristicConfig config;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+  EXPECT_THAT(config.GetDenylistedDomains(), IsEmpty());
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, NoIntent) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristics, {{"json_parameters", R"(
+        {
+          "heuristics":[
+            {
+              "conditionSet":{
+                "urlContains":"something"
+              }
+            }
+          ]
+        }
+        )"}}},
+       {features::kAutofillAssistantInTabTriggering, {}},
+       {features::kAutofillAssistantInCCTTriggering, {}}},
+      /* disabled_features = */ {});
+
+  LegacyStarterHeuristicConfig config;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+  EXPECT_THAT(config.GetDenylistedDomains(), IsEmpty());
+  EXPECT_THAT(config.GetIntent(), IsEmpty());
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, NoConditionSet) {
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristics, {{"json_parameters", R"(
+        {
+          "heuristics":[
+            {
+              "intent":"FAKE_INTENT_A",
+            }
+          ]
+        }
+        )"}}},
+       {features::kAutofillAssistantInTabTriggering, {}},
+       {features::kAutofillAssistantInCCTTriggering, {}}},
+      /* disabled_features = */ {});
+
+  LegacyStarterHeuristicConfig config;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+  EXPECT_THAT(config.GetDenylistedDomains(), IsEmpty());
+  EXPECT_THAT(config.GetIntent(), IsEmpty());
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, EmptyConditionSets) {
+  // Technically allowed, but pointless.
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeaturesAndParameters(
+      {{features::kAutofillAssistantUrlHeuristics, {{"json_parameters", R"(
+        {
+          "heuristics":[]
+        }
+        )"}}},
+       {features::kAutofillAssistantInTabTriggering, {}},
+       {features::kAutofillAssistantInCCTTriggering, {}}},
+      /* disabled_features = */ {});
+
+  LegacyStarterHeuristicConfig config;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+  EXPECT_THAT(config.GetDenylistedDomains(), IsEmpty());
+  EXPECT_THAT(config.GetIntent(), IsEmpty());
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, DisabledForSupervisedUsers) {
+  InitDefaultHeuristic();
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeatures(
+      /* enabled_features = */ {features::kAutofillAssistantInTabTriggering,
+                                features::kAutofillAssistantInCCTTriggering},
+      /* disabled_features = */ {});
+  LegacyStarterHeuristicConfig config;
+
+  fake_platform_delegate_.is_supervised_user_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+
+  fake_platform_delegate_.is_supervised_user_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+}
+
+TEST_F(LegacyStarterHeuristicConfigTest, DisabledIfProactiveHelpSettingOff) {
+  InitDefaultHeuristic();
+  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
+  scoped_feature_list->InitWithFeatures(
+      /* enabled_features = */ {features::kAutofillAssistantInTabTriggering,
+                                features::kAutofillAssistantInCCTTriggering},
+      /* disabled_features = */ {});
+  LegacyStarterHeuristicConfig config;
+
+  fake_platform_delegate_.proactive_help_enabled_ = false;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              IsEmpty());
+
+  fake_platform_delegate_.proactive_help_enabled_ = true;
+  EXPECT_THAT(config.GetConditionSetsForClientState(&fake_platform_delegate_),
+              SizeIs(1));
+}
+
+}  // namespace autofill_assistant
diff --git a/components/autofill_assistant/browser/starter_heuristic_configs/starter_heuristic_config.h b/components/autofill_assistant/browser/starter_heuristic_configs/starter_heuristic_config.h
new file mode 100644
index 0000000..1ce654d
--- /dev/null
+++ b/components/autofill_assistant/browser/starter_heuristic_configs/starter_heuristic_config.h
@@ -0,0 +1,43 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_STARTER_HEURISTIC_CONFIGS_STARTER_HEURISTIC_CONFIG_H_
+#define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_STARTER_HEURISTIC_CONFIGS_STARTER_HEURISTIC_CONFIG_H_
+
+#include <string>
+
+#include "base/containers/flat_set.h"
+#include "base/values.h"
+#include "components/autofill_assistant/browser/script_parameters.h"
+#include "components/autofill_assistant/browser/starter_platform_delegate.h"
+
+namespace autofill_assistant {
+
+// Base class for starter heuristic configs. Instances of this class define
+// heurists that are used by the starter to determine when to start. Configs
+// are usually either hard-coded into Chrome, or supplied via finch parameters.
+class StarterHeuristicConfig {
+ public:
+  StarterHeuristicConfig() = default;
+  virtual ~StarterHeuristicConfig() = default;
+  StarterHeuristicConfig(const StarterHeuristicConfig&) = delete;
+  StarterHeuristicConfig& operator=(const StarterHeuristicConfig&) = delete;
+
+  // Returns the intent script parameter for this starter heuristic.
+  virtual const std::string& GetIntent() const = 0;
+
+  // Returns a list containing the condition sets to use for the
+  // current client state (can be empty). Each conditionSet is a
+  // URLMatcherConditionSet dictionary as defined by the URLMatcherFactory
+  // (components/url_matcher/url_matcher_factory.h).
+  virtual const base::Value::List& GetConditionSetsForClientState(
+      StarterPlatformDelegate* platform_delegate) const = 0;
+
+  // Returns the list of denylisted domains for this config.
+  virtual const base::flat_set<std::string>& GetDenylistedDomains() const = 0;
+};
+
+}  // namespace autofill_assistant
+
+#endif  // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_STARTER_HEURISTIC_CONFIGS_STARTER_HEURISTIC_CONFIG_H_
diff --git a/components/autofill_assistant/browser/starter_heuristic_unittest.cc b/components/autofill_assistant/browser/starter_heuristic_unittest.cc
index 1421a42..2d6e0ea 100644
--- a/components/autofill_assistant/browser/starter_heuristic_unittest.cc
+++ b/components/autofill_assistant/browser/starter_heuristic_unittest.cc
@@ -11,7 +11,9 @@
 #include "base/test/mock_callback.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
+#include "components/autofill_assistant/browser/fake_starter_platform_delegate.h"
 #include "components/autofill_assistant/browser/features.h"
+#include "components/autofill_assistant/browser/starter_heuristic_configs/legacy_starter_heuristic_config.h"
 #include "testing/gmock/include/gmock/gmock.h"
 
 namespace autofill_assistant {
@@ -28,14 +30,37 @@
   base::flat_set<std::string> IsHeuristicMatchForTest(
       const StarterHeuristic& starter_heuristic,
       const GURL& url) {
-    return starter_heuristic.IsHeuristicMatch(url);
+    return starter_heuristic.IsHeuristicMatch(
+        url, starter_heuristic.matcher_id_to_config_map_);
   }
+
+  // Enables in-cct triggering with the specified parameters for
+  // |starter_heuristic|.
+  void InitDefaultHeuristic(StarterHeuristic& starter_heuristic,
+                            const std::string& json_parameters) {
+    scoped_feature_list_ = std::make_unique<base::test::ScopedFeatureList>();
+    scoped_feature_list_->InitWithFeaturesAndParameters(
+        {{features::kAutofillAssistantUrlHeuristics,
+          {{"json_parameters", json_parameters}}},
+         {features::kAutofillAssistantInCCTTriggering, {}}},
+        /* disabled_features = */ {});
+
+    std::vector<std::unique_ptr<StarterHeuristicConfig>> configs;
+    configs.emplace_back(std::make_unique<LegacyStarterHeuristicConfig>());
+    starter_heuristic.InitFromHeuristicConfigs(configs,
+                                               &fake_platform_delegate_);
+  }
+
+ protected:
+  FakeStarterPlatformDelegate fake_platform_delegate_;
+
+ private:
+  std::unique_ptr<base::test::ScopedFeatureList> scoped_feature_list_;
 };
 
 TEST_F(StarterHeuristicTest, SmokeTest) {
-  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
-  scoped_feature_list->InitAndEnableFeatureWithParameters(
-      features::kAutofillAssistantUrlHeuristics, {{"json_parameters", R"(
+  auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
+  InitDefaultHeuristic(*starter_heuristic, R"(
         {
           "heuristics":[
             {
@@ -46,9 +71,8 @@
             }
           ]
         }
-        )"}});
+        )");
 
-  auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
   EXPECT_THAT(IsHeuristicMatchForTest(*starter_heuristic,
                                       GURL("https://www.example.com/cart")),
               ElementsAre("FAKE_INTENT_CART"));
@@ -60,10 +84,8 @@
 }
 
 TEST_F(StarterHeuristicTest, RunHeuristicAsync) {
-  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
-  scoped_feature_list->InitAndEnableFeatureWithParameters(
-      features::kAutofillAssistantUrlHeuristics, {{"json_parameters",
-                                                   R"(
+  auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
+  InitDefaultHeuristic(*starter_heuristic, R"(
         {
           "heuristics":[
             {
@@ -74,63 +96,21 @@
             }
           ]
         }
-        )"}});
+        )");
 
   base::test::TaskEnvironment task_environment;
   base::MockCallback<
       base::OnceCallback<void(const base::flat_set<std::string>&)>>
       callback;
   EXPECT_CALL(callback, Run(base::flat_set<std::string>{"FAKE_INTENT_CART"}));
-  auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
   starter_heuristic->RunHeuristicAsync(GURL("https://www.example.com/cart"),
                                        callback.Get());
   task_environment.RunUntilIdle();
 }
 
-TEST_F(StarterHeuristicTest, MultipleIntentHeuristics) {
-  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
-  scoped_feature_list->InitAndEnableFeatureWithParameters(
-      features::kAutofillAssistantUrlHeuristics, {{"json_parameters",
-                                                   R"(
-        {
-          "heuristics":[
-            {
-              "intent":"FAKE_INTENT_CART",
-              "conditionSet":{
-                "urlContains":"cart"
-              }
-            },
-            {
-              "intent":"FAKE_INTENT_OTHER",
-              "conditionSet":{
-                "urlMatches":".*other.*"
-              }
-            }
-          ]
-        }
-        )"}});
-
-  auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
-  EXPECT_THAT(IsHeuristicMatchForTest(*starter_heuristic,
-                                      GURL("https://www.example.com/cart")),
-              ElementsAre("FAKE_INTENT_CART"));
-  EXPECT_THAT(IsHeuristicMatchForTest(*starter_heuristic,
-                                      GURL("https://www.example.com/other")),
-              ElementsAre("FAKE_INTENT_OTHER"));
-  EXPECT_THAT(IsHeuristicMatchForTest(*starter_heuristic,
-                                      GURL("https://www.example.com")),
-              IsEmpty());
-  EXPECT_THAT(
-      IsHeuristicMatchForTest(*starter_heuristic,
-                              GURL("https://www.example.com/cart/other")),
-      ElementsAre("FAKE_INTENT_CART", "FAKE_INTENT_OTHER"));
-}
-
 TEST_F(StarterHeuristicTest, DenylistedDomains) {
-  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
-  scoped_feature_list->InitAndEnableFeatureWithParameters(
-      features::kAutofillAssistantUrlHeuristics, {{"json_parameters",
-                                                   R"(
+  auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
+  InitDefaultHeuristic(*starter_heuristic, R"(
         {
           "denylistedDomains": ["example.com", "other-example.com"],
           "heuristics":[
@@ -142,11 +122,10 @@
             }
           ]
         }
-        )"}});
+        )");
 
   // URLs on denylisted domains or subdomains thereof will always fail the
   // heuristic even if they would otherwise match.
-  auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
   EXPECT_THAT(IsHeuristicMatchForTest(*starter_heuristic,
                                       GURL("https://www.example.com/cart")),
               IsEmpty());
@@ -172,10 +151,8 @@
 }
 
 TEST_F(StarterHeuristicTest, MultipleConditionSetsForSameIntent) {
-  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
-  scoped_feature_list->InitAndEnableFeatureWithParameters(
-      features::kAutofillAssistantUrlHeuristics, {{"json_parameters",
-                                                   R"(
+  auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
+  InitDefaultHeuristic(*starter_heuristic, R"(
         {
           "heuristics":[
             {
@@ -192,9 +169,8 @@
             }
           ]
         }
-        )"}});
+        )");
 
-  auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
   EXPECT_THAT(IsHeuristicMatchForTest(*starter_heuristic,
                                       GURL("https://example.com/cart")),
               ElementsAre("FAKE_INTENT_CART"));
@@ -206,56 +182,6 @@
               IsEmpty());
 }
 
-TEST_F(StarterHeuristicTest, MultipleConditionSetsForMultipleIntents) {
-  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
-  scoped_feature_list->InitAndEnableFeatureWithParameters(
-      features::kAutofillAssistantUrlHeuristics, {{"json_parameters",
-                                                   R"(
-        {
-          "heuristics":[
-            {
-              "intent":"FAKE_INTENT_A",
-              "conditionSet":{
-                "urlContains":"a_and_b"
-              }
-            },
-            {
-              "intent":"FAKE_INTENT_A",
-              "conditionSet":{
-                "urlContains":"only_a"
-              }
-            },
-            {
-              "intent":"FAKE_INTENT_B",
-              "conditionSet":{
-                "urlContains":"a_and_b"
-              }
-            },
-            {
-              "intent":"FAKE_INTENT_B",
-              "conditionSet":{
-                "urlContains":"only_b"
-              }
-            }
-          ]
-        }
-        )"}});
-
-  auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
-  EXPECT_THAT(IsHeuristicMatchForTest(*starter_heuristic,
-                                      GURL("https://example.com/a_and_b")),
-              ElementsAre("FAKE_INTENT_A", "FAKE_INTENT_B"));
-  EXPECT_THAT(IsHeuristicMatchForTest(*starter_heuristic,
-                                      GURL("https://example.com/only_a")),
-              ElementsAre("FAKE_INTENT_A"));
-  EXPECT_THAT(IsHeuristicMatchForTest(*starter_heuristic,
-                                      GURL("https://example.com/only_b")),
-              ElementsAre("FAKE_INTENT_B"));
-  EXPECT_THAT(IsHeuristicMatchForTest(*starter_heuristic,
-                                      GURL("https://www.example.com")),
-              IsEmpty());
-}
-
 TEST_F(StarterHeuristicTest, FieldTrialNotSet) {
   // Just a check that this does not crash.
   auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
@@ -266,11 +192,9 @@
 
 TEST_F(StarterHeuristicTest, FieldTrialInvalid) {
   // Just a check that this does not crash.
-  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
-  scoped_feature_list->InitAndEnableFeatureWithParameters(
-      features::kAutofillAssistantUrlHeuristics,
-      {{"json_parameters", "invalid"}});
   auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
+  InitDefaultHeuristic(*starter_heuristic, "invalid");
+
   EXPECT_THAT(IsHeuristicMatchForTest(*starter_heuristic,
                                       GURL("https://www.example.com/cart")),
               IsEmpty());
@@ -279,10 +203,8 @@
 TEST_F(StarterHeuristicTest, PartiallyInvalidFieldTrialsAreCompletelyIgnored) {
   // |denylistedDomains| expects an array of strings. If specified but invalid,
   // the entire configuration should be ignored.
-  auto scoped_feature_list = std::make_unique<base::test::ScopedFeatureList>();
-  scoped_feature_list->InitAndEnableFeatureWithParameters(
-      features::kAutofillAssistantUrlHeuristics, {{"json_parameters",
-                                                   R"(
+  auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
+  InitDefaultHeuristic(*starter_heuristic, R"(
         {
           "denylistedDomains": [-1],
           "heuristics":[
@@ -294,8 +216,8 @@
             }
           ]
         }
-        )"}});
-  auto starter_heuristic = base::MakeRefCounted<StarterHeuristic>();
+        )");
+
   EXPECT_THAT(IsHeuristicMatchForTest(*starter_heuristic,
                                       GURL("https://www.example.com/cart")),
               IsEmpty());
diff --git a/components/autofill_assistant/browser/starter_unittest.cc b/components/autofill_assistant/browser/starter_unittest.cc
index 2de8455..19a1097c 100644
--- a/components/autofill_assistant/browser/starter_unittest.cc
+++ b/components/autofill_assistant/browser/starter_unittest.cc
@@ -2013,7 +2013,7 @@
   task_environment()->RunUntilIdle();
 }
 
-TEST(MultipleIntentStarterTest, ImplicitTriggeringSendsAllMatchingIntents) {
+TEST(MultipleIntentStarterTest, ImplicitTriggeringSendsFirstLegacyIntent) {
   content::BrowserTaskEnvironment task_environment(
       base::test::TaskEnvironment::TimeSource::MOCK_TIME);
   content::RenderViewHostTestEnabler rvh_test_enabler;
@@ -2079,8 +2079,7 @@
         auto actual_intents =
             base::SplitString(actual_intent_param->value(), ",",
                               base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
-        EXPECT_THAT(actual_intents,
-                    UnorderedElementsAre("FAKE_INTENT_A", "FAKE_INTENT_B"));
+        EXPECT_THAT(actual_intents, UnorderedElementsAre("FAKE_INTENT_A"));
       }));
 
   content::WebContentsTester::For(web_contents.get())
diff --git a/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/RadioButtonWithEditTextTest.java b/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/RadioButtonWithEditTextTest.java
index fe2885d..d0bfc04 100644
--- a/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/RadioButtonWithEditTextTest.java
+++ b/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/RadioButtonWithEditTextTest.java
@@ -35,6 +35,7 @@
 import org.chromium.base.test.util.Batch;
 import org.chromium.base.test.util.Criteria;
 import org.chromium.base.test.util.CriteriaHelper;
+import org.chromium.base.test.util.DisabledTest;
 import org.chromium.components.browser_ui.widget.test.R;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
 import org.chromium.ui.KeyboardVisibilityDelegate;
@@ -236,6 +237,7 @@
 
     @Test
     @SmallTest
+    @DisabledTest(message = "Test is flaky: https://crbug.com/1344713")
     public void testFocusChange() {
         Assert.assertFalse(mRadioButtonWithEditText.hasFocus());
         TestThreadUtils.runOnUiThreadBlocking(() -> { mRadioButtonWithEditText.setChecked(true); });
diff --git a/components/content_settings/core/browser/cookie_settings.cc b/components/content_settings/core/browser/cookie_settings.cc
index 77862163..ccb9867 100644
--- a/components/content_settings/core/browser/cookie_settings.cc
+++ b/components/content_settings/core/browser/cookie_settings.cc
@@ -55,10 +55,11 @@
       ContentSettingsType::COOKIES, provider_id);
 }
 
-void CookieSettings::GetCookieSettings(
-    ContentSettingsForOneType* settings) const {
+ContentSettingsForOneType CookieSettings::GetCookieSettings() const {
+  ContentSettingsForOneType settings;
   host_content_settings_map_->GetSettingsForOneType(
-      ContentSettingsType::COOKIES, settings);
+      ContentSettingsType::COOKIES, &settings);
+  return settings;
 }
 
 void CookieSettings::RegisterProfilePrefs(
diff --git a/components/content_settings/core/browser/cookie_settings.h b/components/content_settings/core/browser/cookie_settings.h
index 1103a05..57bba8ae 100644
--- a/components/content_settings/core/browser/cookie_settings.h
+++ b/components/content_settings/core/browser/cookie_settings.h
@@ -80,11 +80,10 @@
   ContentSetting GetDefaultCookieSetting(std::string* provider_id) const;
 
   // Returns all patterns with a non-default cookie setting, mapped to their
-  // actual settings, in the precedence order of the setting rules. |settings|
-  // must be a non-nullptr outparam.
+  // actual settings, in the precedence order of the setting rules.
   //
   // This may be called on any thread.
-  void GetCookieSettings(ContentSettingsForOneType* settings) const;
+  ContentSettingsForOneType GetCookieSettings() const;
 
   // Sets the default content setting (CONTENT_SETTING_ALLOW,
   // CONTENT_SETTING_BLOCK, or CONTENT_SETTING_SESSION_ONLY) for cookies.
diff --git a/components/content_settings/core/browser/cookie_settings_unittest.cc b/components/content_settings/core/browser/cookie_settings_unittest.cc
index ee5f639..533ad6a6 100644
--- a/components/content_settings/core/browser/cookie_settings_unittest.cc
+++ b/components/content_settings/core/browser/cookie_settings_unittest.cc
@@ -115,10 +115,8 @@
 
  protected:
   bool ShouldDeleteCookieOnExit(const std::string& domain, bool is_https) {
-    ContentSettingsForOneType settings;
-    cookie_settings_->GetCookieSettings(&settings);
-    return cookie_settings_->ShouldDeleteCookieOnExit(settings, domain,
-                                                      is_https);
+    return cookie_settings_->ShouldDeleteCookieOnExit(
+        cookie_settings_->GetCookieSettings(), domain, is_https);
   }
 
   // There must be a valid ThreadTaskRunnerHandle in HostContentSettingsMap's
diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java b/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java
index 230059b..05d7736f 100644
--- a/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java
+++ b/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java
@@ -66,8 +66,8 @@
         if (mTestRule.testingJavaImpl()) {
             return;
         }
-        // Start HTTP2 Test Server
         System.loadLibrary("cronet_tests");
+        TestFilesInstaller.installIfNeeded(getContext());
         assertTrue(Http2TestServer.startHttp2TestServer(
                 getContext(), SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM));
         mServerHost = "test.example.com";
diff --git a/components/embedder_support/user_agent_utils_unittest.cc b/components/embedder_support/user_agent_utils_unittest.cc
index ff91aaa6..dcc5af1 100644
--- a/components/embedder_support/user_agent_utils_unittest.cc
+++ b/components/embedder_support/user_agent_utils_unittest.cc
@@ -643,7 +643,7 @@
     EXPECT_NE(base::StringPrintf(
                   kAndroid, version_info::GetMajorVersionNumber().c_str(), ""),
               GetUserAgent());
-    EXPECT_NE(content::GetUnifiedPlatform().c_str(),
+    EXPECT_NE(content::GetUnifiedPlatformForTesting().c_str(),
               GetUserAgentPlatformOsCpu(GetUserAgent()));
   }
 
@@ -675,11 +675,12 @@
        blink::features::kForceMajorVersionInMinorPositionInUserAgent},
       {});
   {
-    EXPECT_EQ(base::StringPrintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, "
-                                 "like Gecko) Chrome/%s.%s.0.0 Safari/537.36",
-                                 content::GetUnifiedPlatform().c_str(), "99",
-                                 version_info::GetMajorVersionNumber().c_str()),
-              GetUserAgent());
+    EXPECT_EQ(
+        base::StringPrintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, "
+                           "like Gecko) Chrome/%s.%s.0.0 Safari/537.36",
+                           content::GetUnifiedPlatformForTesting().c_str(),
+                           "99", version_info::GetMajorVersionNumber().c_str()),
+        GetUserAgent());
   }
 
   // Ensure that the ForceMajorVersionToMinorPosition policy is applied even
@@ -706,7 +707,7 @@
 // phase 5.
 #if BUILDFLAG(IS_ANDROID)
   EXPECT_NE(GetUserAgent(), GetReducedUserAgent());
-  EXPECT_NE(content::GetUnifiedPlatform().c_str(),
+  EXPECT_NE(content::GetUnifiedPlatformForTesting().c_str(),
             GetUserAgentPlatformOsCpu(GetUserAgent()));
 #else
   EXPECT_EQ(GetUserAgent(), GetReducedUserAgent());
diff --git a/components/exo/seat.cc b/components/exo/seat.cc
index 82f2ddc..4296e18b 100644
--- a/components/exo/seat.cc
+++ b/components/exo/seat.cc
@@ -39,36 +39,9 @@
 #if BUILDFLAG(IS_CHROMEOS_ASH)
 #include "ash/shell.h"
 #include "ui/aura/env.h"
-#include "ui/events/gestures/gesture_recognizer.h"
-#include "ui/wm/core/coordinate_conversion.h"
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
 namespace exo {
-namespace {
-
-gfx::PointF GetCursorScreenPoint(ui::mojom::DragEventSource event_source,
-                                 aura::Window* window) {
-#if BUILDFLAG(IS_CHROMEOS_ASH)
-  if (event_source == ui::mojom::DragEventSource::kTouch &&
-      aura::Env::GetInstance()->is_touch_down()) {
-    DCHECK(window);
-    DCHECK(window->GetRootWindow());
-    gfx::PointF touch_point_f;
-    bool got_touch_point =
-        aura::Env::GetInstance()
-            ->gesture_recognizer()
-            ->GetLastTouchPointForTarget(window, &touch_point_f);
-    if (got_touch_point) {
-      gfx::Point touch_point = gfx::ToFlooredPoint(touch_point_f);
-      wm::ConvertPointToScreen(window->GetRootWindow(), &touch_point);
-      return gfx::PointF(touch_point);
-    }
-  }
-#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
-  return gfx::PointF(display::Screen::GetScreen()->GetCursorScreenPoint());
-}
-
-}  // namespace
 
 Seat::Seat(std::unique_ptr<DataExchangeDelegate> delegate)
     : changing_clipboard_data_to_selection_source_(false),
@@ -155,12 +128,12 @@
                      Surface* origin,
                      Surface* icon,
                      ui::mojom::DragEventSource event_source) {
-  gfx::PointF cursor_location =
-      GetCursorScreenPoint(event_source, origin->window());
+  gfx::Point cursor_location = aura::Env::GetInstance()->GetLastPointerPoint(
+      event_source, origin->window(), /*fallback=*/absl::nullopt);
   // DragDropOperation manages its own lifetime.
-  drag_drop_operation_ =
-      DragDropOperation::Create(data_exchange_delegate_.get(), source, origin,
-                                icon, cursor_location, event_source);
+  drag_drop_operation_ = DragDropOperation::Create(
+      data_exchange_delegate_.get(), source, origin, icon,
+      gfx::PointF(cursor_location), event_source);
 }
 
 void Seat::AbortPendingDragOperation() {
diff --git a/components/history_clusters/core/query_clusters_state.cc b/components/history_clusters/core/query_clusters_state.cc
index cc4a3ed..3bc58d9 100644
--- a/components/history_clusters/core/query_clusters_state.cc
+++ b/components/history_clusters/core/query_clusters_state.cc
@@ -166,14 +166,19 @@
       return;
 
     const auto& raw_label_value = cluster.raw_label.value();
-
-    // Subtle code below: If we've NEVER encountered the label before, this []
-    // operator initializes the count to 0, and then post-increments it to 1.
-    // If it already exists, the post-increment will up the count, but will
-    // return false.
-    if (raw_label_counts_[raw_label_value]++ == 0) {
-      unique_raw_labels_.push_back(raw_label_value);
+    // Warning: N^2 algorithm below. If this ends up scaling poorly, it can be
+    // optimized by adding a map that tracks which labels have been seen
+    // already.
+    auto it = std::find_if(raw_label_counts_so_far_.begin(),
+                           raw_label_counts_so_far_.end(),
+                           [&raw_label_value](const LabelCount& label_count) {
+                             return label_count.first == raw_label_value;
+                           });
+    if (it == raw_label_counts_so_far_.end()) {
+      it = raw_label_counts_so_far_.insert(it,
+                                           std::make_pair(raw_label_value, 0));
     }
+    it->second++;
   }
 }
 
diff --git a/components/history_clusters/core/query_clusters_state.h b/components/history_clusters/core/query_clusters_state.h
index 7d5c5d6..511c512 100644
--- a/components/history_clusters/core/query_clusters_state.h
+++ b/components/history_clusters/core/query_clusters_state.h
@@ -24,6 +24,8 @@
 
 class HistoryClustersService;
 
+using LabelCount = std::pair<std::u16string, size_t>;
+
 // This object encapsulates the results of a query to HistoryClustersService.
 // It manages fetching more pages from the clustering backend as the user
 // scrolls down.
@@ -54,15 +56,12 @@
   // Used to request another batch of clusters of the same query.
   void LoadNextBatchOfClusters(ResultCallback callback);
 
-  // Use these together to iterate through the list of raw labels in the same
-  // order as the clusters are ordered. The counts can be fetched by inputting
-  // the labels into the map as keys - but note, this only counts the number
-  // of label instances seen SO FAR, not necessarily in all of History.
-  const std::vector<std::u16string>& unique_raw_labels() {
-    return unique_raw_labels_;
-  }
-  const std::map<std::u16string, size_t>& raw_label_counts() {
-    return raw_label_counts_;
+  // The list of raw labels in the same order as the clusters are ordered
+  // alongside the number of occurrences so far. The counts can be fetched by
+  // inputting the labels into the map as keys - but note, this only counts the
+  // number of label instances seen SO FAR, not necessarily in all of History.
+  const std::vector<LabelCount>& raw_label_counts_so_far() {
+    return raw_label_counts_so_far_;
   }
 
  private:
@@ -97,16 +96,11 @@
   // The string query the user entered into the searchbox.
   const std::string query_;
 
-  // The de-duplicated list of raw labels we've seen so far, in the same order
-  // as the clusters themselves were provided. This is only computed if `query`
-  // is empty. For non-empty `query`, this will be an empty list.
-  std::vector<std::u16string> unique_raw_labels_;
-  // Counts the number of instances of each raw label we've seen. Note that
-  // the value will always be an integer 1 or above. This is also used for our
-  // internal uniqueness test. Note: this only counts the number of raw labels
-  // of this string seen SO FAR, so unless we iterate through ALL the clusters,
-  // this number may be smaller than the total number in History.
-  std::map<std::u16string, size_t> raw_label_counts_;
+  // The de-duplicated list of raw labels we've seen so far and their number of
+  // occurrences, in the same order as the clusters themselves were provided.
+  // This is only computed if `query` is empty. For non-empty `query`, this will
+  // be an empty list.
+  std::vector<LabelCount> raw_label_counts_so_far_;
 
   // The continuation params used to track where the last query left off and
   // query for the "next page".
diff --git a/components/history_clusters/core/query_clusters_state_unittest.cc b/components/history_clusters/core/query_clusters_state_unittest.cc
index 0941a51c..3b5d1ce 100644
--- a/components/history_clusters/core/query_clusters_state_unittest.cc
+++ b/components/history_clusters/core/query_clusters_state_unittest.cc
@@ -285,20 +285,18 @@
   auto result = InjectRawClustersAndAwaitPostProcessing(
       &state, {cluster1, cluster2, cluster4}, {});
   ASSERT_EQ(result.cluster_batch.size(), 3U);
-  EXPECT_THAT(state.unique_raw_labels(),
-              ElementsAre(u"rawlabel1", u"rawlabel2"));
-  EXPECT_EQ(state.raw_label_counts().at(u"rawlabel1"), 2U);
-  EXPECT_EQ(state.raw_label_counts().at(u"rawlabel2"), 1U);
+  EXPECT_THAT(state.raw_label_counts_so_far(),
+              ElementsAre(std::make_pair(u"rawlabel1", 2),
+                          std::make_pair(u"rawlabel2", 1)));
 
   // Test updating an existing count, and adding new ones after that.
   result =
       InjectRawClustersAndAwaitPostProcessing(&state, {cluster5, cluster3}, {});
   ASSERT_EQ(result.cluster_batch.size(), 2U);
-  EXPECT_THAT(state.unique_raw_labels(),
-              ElementsAre(u"rawlabel1", u"rawlabel2", u"rawlabel3"));
-  EXPECT_EQ(state.raw_label_counts().at(u"rawlabel1"), 2U);
-  EXPECT_EQ(state.raw_label_counts().at(u"rawlabel2"), 2U);
-  EXPECT_EQ(state.raw_label_counts().at(u"rawlabel3"), 1U);
+  EXPECT_THAT(state.raw_label_counts_so_far(),
+              ElementsAre(std::make_pair(u"rawlabel1", 2),
+                          std::make_pair(u"rawlabel2", 2),
+                          std::make_pair(u"rawlabel3", 1)));
 }
 
 }  // namespace history_clusters
diff --git a/components/history_clusters_strings.grdp b/components/history_clusters_strings.grdp
index 30670a4..fe94d0a 100644
--- a/components/history_clusters_strings.grdp
+++ b/components/history_clusters_strings.grdp
@@ -70,4 +70,8 @@
   <message name="IDS_HISTORY_CLUSTERS_SEARCH_YOUR_JOURNEYS" desc="A label for the hint text for the search box in Chrome Journeys." formatter_data="android_java">
     Search your Journeys
   </message>
+  <message name="IDS_HISTORY_CLUSTERS_N_MATCHES" desc="A label for the number of matching clusters for a given label." formatter_data="android_java">
+   {NUM_MATCHES, plural,
+        =1 {# match} other {# matches}}
+  </message>
 </grit-part>
diff --git a/components/history_clusters_strings_grdp/IDS_HISTORY_CLUSTERS_N_MATCHES.png.sha1 b/components/history_clusters_strings_grdp/IDS_HISTORY_CLUSTERS_N_MATCHES.png.sha1
new file mode 100644
index 0000000..160e9ad
--- /dev/null
+++ b/components/history_clusters_strings_grdp/IDS_HISTORY_CLUSTERS_N_MATCHES.png.sha1
@@ -0,0 +1 @@
+e5b8707aa16ed376ba83f2258f78b489b928d069
\ No newline at end of file
diff --git a/components/metrics/metrics_service.cc b/components/metrics/metrics_service.cc
index 5963fd95..fda6c7e2 100644
--- a/components/metrics/metrics_service.cc
+++ b/components/metrics/metrics_service.cc
@@ -937,11 +937,6 @@
   state_ = SENDING_LOGS;
 }
 
-void MetricsService::IncrementLongPrefsValue(const char* path) {
-  int64_t value = local_state_->GetInt64(path);
-  local_state_->SetInt64(path, value + 1);
-}
-
 void MetricsService::RegisterMetricsProvider(
     std::unique_ptr<MetricsProvider> provider) {
   DCHECK_EQ(CONSTRUCTED, state_);
diff --git a/components/metrics/metrics_service.h b/components/metrics/metrics_service.h
index 0963e20..1a6b61d 100644
--- a/components/metrics/metrics_service.h
+++ b/components/metrics/metrics_service.h
@@ -367,10 +367,6 @@
   // profiler data, as well as incremental stability-related metrics.
   void PrepareInitialMetricsLog();
 
-  // Reads, increments and then sets the specified long preference that is
-  // stored as a string.
-  void IncrementLongPrefsValue(const char* path);
-
   // Creates a new MetricsLog instance with the given |log_type|.
   std::unique_ptr<MetricsLog> CreateLog(MetricsLog::LogType log_type);
 
diff --git a/components/minidump_uploader/android/java/src/org/chromium/components/minidump_uploader/CrashFileManager.java b/components/minidump_uploader/android/java/src/org/chromium/components/minidump_uploader/CrashFileManager.java
index 2062076..eeace51 100644
--- a/components/minidump_uploader/android/java/src/org/chromium/components/minidump_uploader/CrashFileManager.java
+++ b/components/minidump_uploader/android/java/src/org/chromium/components/minidump_uploader/CrashFileManager.java
@@ -214,6 +214,13 @@
     }
 
     /**
+     * @return True iff the provided File was ready be uploaded for the first time.
+     */
+    public static boolean isReadyUploadForFirstTime(File fileToUpload) {
+        return fileToUpload.getName().contains(READY_FOR_UPLOAD_SUFFIX);
+    }
+
+    /**
      * Attempts to rename the given file to mark it as a forced upload. This is useful for allowing
      * users to manually initiate previously skipped uploads.
      *
diff --git a/components/minidump_uploader/android/javatests/src/org/chromium/components/minidump_uploader/CrashFileManagerTest.java b/components/minidump_uploader/android/javatests/src/org/chromium/components/minidump_uploader/CrashFileManagerTest.java
index 10b06056..34d2b13f 100644
--- a/components/minidump_uploader/android/javatests/src/org/chromium/components/minidump_uploader/CrashFileManagerTest.java
+++ b/components/minidump_uploader/android/javatests/src/org/chromium/components/minidump_uploader/CrashFileManagerTest.java
@@ -162,6 +162,18 @@
     @Test
     @SmallTest
     @Feature({"Android-AppBase"})
+    public void testReadyForUploadForFirstTime() {
+        Assert.assertTrue(CrashFileManager.isReadyUploadForFirstTime(
+                new File(mTestRule.getCrashDir(), "foo.try0")));
+        Assert.assertFalse(CrashFileManager.isReadyUploadForFirstTime(
+                new File(mTestRule.getCrashDir(), "foo.try1")));
+        Assert.assertFalse(CrashFileManager.isReadyUploadForFirstTime(
+                new File(mTestRule.getCrashDir(), "foo")));
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"Android-AppBase"})
     public void testCrashFileManagerWithNull() {
         try {
             new CrashFileManager(null);
diff --git a/components/net_log/net_export_file_writer_unittest.cc b/components/net_log/net_export_file_writer_unittest.cc
index 23244ff..15817d36e 100644
--- a/components/net_log/net_export_file_writer_unittest.cc
+++ b/components/net_log/net_export_file_writer_unittest.cc
@@ -804,13 +804,12 @@
   ASSERT_GE(events->GetListDeprecated().size(), 1u);
 
   // Check the URL in the params of the first event.
-  base::DictionaryValue* event;
-  EXPECT_TRUE(events->GetDictionary(0, &event));
-  base::DictionaryValue* event_params;
-  EXPECT_TRUE(event->GetDictionary("params", &event_params));
-  std::string event_url;
-  EXPECT_TRUE(event_params->GetString("url", &event_url));
-  EXPECT_EQ(test_server.GetURL(kRedirectURL), event_url);
+  base::Value::Dict* event = events->GetList()[0].GetIfDict();
+  EXPECT_TRUE(event);
+  base::Value::Dict* event_params = event->FindDict("params");
+  EXPECT_TRUE(event_params);
+  EXPECT_EQ(test_server.GetURL(kRedirectURL),
+            *(event_params->FindString("url")));
 
   block_fetch.Signal();
   run_loop2.Run();
diff --git a/components/omnibox/browser/autocomplete_controller.cc b/components/omnibox/browser/autocomplete_controller.cc
index 7ad7ce3f..f6a85f4 100644
--- a/components/omnibox/browser/autocomplete_controller.cc
+++ b/components/omnibox/browser/autocomplete_controller.cc
@@ -296,6 +296,47 @@
       search_service_worker_signal_sent_(false),
       template_url_service_(provider_client_->GetTemplateURLService()) {
   provider_types &= ~OmniboxFieldTrial::GetDisabledProviderTypes();
+
+  if (OmniboxFieldTrial::kAutocompleteStabilityAsyncProvidersFirst.Get()) {
+    // Providers run in the order they're added. Run these async providers 1st
+    // so their async requests can be kicked off before waiting a few
+    // milliseconds for the other sync providers to complete.
+
+    if (provider_types & AutocompleteProvider::TYPE_SEARCH) {
+      search_provider_ = new SearchProvider(provider_client_.get(), this);
+      providers_.push_back(search_provider_.get());
+    }
+    // Providers run in the order they're added.  Add `HistoryURLProvider` after
+    // `SearchProvider` because:
+    // - `SearchProvider` synchronously queries the history database's
+    //   keyword_search_terms and url table.
+    // - `HistoryUrlProvider` schedules a background task that also accesses the
+    //    history database.
+    // If both db accesses happen concurrently, TSan complains. So put
+    // `HistoryURLProvider` later to make sure that `SearchProvider` is done
+    // doing its thing by the time the `HistoryURLProvider` task runs. (And hope
+    // that it completes before `AutocompleteController::Start()` is called the
+    // next time.)
+    if (provider_types & AutocompleteProvider::TYPE_HISTORY_URL) {
+      history_url_provider_ =
+          new HistoryURLProvider(provider_client_.get(), this);
+      if (provider_types & AutocompleteProvider::TYPE_HISTORY_URL)
+        providers_.push_back(history_url_provider_.get());
+    }
+    if (provider_types & AutocompleteProvider::TYPE_DOCUMENT) {
+      document_provider_ =
+          DocumentProvider::Create(provider_client_.get(), this);
+      providers_.push_back(document_provider_.get());
+    }
+    if (provider_types & AutocompleteProvider::TYPE_ON_DEVICE_HEAD) {
+      on_device_head_provider_ =
+          OnDeviceHeadProvider::Create(provider_client_.get(), this);
+      if (on_device_head_provider_) {
+        providers_.push_back(on_device_head_provider_.get());
+      }
+    }
+  }
+
   if (provider_types & AutocompleteProvider::TYPE_BOOKMARK) {
     bookmark_provider_ = new BookmarkProvider(provider_client_.get());
     providers_.push_back(bookmark_provider_.get());
@@ -310,27 +351,28 @@
     keyword_provider_ = new KeywordProvider(provider_client_.get(), this);
     providers_.push_back(keyword_provider_.get());
   }
-  if (provider_types & AutocompleteProvider::TYPE_SEARCH) {
-    search_provider_ = new SearchProvider(provider_client_.get(), this);
-    providers_.push_back(search_provider_.get());
-  }
-  // It's important that the HistoryURLProvider gets added after SearchProvider:
-  // AutocompleteController::Start() calls each providers' Start() function
-  // synchronously in the order they're in providers_.
-  // - SearchProvider::Start() synchronously queries the history database's
-  //   keyword_search_terms and url table.
-  // - HistoryUrlProvider::Start schedules a background task that also accesses
-  //   the history database.
-  // If both db accesses happen concurrently, TSan complains.
-  // So put HistoryURLProvider later to make sure that SearchProvider is done
-  // doing its thing by the time the HistoryURLProvider task runs.
-  // (And hope that it completes before AutocompleteController::Start() is
-  // called the next time.)
-  if (provider_types & AutocompleteProvider::TYPE_HISTORY_URL) {
-    history_url_provider_ =
-        new HistoryURLProvider(provider_client_.get(), this);
-    if (provider_types & AutocompleteProvider::TYPE_HISTORY_URL)
-      providers_.push_back(history_url_provider_.get());
+  if (!OmniboxFieldTrial::kAutocompleteStabilityAsyncProvidersFirst.Get()) {
+    if (provider_types & AutocompleteProvider::TYPE_SEARCH) {
+      search_provider_ = new SearchProvider(provider_client_.get(), this);
+      providers_.push_back(search_provider_.get());
+    }
+    // Providers run in the order they're added.  Add `HistoryURLProvider` after
+    // `SearchProvider` because:
+    // - `SearchProvider` synchronously queries the history database's
+    //   keyword_search_terms and url table.
+    // - `HistoryUrlProvider` schedules a background task that also accesses the
+    //    history database.
+    // If both db accesses happen concurrently, TSan complains. So put
+    // `HistoryURLProvider` later to make sure that `SearchProvider` is done
+    // doing its thing by the time the `HistoryURLProvider` task runs. (And hope
+    // that it completes before `AutocompleteController::Start()` is called the
+    // next time.)
+    if (provider_types & AutocompleteProvider::TYPE_HISTORY_URL) {
+      history_url_provider_ =
+          new HistoryURLProvider(provider_client_.get(), this);
+      if (provider_types & AutocompleteProvider::TYPE_HISTORY_URL)
+        providers_.push_back(history_url_provider_.get());
+    }
   }
   if (provider_types & AutocompleteProvider::TYPE_SHORTCUTS)
     providers_.push_back(new ShortcutsProvider(provider_client_.get()));
@@ -353,15 +395,18 @@
     providers_.push_back(
         new ZeroSuggestVerbatimMatchProvider(provider_client_.get()));
   }
-  if (provider_types & AutocompleteProvider::TYPE_DOCUMENT) {
-    document_provider_ = DocumentProvider::Create(provider_client_.get(), this);
-    providers_.push_back(document_provider_.get());
-  }
-  if (provider_types & AutocompleteProvider::TYPE_ON_DEVICE_HEAD) {
-    on_device_head_provider_ =
-        OnDeviceHeadProvider::Create(provider_client_.get(), this);
-    if (on_device_head_provider_) {
-      providers_.push_back(on_device_head_provider_.get());
+  if (!OmniboxFieldTrial::kAutocompleteStabilityAsyncProvidersFirst.Get()) {
+    if (provider_types & AutocompleteProvider::TYPE_DOCUMENT) {
+      document_provider_ =
+          DocumentProvider::Create(provider_client_.get(), this);
+      providers_.push_back(document_provider_.get());
+    }
+    if (provider_types & AutocompleteProvider::TYPE_ON_DEVICE_HEAD) {
+      on_device_head_provider_ =
+          OnDeviceHeadProvider::Create(provider_client_.get(), this);
+      if (on_device_head_provider_) {
+        providers_.push_back(on_device_head_provider_.get());
+      }
     }
   }
   if (provider_types & AutocompleteProvider::TYPE_CLIPBOARD) {
@@ -396,7 +441,8 @@
         provider_client_.get(), history_quick_provider_, bookmark_provider_));
   }
   if (provider_types & AutocompleteProvider::TYPE_OPEN_TAB) {
-    providers_.push_back(new OpenTabProvider(provider_client_.get()));
+    open_tab_provider_ = new OpenTabProvider(provider_client_.get());
+    providers_.push_back(open_tab_provider_.get());
   }
   // Ideally, we'd check `IsApplicationLocaleSupportedByJourneys()` when
   // constructing `provider_types`. But that's usually constructed in
@@ -870,8 +916,7 @@
   if (!done_) {
     // This conditional needs to match the conditional in Start that invokes
     // StartExpireTimer.
-    result_.TransferOldMatches(input_, &old_matches_to_reuse,
-                               template_url_service_);
+    result_.TransferOldMatches(input_, &old_matches_to_reuse);
     if (OmniboxFieldTrial::kAutocompleteStabilityPreserveDefaultAfterTransfer
             .Get()) {
       result_.SortAndCull(input_, template_url_service_,
diff --git a/components/omnibox/browser/autocomplete_controller.h b/components/omnibox/browser/autocomplete_controller.h
index e6290e1b..2f685a0 100644
--- a/components/omnibox/browser/autocomplete_controller.h
+++ b/components/omnibox/browser/autocomplete_controller.h
@@ -27,6 +27,7 @@
 #include "components/omnibox/browser/autocomplete_result.h"
 #include "components/omnibox/browser/bookmark_provider.h"
 #include "components/omnibox/browser/omnibox_log.h"
+#include "components/omnibox/browser/open_tab_provider.h"
 
 class ClipboardProvider;
 class DocumentProvider;
@@ -196,6 +197,7 @@
   VoiceSuggestProvider* voice_suggest_provider() const {
     return voice_suggest_provider_;
   }
+  OpenTabProvider* open_tab_provider() const { return open_tab_provider_; }
 
   const AutocompleteInput& input() const { return input_; }
   const AutocompleteResult& result() const { return result_; }
@@ -353,6 +355,8 @@
 
   raw_ptr<VoiceSuggestProvider> voice_suggest_provider_;
 
+  raw_ptr<OpenTabProvider> open_tab_provider_;
+
   // Input passed to Start.
   AutocompleteInput input_;
 
diff --git a/components/omnibox/browser/autocomplete_match.h b/components/omnibox/browser/autocomplete_match.h
index 3e5c26a..2cca96b 100644
--- a/components/omnibox/browser/autocomplete_match.h
+++ b/components/omnibox/browser/autocomplete_match.h
@@ -41,6 +41,8 @@
 class TemplateURL;
 class TemplateURLService;
 
+enum class SuggestionGroupId;
+
 namespace base {
 class Time;
 }  // namespace base
@@ -677,9 +679,9 @@
   // The optional suggestion group Id. Used to look up the suggestion group info
   // such as the header text this match must appear under from ACResult.
   //
-  // If this value exists, it should always be positive and nonzero. In Java and
-  // JavaScript, -1 is used as a sentinel value, but should never occur in C++.
-  absl::optional<int> suggestion_group_id;
+  // This is converted to a primitive int type in Java and JavaScript; with -1
+  // (SuggestionGroupId::kInvalid) used as a sentinel value.
+  absl::optional<SuggestionGroupId> suggestion_group_id;
 
   // If true, UI-level code should swap the contents and description fields
   // before displaying.
diff --git a/components/omnibox/browser/autocomplete_match_android.cc b/components/omnibox/browser/autocomplete_match_android.cc
index be46479..81f8112 100644
--- a/components/omnibox/browser/autocomplete_match_android.cc
+++ b/components/omnibox/browser/autocomplete_match_android.cc
@@ -113,7 +113,8 @@
           url::GURLAndroid::FromNativeGURL(env, image_url),
           j_image_dominant_color, SupportsDeletion(), j_post_content_type,
           j_post_content,
-          suggestion_group_id.value_or(kInvalidSuggestionGroupId),
+          static_cast<int>(
+              suggestion_group_id.value_or(SuggestionGroupId::kInvalid)),
           j_query_tiles, ToJavaByteArray(env, clipboard_image_data),
           has_tab_match.value_or(false),
           ToJavaArrayOfStrings(env, suggest_titles),
diff --git a/components/omnibox/browser/autocomplete_provider_unittest.cc b/components/omnibox/browser/autocomplete_provider_unittest.cc
index 0a0cdcc..c4881c57 100644
--- a/components/omnibox/browser/autocomplete_provider_unittest.cc
+++ b/components/omnibox/browser/autocomplete_provider_unittest.cc
@@ -326,7 +326,7 @@
 
   struct SuggestionGroupsTestData {
     SuggestionGroupsMap suggestion_groups_map;
-    std::vector<absl::optional<int>> suggestion_group_ids;
+    std::vector<absl::optional<SuggestionGroupId>> suggestion_group_ids;
   };
 
   struct AssistedQueryStatsTestData {
@@ -608,7 +608,9 @@
   for (auto suggestion_group_id : test_data.suggestion_group_ids) {
     AutocompleteMatch match(nullptr, relevance--, false,
                             AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED);
-    match.suggestion_group_id = suggestion_group_id;
+    if (suggestion_group_id.has_value()) {
+      match.suggestion_group_id = suggestion_group_id.value();
+    }
     matches.push_back(match);
   }
   result_.Reset();
@@ -853,24 +855,34 @@
 
 // Tests that the AutocompleteResult is updated with the suggestion group
 // information and matches with group IDs are grouped and demoted correctly.
-// Also verifies that 1) suggestion group IDs without associated suggestion
-// group information are stripped away. and 2) headers are optional for groups.
-TEST_F(AutocompleteProviderTest, Headers) {
+// Also verifies that:
+// 1) headers are optional for suggestion groups.
+// 2) suggestion groups are ordered based on their priories.
+// 3) suggestion group IDs without associated suggestion group information are
+//    stripped away.
+TEST_F(AutocompleteProviderTest, SuggestionGroups) {
   ResetControllerWithKeywordAndSearchProviders();
 
-  const int kRecommendedGroupId = 1;
+  const auto kRecommendedGroupId =
+      SuggestionGroupId::kNonPersonalizedZeroSuggest1;
   const std::u16string kRecommended = u"Recommended for you";
-  const int kRecentSearchesGroupId = 2;
+  const auto kRecentSearchesGroupId =
+      SuggestionGroupId::kNonPersonalizedZeroSuggest2;
   const std::u16string kRecentSearches = u"Recent Searches";
 
   // This exists to verify that suggestion group IDs without associated
   // suggestion groups information are stripped away.
-  const int kBadSuggestionGroupId = 99;
+  const auto kBadSuggestionGroupId = SuggestionGroupId::kInvalid;
 
   {
+    // Headers are optional for suggestion groups.
     SuggestionGroupsMap suggestion_groups_map;
     suggestion_groups_map[kRecommendedGroupId].header = kRecommended;
+    suggestion_groups_map[kRecommendedGroupId].priority =
+        SuggestionGroupPriority::kRemoteZeroSuggest4;
     suggestion_groups_map[kRecentSearchesGroupId].header = u"";
+    suggestion_groups_map[kRecentSearchesGroupId].priority =
+        SuggestionGroupPriority::kRemoteZeroSuggest3;
     UpdateResultsWithSuggestionGroupsTestData({std::move(suggestion_groups_map),
                                                {
                                                    {},
@@ -896,9 +908,14 @@
               result_.GetHeaderForSuggestionGroup(kRecommendedGroupId));
   }
   {
+    // Suggestion groups are ordered based on their priories.
     SuggestionGroupsMap suggestion_groups_map;
     suggestion_groups_map[kRecommendedGroupId].header = kRecommended;
+    suggestion_groups_map[kRecommendedGroupId].priority =
+        SuggestionGroupPriority::kRemoteZeroSuggest3;
     suggestion_groups_map[kRecentSearchesGroupId].header = kRecentSearches;
+    suggestion_groups_map[kRecentSearchesGroupId].priority =
+        SuggestionGroupPriority::kRemoteZeroSuggest4;
     UpdateResultsWithSuggestionGroupsTestData({std::move(suggestion_groups_map),
                                                {
                                                    {},
@@ -912,22 +929,24 @@
 
     EXPECT_FALSE(result_.match_at(1)->suggestion_group_id.has_value());
 
-    EXPECT_EQ(kRecentSearchesGroupId,
+    EXPECT_EQ(kRecommendedGroupId,
               result_.match_at(2)->suggestion_group_id.value());
-    EXPECT_EQ(kRecentSearches,
-              result_.GetHeaderForSuggestionGroup(kRecentSearchesGroupId));
+    EXPECT_EQ(kRecommended,
+              result_.GetHeaderForSuggestionGroup(kRecommendedGroupId));
 
     EXPECT_EQ(kRecentSearchesGroupId,
               result_.match_at(3)->suggestion_group_id.value());
     EXPECT_EQ(kRecentSearches,
               result_.GetHeaderForSuggestionGroup(kRecentSearchesGroupId));
 
-    EXPECT_EQ(kRecommendedGroupId,
+    EXPECT_EQ(kRecentSearchesGroupId,
               result_.match_at(4)->suggestion_group_id.value());
-    EXPECT_EQ(kRecommended,
-              result_.GetHeaderForSuggestionGroup(kRecommendedGroupId));
+    EXPECT_EQ(kRecentSearches,
+              result_.GetHeaderForSuggestionGroup(kRecentSearchesGroupId));
   }
   {
+    // suggestion group IDs without associated suggestion group information are
+    // stripped away.
     SuggestionGroupsMap suggestion_groups_map;
     suggestion_groups_map[kRecommendedGroupId].header = kRecommended;
     suggestion_groups_map[kRecentSearchesGroupId].header = kRecentSearches;
diff --git a/components/omnibox/browser/autocomplete_result.cc b/components/omnibox/browser/autocomplete_result.cc
index b157ce0..67ff2127 100644
--- a/components/omnibox/browser/autocomplete_result.cc
+++ b/components/omnibox/browser/autocomplete_result.cc
@@ -158,10 +158,8 @@
 
 AutocompleteResult::~AutocompleteResult() = default;
 
-void AutocompleteResult::TransferOldMatches(
-    const AutocompleteInput& input,
-    AutocompleteResult* old_matches,
-    TemplateURLService* template_url_service) {
+void AutocompleteResult::TransferOldMatches(const AutocompleteInput& input,
+                                            AutocompleteResult* old_matches) {
   // Don't transfer matches from done providers. If the match is still
   // relevant, it'll already be in `result_`, potentially with updated fields
   // that shouldn't be deduped with the out-of-date match. Otherwise, the
@@ -331,19 +329,7 @@
   const size_t num_matches =
       CalculateNumMatches(is_zero_suggest, matches_, comparing_object);
 
-  if (base::FeatureList::IsEnabled(omnibox::kRetainSuggestionsWithHeaders)) {
-    size_t num_regular_suggestions = 0;
-    base::EraseIf(matches_,
-                  [&num_regular_suggestions, num_matches](const auto& match) {
-                    // Trim suggestions without group IDs to the given limit.
-                    if (!match.suggestion_group_id.has_value()) {
-                      num_regular_suggestions++;
-                      return num_regular_suggestions > num_matches;
-                    }
-                    // Do not trim suggestions with group IDs.
-                    return false;
-                  });
-  } else {
+  if (!is_zero_suggest) {
     matches_.resize(num_matches);
   }
 
@@ -359,6 +345,24 @@
 
   GroupAndDemoteMatchesInGroups();
 
+  if (is_zero_suggest) {
+    if (base::FeatureList::IsEnabled(omnibox::kRetainSuggestionsWithHeaders)) {
+      size_t num_regular_suggestions = 0;
+      base::EraseIf(matches_,
+                    [&num_regular_suggestions, num_matches](const auto& match) {
+                      // Trim suggestions without group IDs to the given limit.
+                      if (!match.suggestion_group_id.has_value()) {
+                        num_regular_suggestions++;
+                        return num_regular_suggestions > num_matches;
+                      }
+                      // Do not trim suggestions with group IDs.
+                      return false;
+                    });
+    } else {
+      matches_.resize(num_matches);
+    }
+  }
+
   // Run sanity checks on the default match to make sure all the suggestions
   // are congruent with the user's input. Skip checks in these cases:
   //  - If the default match has no |destination_url|. An example of this is the
@@ -395,52 +399,50 @@
 }
 
 void AutocompleteResult::GroupAndDemoteMatchesInGroups() {
-  // Create a map from suggestion group ID to the index it first appears.
-  // Reserve the first spot for matches without group IDs.
-  std::map<int, int> group_id_index_map = {{kInvalidSuggestionGroupId, 0}};
-  for (auto it = matches_.begin(); it != matches_.end(); ++it) {
-    if (it->suggestion_group_id.has_value()) {
-      const int group_id = it->suggestion_group_id.value();
-
-      if (suggestion_groups_map_.find(group_id) !=
-          suggestion_groups_map_.end()) {
-        // Record suggestion group information into the additional_info field
-        // for chrome://omnibox.
-        it->RecordAdditionalInfo("group id", group_id);
-        it->RecordAdditionalInfo("group header",
-                                 GetHeaderForSuggestionGroup(group_id));
-      } else {
-        // Strip group IDs from the matches for which there is no suggestion
-        // group information. These matches should instead be treated as
-        // ordinary matches with no group IDs.
-        it->suggestion_group_id.reset();
-      }
+  bool any_matches_in_groups = false;
+  for (auto& match : *this) {
+    if (!match.suggestion_group_id.has_value()) {
+      continue;
     }
+    any_matches_in_groups = true;
 
-    int group_id = it->suggestion_group_id.value_or(kInvalidSuggestionGroupId);
-    // Use the 1-based index of the match to record the first appearance of its
-    // group ID since 0 is reserved for matches without group IDs. We are
-    // interested in the relative values of these indices only and their
-    // absolute values hardly matter.
-    int index = std::distance(matches_.begin(), it) + 1;
-    // map::insert doesn't insert the value if the map already contains the key.
-    group_id_index_map.insert(std::pair<int, int>(group_id, index));
+    const SuggestionGroupId group_id = match.suggestion_group_id.value();
+    if (suggestion_groups_map_.find(group_id) != suggestion_groups_map_.end()) {
+      // Record suggestion group information into the additional_info field
+      // for chrome://omnibox.
+      match.RecordAdditionalInfo("group id", static_cast<int>(group_id));
+      match.RecordAdditionalInfo("group header",
+                                 GetHeaderForSuggestionGroup(group_id));
+      match.RecordAdditionalInfo(
+          "group priority",
+          static_cast<int>(GetPriorityForSuggestionGroup(group_id)));
+    } else {
+      // Strip group IDs from the matches for which there is no suggestion
+      // group information. These matches should instead be treated as
+      // ordinary matches with no group IDs.
+      match.suggestion_group_id.reset();
+    }
   }
 
-  // No need to group and demote matches with group IDs if none exists.
-  if (group_id_index_map.size() == 1)
+  // No need to group and demote matches in groups if none exists.
+  if (!any_matches_in_groups) {
     return;
+  }
 
-  // Sort the matches based on the order in which their group IDs first appear
-  // while preserving the existing order of matches with the same group ID.
+  // Sort the matches based on the order in which their groups should appear
+  // while preserving the existing order of matches within the same group.
   std::stable_sort(
-      matches_.begin(), matches_.end(),
-      [&group_id_index_map](const auto& a, const auto& b) {
-        const int a_group_id =
-            a.suggestion_group_id.value_or(kInvalidSuggestionGroupId);
-        const int b_group_id =
-            b.suggestion_group_id.value_or(kInvalidSuggestionGroupId);
-        return group_id_index_map[a_group_id] < group_id_index_map[b_group_id];
+      matches_.begin(), matches_.end(), [this](const auto& a, const auto& b) {
+        // Note that matches not in a group must appear before the matches in
+        // one; thus the order of the following two early checks is important.
+        if (!b.suggestion_group_id.has_value()) {
+          return false;
+        }
+        if (!a.suggestion_group_id.has_value()) {
+          return true;
+        }
+        return GetPriorityForSuggestionGroup(a.suggestion_group_id.value()) <
+               GetPriorityForSuggestionGroup(b.suggestion_group_id.value());
       });
 }
 
@@ -956,19 +958,18 @@
 }
 
 std::u16string AutocompleteResult::GetHeaderForSuggestionGroup(
-    int suggestion_group_id) const {
+    SuggestionGroupId suggestion_group_id) const {
   const auto& it = suggestion_groups_map_.find(suggestion_group_id);
-  if (it != suggestion_groups_map_.end())
-    return it->second.header;
-  return std::u16string();
+  DCHECK(it != suggestion_groups_map_.end());
+  return it->second.header;
 }
 
 bool AutocompleteResult::IsSuggestionGroupHidden(
     PrefService* prefs,
-    int suggestion_group_id) const {
+    SuggestionGroupId suggestion_group_id) const {
   omnibox::SuggestionGroupVisibility user_preference =
       omnibox::GetUserPreferenceForSuggestionGroupVisibility(
-          prefs, suggestion_group_id);
+          prefs, static_cast<int>(suggestion_group_id));
 
   if (user_preference == omnibox::SuggestionGroupVisibility::HIDDEN)
     return true;
@@ -977,9 +978,15 @@
 
   DCHECK_EQ(user_preference, omnibox::SuggestionGroupVisibility::DEFAULT);
   const auto& it = suggestion_groups_map_.find(suggestion_group_id);
-  if (it != suggestion_groups_map_.end())
-    return it->second.hidden;
-  return false;
+  DCHECK(it != suggestion_groups_map_.end());
+  return it->second.hidden;
+}
+
+SuggestionGroupPriority AutocompleteResult::GetPriorityForSuggestionGroup(
+    SuggestionGroupId suggestion_group_id) const {
+  const auto& it = suggestion_groups_map_.find(suggestion_group_id);
+  DCHECK(it != suggestion_groups_map_.end());
+  return it->second.priority;
 }
 
 void AutocompleteResult::MergeSuggestionGroupsMap(
diff --git a/components/omnibox/browser/autocomplete_result.h b/components/omnibox/browser/autocomplete_result.h
index 60391fc..f71907e 100644
--- a/components/omnibox/browser/autocomplete_result.h
+++ b/components/omnibox/browser/autocomplete_result.h
@@ -92,8 +92,7 @@
   // Moves matches from |old_matches| to provide a consistent result set.
   // |old_matches| is mutated during this, and should not be used afterwards.
   void TransferOldMatches(const AutocompleteInput& input,
-                          AutocompleteResult* old_matches,
-                          TemplateURLService* template_url_service);
+                          AutocompleteResult* old_matches);
 
   // Adds a new set of matches to the result set.  Does not re-sort.
   void AppendMatches(const ACMatches& matches);
@@ -113,24 +112,26 @@
                    TemplateURLService* template_url_service,
                    const AutocompleteMatch* preserve_default_match = nullptr);
 
-  // Ensures that matches with a suggestion_group_id value, are grouped together
-  // at the bottom of result set based on the order in which their group IDs
-  // first appear in the result set. This is done for two reasons:
+  // Ensures that matches belonging to suggestion groups, i.e., those with a
+  // suggestion_group_id value and a corresponding suggestion group info, are
+  // grouped together at the bottom of result set based on the order in which
+  // the groups should appear in the result set. This is done for two reasons:
   //
   // 1) Certain groups of remote zero-prefix matches need to appear under a
-  // header for transparency reasons. These optional headers are uniquely
-  // identified by the group IDs. Also it is possible for zero-prefix matches
-  // from different providers (e.g., local and remote) to mix and match. Hence,
-  // after mixing and sorting the matches, we group the ones with the same
-  // group ID and demote them to the bottom of the result set to ensure, one,
-  // matches without group IDs (and thus headers) appear at the top of the
-  // result set, and two, there are no interleaving headers; whether caused by
-  // bad server data or by mixing of local and remote zero-prefix suggestions.
+  // header as specified in SuggestionGroup. SuggestionGroups are uniquely
+  // identified by the group IDs in |suggestion_groups_map_|. It is also
+  // possible for zero-prefix matches to mix and match while belonging to the
+  // same groups (e.g., bad server data or mixing of local and remote
+  // suggestions from different providers). Hence, after mixing, deduping, and
+  // sorting the matches, we group the ones with the same group ID and demote
+  // them to the bottom of the result set based on a predetermined order. This
+  // ensures matches without group IDs or SuggestionGroup to appear at the top
+  // of the result set, and two, there are no interleaving of groups or headers;
   //
   // 2) Certain groups of non-zero-prefix matches, such as those produced by the
   // HistoryClusterProvider, must appear at the bottom of the result set.
-  // Setting a group ID on those matches ensures they sink to the bottom of the
-  // result set.
+  // Specifying a group ID (and a corresponding suggestion group info) for those
+  // matches ensures that would happen.
   //
   // Called after matches are deduped and sorted and before they are culled.
   void GroupAndDemoteMatchesInGroups();
@@ -232,19 +233,26 @@
   // This is only used for logging.
   std::vector<MatchDedupComparator> GetMatchDedupComparators() const;
 
-  // Gets the header string associated with |suggestion_group_id|. Returns an
-  // empty string if no suggestion group is found.
-  std::u16string GetHeaderForSuggestionGroup(int suggestion_group_id) const;
+  // Returns the header string associated with |suggestion_group_id|.
+  // DCHECKs whether |suggestion_group_id| is found in |suggestion_groups_map_|.
+  std::u16string GetHeaderForSuggestionGroup(
+      SuggestionGroupId suggestion_group_id) const;
 
   // Returns whether or not |suggestion_group_id| should be collapsed in the UI.
   // This method takes into account both the user's stored |prefs| as well as
   // the server-provided visibility hint for |suggestion_group_id|.
+  // DCHECKs whether |suggestion_group_id| is found in |suggestion_groups_map_|.
   bool IsSuggestionGroupHidden(PrefService* prefs,
-                               int suggestion_group_id) const;
+                               SuggestionGroupId suggestion_group_id) const;
+
+  // Returns the priority associated with |suggestion_group_id|.
+  // DCHECKs whether |suggestion_group_id| is found in |suggestion_groups_map_|.
+  SuggestionGroupPriority GetPriorityForSuggestionGroup(
+      SuggestionGroupId suggestion_group_id) const;
 
   // Updates |suggestion_groups_map_| with the suggestion groups information
   // from |suggeston_groups_map|. Followed by GroupAndDemoteMatchesInGroups()
-  // which sorts the matches based on the order in which their groups first
+  // which sorts the matches based on the order in which their groups should
   // appear while preserving the existing order of matches within the same
   // group.
   void MergeSuggestionGroupsMap(
diff --git a/components/omnibox/browser/autocomplete_result_android.cc b/components/omnibox/browser/autocomplete_result_android.cc
index 91049f41..818e528 100644
--- a/components/omnibox/browser/autocomplete_result_android.cc
+++ b/components/omnibox/browser/autocomplete_result_android.cc
@@ -99,7 +99,7 @@
 
   size_t index = 0;
   for (const auto& suggestion_group : suggestion_groups_map_) {
-    group_ids[index] = suggestion_group.first;
+    group_ids[index] = static_cast<int>(suggestion_group.first);
     group_names[index] = suggestion_group.second.header;
     group_collapsed_states[index] = suggestion_group.second.hidden;
     ++index;
diff --git a/components/omnibox/browser/autocomplete_result_unittest.cc b/components/omnibox/browser/autocomplete_result_unittest.cc
index 671e78b..b30a292f 100644
--- a/components/omnibox/browser/autocomplete_result_unittest.cc
+++ b/components/omnibox/browser/autocomplete_result_unittest.cc
@@ -106,7 +106,7 @@
     AutocompleteMatchType::Type type{AutocompleteMatchType::SEARCH_SUGGEST};
 
     // Suggestion Group ID for this suggestion
-    absl::optional<int> suggestion_group_id;
+    absl::optional<SuggestionGroupId> suggestion_group_id;
 
     // Inline autocompletion.
     std::string inline_autocompletion;
@@ -208,7 +208,9 @@
   match->relevance = data.relevance;
   match->allowed_to_be_default_match = data.allowed_to_be_default_match;
   match->duplicate_matches = data.duplicate_matches;
-  match->suggestion_group_id = data.suggestion_group_id;
+  if (data.suggestion_group_id.has_value()) {
+    match->suggestion_group_id = data.suggestion_group_id.value();
+  }
   match->inline_autocompletion = base::UTF8ToUTF16(data.inline_autocompletion);
 }
 
@@ -280,8 +282,7 @@
   AutocompleteResult current_result;
   current_result.AppendMatches(current_matches);
   current_result.SortAndCull(input, template_url_service_.get());
-  current_result.TransferOldMatches(input, &last_result,
-                                    template_url_service_.get());
+  current_result.TransferOldMatches(input, &last_result);
   current_result.SortAndCull(input, template_url_service_.get());
 
   AssertResultMatches(current_result, expected, expected_size);
@@ -1819,7 +1820,8 @@
   result.AppendMatches(matches);
   result.SortAndCull(input, template_url_service_.get());
 
-  TestData expected_data[] = {
+  ASSERT_EQ(6U, AutocompleteResult::GetMaxMatches(/*is_zero_suggest=*/false));
+  const std::array<TestData, 6> expected_data{{
       // default match unmoved
       {3, 2, 800, true, {}, AutocompleteMatchType::HISTORY_TITLE},
       // search types
@@ -1829,207 +1831,300 @@
       {6, 3, 1100, false, {}, AutocompleteMatchType::BOOKMARK_TITLE},
       {5, 2, 1000, false, {}, AutocompleteMatchType::HISTORY_BODY},
       {1, 2, 600, false, {}, AutocompleteMatchType::HISTORY_URL},
-  };
-
-  AssertResultMatches(result, expected_data,
-                      AutocompleteResult::GetMaxMatches());
+  }};
+  AssertResultMatches(result, expected_data.begin(), expected_data.size());
 }
 #endif
 
-TEST_F(AutocompleteResultTest, SortAndCullKeepGroupedSuggestionsLast) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitAndEnableFeatureWithParameters(
-      omnibox::kUIExperimentMaxAutocompleteMatches,
-      {{OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam, "6"}});
-  TestData data[] = {
-      {0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      {1, 2, 600, false, {}, AutocompleteMatchType::HISTORY_URL},
-      {2, 1, 700, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      {3, 2, 800, true, {}, AutocompleteMatchType::HISTORY_TITLE},
-      {4, 1, 900, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, 2},
-      {5, 2, 1000, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, 2},
-      {6, 3, 1100, false, {}, AutocompleteMatchType::BOOKMARK_TITLE},
-  };
-  ACMatches matches;
-  PopulateAutocompleteMatches(data, std::size(data), &matches);
-
-  AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
-                          TestSchemeClassifier());
-  AutocompleteResult result;
-
-  SuggestionGroupsMap suggestion_groups_map;
-  suggestion_groups_map[1].header = u"1";
-  suggestion_groups_map[2].header = u"2";
-  result.MergeSuggestionGroupsMap(suggestion_groups_map);
-  result.AppendMatches(matches);
-  result.SortAndCull(input, template_url_service_.get());
-
-  TestData expected_data[] = {
-      // default match unmoved
-      {3, 2, 800, true, {}, AutocompleteMatchType::HISTORY_TITLE},
-      // search types
-      {6, 3, 1100, false, {}, AutocompleteMatchType::BOOKMARK_TITLE},
-      {1, 2, 600, false, {}, AutocompleteMatchType::HISTORY_URL},
-      // Group <2> is scored higher
-      {5, 2, 1000, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      {4, 1, 900, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      // Group <1> is scored lower
-      {2, 1, 700, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-  };
-
-  AssertResultMatches(result, expected_data,
-                      AutocompleteResult::GetMaxMatches());
-}
-
-TEST_F(AutocompleteResultTest, SortAndCull_NoRetainSuggestionsWithHeaders) {
+TEST_F(AutocompleteResultTest, SortAndCull_DemoteSuggestionGroups_ExceedLimit) {
   base::test::ScopedFeatureList feature_list;
   feature_list.InitWithFeaturesAndParameters(
-      {
-          /* Enabled list */
-          {omnibox::kUIExperimentMaxAutocompleteMatches,
-           {{OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam, "2"}}},  //
-      },
+      {{omnibox::kUIExperimentMaxAutocompleteMatches,
+        {{OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam, "6"}}},
+       {omnibox::kMaxZeroSuggestMatches,
+        {{OmniboxFieldTrial::kMaxZeroSuggestMatchesParam, "5"}}}},
       {{omnibox::kDynamicMaxAutocomplete,
         omnibox::kRetainSuggestionsWithHeaders}});
+
+  const auto group_1 = SuggestionGroupId::kNonPersonalizedZeroSuggest1;
+  const auto group_2 = SuggestionGroupId::kNonPersonalizedZeroSuggest2;
   TestData data[] = {
-      {1, 1, 1100, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      {2, 1, 1099, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      {3, 1, 1098, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      {4, 1, 1097, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      {5, 1, 1096, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      // Do not trip the Dynamic Max Autocomplete
-      {5, 2, 1097, false, {}, AutocompleteMatchType::HISTORY_URL},
-      {6, 1, 1095, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      {7, 1, 1094, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      {8, 1, 1093, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      {9, 1, 1092, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
+      {0, 4, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_1},
+      {1, 2, 600, false, {}, AutocompleteMatchType::HISTORY_URL},
+      {2, 1, 700, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_1},
+      {3, 2, 800, true, {}, AutocompleteMatchType::HISTORY_TITLE},
+      {4, 1, 900, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
+      {5, 2, 1000, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
+      {6, 3, 1100, false, {}, AutocompleteMatchType::BOOKMARK_TITLE},
   };
   ACMatches matches;
   PopulateAutocompleteMatches(data, std::size(data), &matches);
 
-  AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
-                          TestSchemeClassifier());
-  AutocompleteResult result;
-
+  // Suggestion groups have SuggestionGroupPriority::kDefault priority by
+  // default.
   SuggestionGroupsMap suggestion_groups_map;
-  suggestion_groups_map[1].header = u"1";
-  suggestion_groups_map[2].header = u"2";
-  result.MergeSuggestionGroupsMap(suggestion_groups_map);
-  result.AppendMatches(matches);
-  result.SortAndCull(input, template_url_service_.get());
+  suggestion_groups_map[group_1].header = u"1";
+  suggestion_groups_map[group_2].header = u"2";
 
-  std::array<TestData, 2> expected_data{{
-      {1, 1, 1100, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      {2, 1, 1099, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-  }};
+  {
+    AutocompleteInput typed_input(u"a", metrics::OmniboxEventProto::OTHER,
+                                  TestSchemeClassifier());
+    AutocompleteResult result;
+    result.MergeSuggestionGroupsMap(suggestion_groups_map);
+    result.AppendMatches(matches);
+    result.SortAndCull(typed_input, template_url_service_.get());
 
-  AssertResultMatches(result, expected_data.begin(), expected_data.size());
+    ASSERT_EQ(6U, AutocompleteResult::GetMaxMatches(/*is_zero_suggest=*/false));
+    const std::array<TestData, 6> expected_data{{
+        // default match unmoved
+        {3, 2, 800, true, {}, AutocompleteMatchType::HISTORY_TITLE},
+        // other types
+        {6, 3, 1100, false, {}, AutocompleteMatchType::BOOKMARK_TITLE},
+        {1, 2, 600, false, {}, AutocompleteMatchType::HISTORY_URL},
+        // Group one is scored higher
+        {5, 2, 1000, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        {4, 1, 900, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        // Group two is scored lower
+        {2, 1, 700, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+    }};
+    AssertResultMatches(result, expected_data.begin(), expected_data.size());
+  }
+  {
+    AutocompleteInput zero_prefix_input(u"", metrics::OmniboxEventProto::NTP,
+                                        TestSchemeClassifier());
+    zero_prefix_input.set_focus_type(OmniboxFocusType::ON_FOCUS);
+    AutocompleteResult result;
+    result.MergeSuggestionGroupsMap(suggestion_groups_map);
+    result.AppendMatches(matches);
+    result.SortAndCull(zero_prefix_input, template_url_service_.get());
+
+    ASSERT_EQ(5U, AutocompleteResult::GetMaxMatches(/*is_zero_suggest=*/true));
+    const std::array<TestData, 5> expected_data{{
+        // default match unmoved
+        {3, 2, 800, true, {}, AutocompleteMatchType::HISTORY_TITLE},
+        // other types
+        {6, 3, 1100, false, {}, AutocompleteMatchType::BOOKMARK_TITLE},
+        {1, 2, 600, false, {}, AutocompleteMatchType::HISTORY_URL},
+        // Group two is scored higher
+        {5, 2, 1000, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        {4, 1, 900, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+    }};
+    AssertResultMatches(result, expected_data.begin(), expected_data.size());
+  }
+
+  // Set priorities that contradict the scores of the matches in groups.
+  suggestion_groups_map[group_1].priority =
+      SuggestionGroupPriority::kRemoteZeroSuggest1;
+  suggestion_groups_map[group_2].priority =
+      SuggestionGroupPriority::kRemoteZeroSuggest2;
+
+  {
+    AutocompleteInput typed_input(u"a", metrics::OmniboxEventProto::OTHER,
+                                  TestSchemeClassifier());
+    AutocompleteResult result;
+    result.MergeSuggestionGroupsMap(suggestion_groups_map);
+    result.AppendMatches(matches);
+    result.SortAndCull(typed_input, template_url_service_.get());
+
+    // typed matches are first culled, then grouped based on group IDs, and
+    // then ordered based on group priorities.
+    ASSERT_EQ(6U, AutocompleteResult::GetMaxMatches(/*is_zero_suggest=*/false));
+    const std::array<TestData, 6> expected_data{{
+        // default match unmoved
+        {3, 2, 800, true, {}, AutocompleteMatchType::HISTORY_TITLE},
+        // other types
+        {6, 3, 1100, false, {}, AutocompleteMatchType::BOOKMARK_TITLE},
+        {1, 2, 600, false, {}, AutocompleteMatchType::HISTORY_URL},
+        // Group one has a higher priority
+        {2, 1, 700, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        // Group two has a lower priority
+        {5, 2, 1000, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        {4, 1, 900, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+    }};
+    AssertResultMatches(result, expected_data.begin(), expected_data.size());
+  }
+  {
+    AutocompleteInput zero_prefix_input(u"", metrics::OmniboxEventProto::NTP,
+                                        TestSchemeClassifier());
+    zero_prefix_input.set_focus_type(OmniboxFocusType::ON_FOCUS);
+    AutocompleteResult result;
+    result.MergeSuggestionGroupsMap(suggestion_groups_map);
+    result.AppendMatches(matches);
+    result.SortAndCull(zero_prefix_input, template_url_service_.get());
+
+    // zero-prefix matches are first grouped basd on group IDs, then ordered
+    // based on group priorities, then culled.
+    ASSERT_EQ(5U, AutocompleteResult::GetMaxMatches(/*is_zero_suggest=*/true));
+    const std::array<TestData, 5> expected_data{{
+        // default match unmoved
+        {3, 2, 800, true, {}, AutocompleteMatchType::HISTORY_TITLE},
+        // other types
+        {6, 3, 1100, false, {}, AutocompleteMatchType::BOOKMARK_TITLE},
+        {1, 2, 600, false, {}, AutocompleteMatchType::HISTORY_URL},
+        // Group <1> has a higher priority
+        {2, 1, 700, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        {0, 4, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+    }};
+    AssertResultMatches(result, expected_data.begin(), expected_data.size());
+  }
 }
 
 TEST_F(AutocompleteResultTest,
-       SortAndCull_RetainSuggestionsWithHeaders_NoDynamicMaxAutocomplete) {
+       SortAndCull_RetainSuggestionGroups_NoDynamicMaxAutocomplete) {
   base::test::ScopedFeatureList feature_list;
   feature_list.InitWithFeaturesAndParameters(
-      {
-          /* Enabled list */
-          {omnibox::kUIExperimentMaxAutocompleteMatches,
-           {{OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam, "2"}}},  //
-          {omnibox::kRetainSuggestionsWithHeaders, {}}                   //
-      },
+      {{omnibox::kUIExperimentMaxAutocompleteMatches,
+        {{OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam, "2"}}},
+       {omnibox::kMaxZeroSuggestMatches,
+        {{OmniboxFieldTrial::kMaxZeroSuggestMatchesParam, "2"}}},
+       {omnibox::kRetainSuggestionsWithHeaders, {}}},
       {{omnibox::kDynamicMaxAutocomplete}});
+
+  const auto group_1 = SuggestionGroupId::kNonPersonalizedZeroSuggest1;
+  const auto group_2 = SuggestionGroupId::kNonPersonalizedZeroSuggest2;
   TestData data[] = {
       {1, 1, 1100, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
       {2, 1, 1099, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
       {3, 1, 1098, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
       {4, 1, 1097, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
       {5, 1, 1096, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      // Do not trip the Dynamic Max Autocomplete
       {5, 2, 1097, false, {}, AutocompleteMatchType::HISTORY_URL},
-      {6, 1, 1095, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      {7, 1, 1094, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      {8, 1, 1093, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      {9, 1, 1092, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
+      {6, 1, 1095, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_1},
+      {7, 1, 1094, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
+      {8, 1, 1093, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
+      {9, 1, 1092, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
   };
   ACMatches matches;
   PopulateAutocompleteMatches(data, std::size(data), &matches);
 
-  AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
-                          TestSchemeClassifier());
-  AutocompleteResult result;
-
+  // Suggestion groups have SuggestionGroupPriority::kDefault priority by
+  // default.
   SuggestionGroupsMap suggestion_groups_map;
-  suggestion_groups_map[1].header = u"1";
-  suggestion_groups_map[2].header = u"2";
-  result.MergeSuggestionGroupsMap(suggestion_groups_map);
-  result.AppendMatches(matches);
-  result.SortAndCull(input, template_url_service_.get());
+  suggestion_groups_map[group_1].header = u"1";
+  suggestion_groups_map[group_2].header = u"2";
 
-  std::array<TestData, 6> expected_data{{
-      {1, 1, 1100, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      {2, 1, 1099, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      {6, 1, 1095, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      {7, 1, 1094, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      {8, 1, 1093, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      {9, 1, 1092, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-  }};
+  {
+    AutocompleteInput typed_input(u"a", metrics::OmniboxEventProto::OTHER,
+                                  TestSchemeClassifier());
+    AutocompleteResult result;
+    result.MergeSuggestionGroupsMap(suggestion_groups_map);
+    result.AppendMatches(matches);
+    result.SortAndCull(typed_input, template_url_service_.get());
 
-  AssertResultMatches(result, expected_data.begin(), expected_data.size());
+    // Retaining suggestion groups has no effect on typed matched.
+    ASSERT_EQ(2U, AutocompleteResult::GetMaxMatches(/*is_zero_suggest=*/false));
+    const std::array<TestData, 2> expected_data{{
+        {1, 1, 1100, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        {2, 1, 1099, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+    }};
+    AssertResultMatches(result, expected_data.begin(), expected_data.size());
+  }
+  {
+    AutocompleteInput zero_prefix_input(u"", metrics::OmniboxEventProto::NTP,
+                                        TestSchemeClassifier());
+    zero_prefix_input.set_focus_type(OmniboxFocusType::ON_FOCUS);
+    AutocompleteResult result;
+
+    result.MergeSuggestionGroupsMap(suggestion_groups_map);
+    result.AppendMatches(matches);
+    result.SortAndCull(zero_prefix_input, template_url_service_.get());
+
+    ASSERT_EQ(2U, AutocompleteResult::GetMaxMatches(/*is_zero_suggest=*/true));
+    const std::array<TestData, 6> expected_data{{
+        {1, 1, 1100, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        {2, 1, 1099, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        // Group one is scored higher
+        {6, 1, 1095, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_1},
+        // Group two is scored lower
+        {7, 1, 1094, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
+        {8, 1, 1093, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
+        {9, 1, 1092, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
+    }};
+    AssertResultMatches(result, expected_data.begin(), expected_data.size());
+  }
 }
 
 TEST_F(AutocompleteResultTest,
-       SortAndCull_RetainSuggestionsWithHeaders_WithDynamicMaxAutocomplete) {
+       SortAndCull_RetainSuggestionGroups_WithDynamicMaxAutocomplete) {
   base::test::ScopedFeatureList feature_list;
   feature_list.InitWithFeaturesAndParameters(
-      {
-          /* Enabled list */
-          {omnibox::kUIExperimentMaxAutocompleteMatches,
-           {{OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam, "2"}}},  //
-          {omnibox::kDynamicMaxAutocomplete,
-           {{OmniboxFieldTrial::kDynamicMaxAutocompleteIncreasedLimitParam,
-             "3"}}},
-          {omnibox::kRetainSuggestionsWithHeaders, {}}  //
-      },
-      {/* Disabled list */});
+      {{omnibox::kUIExperimentMaxAutocompleteMatches,
+        {{OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam, "2"}}},
+       {omnibox::kMaxZeroSuggestMatches,
+        {{OmniboxFieldTrial::kMaxZeroSuggestMatchesParam, "2"}}},
+       {omnibox::kDynamicMaxAutocomplete,
+        {{OmniboxFieldTrial::kDynamicMaxAutocompleteIncreasedLimitParam, "3"}}},
+       {omnibox::kRetainSuggestionsWithHeaders, {}}},
+      {});
+
+  const auto group_1 = SuggestionGroupId::kNonPersonalizedZeroSuggest1;
+  const auto group_2 = SuggestionGroupId::kNonPersonalizedZeroSuggest2;
   TestData data[] = {
       {1, 1, 1100, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
       {2, 1, 1099, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
       {3, 1, 1098, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
       {4, 1, 1097, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
       {5, 1, 1096, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      // Do not trip the Dynamic Max Autocomplete
       {5, 2, 1097, false, {}, AutocompleteMatchType::HISTORY_URL},
-      {6, 1, 1095, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      {7, 1, 1094, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 2},
-      {8, 1, 1093, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 2},
-      {9, 1, 1092, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 2},
+      {6, 1, 1095, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_1},
+      {7, 1, 1094, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
+      {8, 1, 1093, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
+      {9, 1, 1092, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
   };
   ACMatches matches;
   PopulateAutocompleteMatches(data, std::size(data), &matches);
 
-  AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
-                          TestSchemeClassifier());
-  AutocompleteResult result;
-
+  // Set priorities that contradict the scores of the matches in groups.
   SuggestionGroupsMap suggestion_groups_map;
-  suggestion_groups_map[1].header = u"1";
-  suggestion_groups_map[2].header = u"2";
-  result.MergeSuggestionGroupsMap(suggestion_groups_map);
-  result.AppendMatches(matches);
-  result.SortAndCull(input, template_url_service_.get());
+  suggestion_groups_map[group_1].header = u"1";
+  suggestion_groups_map[group_1].priority =
+      SuggestionGroupPriority::kRemoteZeroSuggest2;
+  suggestion_groups_map[group_2].header = u"2";
+  suggestion_groups_map[group_2].priority =
+      SuggestionGroupPriority::kRemoteZeroSuggest1;
 
-  std::array<TestData, 7> expected_data{{
-      {1, 1, 1100, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      {2, 1, 1099, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      {3, 1, 1098, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
-      // Group <1> is scored higher
-      {6, 1, 1095, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 1},
-      // Group <2> is scored lower
-      {7, 1, 1094, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 2},
-      {8, 1, 1093, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 2},
-      {9, 1, 1092, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, 2},
-  }};
+  {
+    AutocompleteInput typed_input(u"a", metrics::OmniboxEventProto::OTHER,
+                                  TestSchemeClassifier());
+    AutocompleteResult result;
+    result.MergeSuggestionGroupsMap(suggestion_groups_map);
+    result.AppendMatches(matches);
+    result.SortAndCull(typed_input, template_url_service_.get());
 
-  AssertResultMatches(result, expected_data.begin(), expected_data.size());
+    // Retaining suggestion groups has no effect on typed matched.
+    ASSERT_EQ(2U, AutocompleteResult::GetMaxMatches(/*is_zero_suggest=*/false));
+    const std::array<TestData, 3> expected_data{{
+        {1, 1, 1100, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        {2, 1, 1099, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        {3, 1, 1098, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+    }};
+    AssertResultMatches(result, expected_data.begin(), expected_data.size());
+  }
+  {
+    AutocompleteInput zero_prefix_input(u"", metrics::OmniboxEventProto::NTP,
+                                        TestSchemeClassifier());
+    zero_prefix_input.set_focus_type(OmniboxFocusType::ON_FOCUS);
+    AutocompleteResult result;
+
+    result.MergeSuggestionGroupsMap(suggestion_groups_map);
+    result.AppendMatches(matches);
+    result.SortAndCull(zero_prefix_input, template_url_service_.get());
+
+    // DynamicMaxAutocomplete has no effect on zero-prefix matched. The number
+    // of results not in groups is determined by the zero-suggest limit.
+    ASSERT_EQ(2U, AutocompleteResult::GetMaxMatches(/*is_zero_suggest=*/true));
+    const std::array<TestData, 6> expected_data{{
+        {1, 1, 1100, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        {2, 1, 1099, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
+        // Group two has a higher priority
+        {7, 1, 1094, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
+        {8, 1, 1093, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
+        {9, 1, 1092, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_2},
+        // Group one has a lower priority
+        {6, 1, 1095, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group_1},
+    }};
+    AssertResultMatches(result, expected_data.begin(), expected_data.size());
+  }
 }
 
 TEST_F(AutocompleteResultTest,
diff --git a/components/omnibox/browser/history_cluster_provider.cc b/components/omnibox/browser/history_cluster_provider.cc
index a7df8e0..8b79f85 100644
--- a/components/omnibox/browser/history_cluster_provider.cc
+++ b/components/omnibox/browser/history_cluster_provider.cc
@@ -108,8 +108,10 @@
   match.contents_class.push_back(
       ACMatchClassification(0, ACMatchClassification::URL));
 
-  match.suggestion_group_id = 1;
-  suggestion_groups_map_[1].MergeFrom({});
+  match.suggestion_group_id = SuggestionGroupId::kHistoryCluster;
+  // Insert a corresponding SuggestionGroup with default values in the
+  // suggestion groups map; otherwise the group ID will get dropped.
+  suggestion_groups_map_[SuggestionGroupId::kHistoryCluster];
 
   return match;
 }
diff --git a/components/omnibox/browser/history_url_provider.cc b/components/omnibox/browser/history_url_provider.cc
index fcc0554..99941c6 100644
--- a/components/omnibox/browser/history_url_provider.cc
+++ b/components/omnibox/browser/history_url_provider.cc
@@ -112,7 +112,7 @@
   // precalculate the ending position.
   for (size_t i = 0; i < matches->size(); ++i) {
     for (history::HistoryMatches::iterator j(matches->begin() + i + 1);
-         j != matches->end(); ) {
+         j != matches->end();) {
       if ((*matches)[i].url_info.url() == j->url_info.url())
         j = matches->erase(j);
       else
@@ -121,9 +121,9 @@
   }
 }
 
-// Calculates a new relevance score applying half-life time decaying to |count|
-// using |time_since_last_visit| and |score_buckets|.  This function will never
-// return a score higher than |undecayed_relevance|; in other words, it can only
+// Calculates a new relevance score applying half-life time decaying to `count`
+// using `time_since_last_visit` and `score_buckets`.  This function will never
+// return a score higher than `undecayed_relevance`; in other words, it can only
 // demote the old score.
 double CalculateRelevanceUsingScoreBuckets(
     const HUPScoringParams::ScoreBuckets& score_buckets,
@@ -143,22 +143,23 @@
 
   const HUPScoringParams::ScoreBuckets::CountMaxRelevance* score_bucket =
       nullptr;
-  const double factor = (score_buckets.use_decay_factor() ?
-      decay_factor : decayed_count);
+  const double factor =
+      (score_buckets.use_decay_factor() ? decay_factor : decayed_count);
   for (size_t i = 0; i < score_buckets.buckets().size(); ++i) {
     score_bucket = &score_buckets.buckets()[i];
     if (factor >= score_bucket->first)
       break;
   }
 
-  return (score_bucket && (undecayed_relevance > score_bucket->second)) ?
-      score_bucket->second : undecayed_relevance;
+  return (score_bucket && (undecayed_relevance > score_bucket->second))
+             ? score_bucket->second
+             : undecayed_relevance;
 }
 
-// Returns a new relevance score for the given |match| based on the
-// |old_relevance| score and |scoring_params|.  The new relevance score is
-// guaranteed to be less than or equal to |old_relevance|.  In other words, this
-// function can only demote a score, never boost it.  Returns |old_relevance| if
+// Returns a new relevance score for the given `match` based on the
+// `old_relevance` score and `scoring_params`.  The new relevance score is
+// guaranteed to be less than or equal to `old_relevance`.  In other words, this
+// function can only demote a score, never boost it.  Returns `old_relevance` if
 // experimental scoring is disabled.
 int CalculateRelevanceScoreUsingScoringParams(
     const history::HistoryMatch& match,
@@ -184,7 +185,7 @@
 }
 
 // Extracts typed_count, visit_count, and last_visited time from the URLRow and
-// puts them in the additional info field of the |match| for display in
+// puts them in the additional info field of the `match` for display in
 // about:omnibox.
 void RecordAdditionalInfoFromUrlRow(const history::URLRow& info,
                                     AutocompleteMatch* match) {
@@ -193,16 +194,16 @@
   match->RecordAdditionalInfo("last visit", info.last_visit());
 }
 
-// If |create_if_necessary| is true, ensures that |matches| contains an entry
-// for |info|, creating a new such entry if necessary (using |match_template|
+// If `create_if_necessary` is true, ensures that `matches` contains an entry
+// for `info`, creating a new such entry if necessary (using `match_template`
 // to get all the other match data).
 //
-// If |promote| is true, this also ensures the entry is the first element in
-// |matches|, moving or adding it to the front as appropriate.  When |promote|
+// If `promote` is true, this also ensures the entry is the first element in
+// `matches`, moving or adding it to the front as appropriate.  When `promote`
 // is false, existing matches are left in place, and newly added matches are
 // placed at the back.
 //
-// It's OK to call this function with both |create_if_necessary| and |promote|
+// It's OK to call this function with both `create_if_necessary` and `promote`
 // false, in which case we'll do nothing.
 //
 // Returns whether the match exists regardless if it was promoted/created.
@@ -211,7 +212,7 @@
                           history::HistoryMatches* matches,
                           bool create_if_necessary,
                           bool promote) {
-  // |matches| may already have an entry for this.
+  // `matches` may already have an entry for this.
   for (history::HistoryMatches::iterator i(matches->begin());
        i != matches->end(); ++i) {
     if (i->url_info.url() == info.url()) {
@@ -225,7 +226,7 @@
   if (!create_if_necessary)
     return false;
 
-  // No entry, so create one using |match_template| as a basis.
+  // No entry, so create one using `match_template` as a basis.
   history::HistoryMatch match = match_template;
   match.url_info = info;
   if (promote)
@@ -236,7 +237,7 @@
   return true;
 }
 
-// Returns whether |match| is suitable for inline autocompletion.
+// Returns whether `match` is suitable for inline autocompletion.
 bool CanPromoteMatchForInlineAutocomplete(const history::HistoryMatch& match) {
   // We can promote this match if it's been typed at least n times, where n == 1
   // for "simple" (host-only) URLs and n == 2 for others.  We set a higher bar
@@ -245,10 +246,10 @@
   // URLs, if the user manually edits the URL or types some long thing in by
   // hand, we wouldn't want to immediately start autocompleting it.
   return match.url_info.typed_count() &&
-      ((match.url_info.typed_count() > 1) || match.IsHostOnly());
+         ((match.url_info.typed_count() > 1) || match.IsHostOnly());
 }
 
-// Given the user's |input| and a |match| created from it, reduce the match's
+// Given the user's `input` and a `match` created from it, reduce the match's
 // URL to just a host.  If this host still matches the user input, return it.
 // Returns the empty string on failure.
 GURL ConvertToHostOnly(const history::HistoryMatch& match,
@@ -314,7 +315,7 @@
  private:
   raw_ptr<HistoryURLProvider> provider_;
   raw_ptr<history::URLDatabase> db_;
-  Type type_;
+  Type type_ = Type::kInvalid;
   history::URLRow url_row_;
 };
 
@@ -322,7 +323,7 @@
     HistoryURLProvider* provider,
     const AutocompleteInput& input,
     history::URLDatabase* db)
-    : provider_(provider), db_(db), type_(Type::kInvalid) {
+    : provider_(provider), db_(db) {
   // Detect email addresses.  These cases will look like "http://user@site/",
   // and because the history backend strips auth creds, we'll get a bogus exact
   // match below if the user has visited "site".
@@ -354,7 +355,7 @@
 
   // If the input does not correspond to a visited URL, we check if the
   // canonical URL has an intranet hostname that the user visited (albeit with a
-  // different port and/or path) before. If this is true, |url_row_| will be
+  // different port and/or path) before. If this is true, `url_row_` will be
   // mostly empty: the URL field will be set to an unvisited URL with the same
   // scheme and host as some visited URL in the db.
   const GURL as_known_intranet_url = provider_->AsKnownIntranetURL(db_, input);
@@ -387,8 +388,7 @@
       search_terms_data(SearchTermsData::MakeSnapshot(search_terms_data)),
       allow_deleting_browser_history(allow_deleting_browser_history) {}
 
-HistoryURLProviderParams::~HistoryURLProviderParams() {
-}
+HistoryURLProviderParams::~HistoryURLProviderParams() = default;
 
 size_t HistoryURLProviderParams::EstimateMemoryUsage() const {
   size_t res = 0;
@@ -417,14 +417,14 @@
 void HistoryURLProvider::Start(const AutocompleteInput& input,
                                bool minimal_changes) {
   TRACE_EVENT0("omnibox", "HistoryURLProvider::Start");
-  // NOTE: We could try hard to do less work in the |minimal_changes| case
+  // NOTE: We could try hard to do less work in the `minimal_changes` case
   // here; some clever caching would let us reuse the raw matches from the
   // history DB without re-querying.  However, we'd still have to go back to
   // the history thread to mark these up properly, and if pass 2 is currently
   // running, we'd need to wait for it to return to the main thread before
   // doing this (we can't just write new data for it to read due to thread
   // safety issues).  At that point it's just as fast, and easier, to simply
-  // re-run the query from scratch and ignore |minimal_changes|.
+  // re-run the query from scratch and ignore `minimal_changes`.
 
   // Cancel any in-progress query.
   Stop(true, false);
@@ -491,16 +491,17 @@
 
   // Get the default search provider and search terms data now since we have to
   // retrieve these on the UI thread, and the second pass runs on the history
-  // thread. |template_url_service| can be null when testing.
+  // thread. `template_url_service` can be null when testing.
   TemplateURLService* template_url_service = client()->GetTemplateURLService();
-  const TemplateURL* default_search_provider = template_url_service ?
-      template_url_service->GetDefaultSearchProvider() : nullptr;
+  const TemplateURL* default_search_provider =
+      template_url_service ? template_url_service->GetDefaultSearchProvider()
+                           : nullptr;
   const SearchTermsData* search_terms_data =
       template_url_service ? &template_url_service->search_terms_data()
                            : nullptr;
 
   // Create the data structure for the autocomplete passes.  We'll save this off
-  // onto the |params_| member for later deletion below if we need to run pass
+  // onto the `params_` member for later deletion below if we need to run pass
   // 2.
   std::unique_ptr<HistoryURLProviderParams> params(new HistoryURLProviderParams(
       fixed_up_input, autocomplete_input, trim_http, what_you_typed_match,
@@ -520,7 +521,7 @@
     DoAutocomplete(nullptr, url_db, params.get());
     matches_.clear();
     PromoteMatchesIfNecessary(*params);
-    // NOTE: We don't reset |params| here since at least the |promote_type|
+    // NOTE: We don't reset `params` here since at least the `promote_type`
     // field on it will be read by the second pass -- see comments in
     // DoAutocomplete().
   }
@@ -576,15 +577,14 @@
       base::BindOnce(&HistoryURLProvider::QueryComplete, this, params));
 }
 
-HistoryURLProvider::~HistoryURLProvider() {
-  // Note: This object can get leaked on shutdown if there are pending
-  // requests on the database (which hold a reference to us). Normally, these
-  // messages get flushed for each thread. We do a round trip from main, to
-  // history, back to main while holding a reference. If the main thread
-  // completes before the history thread, the message to delegate back to the
-  // main thread will not run and the reference will leak. Therefore, don't do
-  // anything on destruction.
-}
+// Note: This object can get leaked on shutdown if there are pending
+// requests on the database (which hold a reference to us). Normally, these
+// messages get flushed for each thread. We do a round trip from main, to
+// history, back to main while holding a reference. If the main thread
+// completes before the history thread, the message to delegate back to the
+// main thread will not run and the reference will leak. Therefore, don't do
+// anything on destruction.
+HistoryURLProvider::~HistoryURLProvider() = default;
 
 // static
 int HistoryURLProvider::CalculateRelevance(MatchType match_type,
@@ -670,7 +670,7 @@
   }
 
   // Try to create a shorter suggestion from the best match.
-  // We consider the what you typed match eligible for display when it's
+  // We consider the what-you-typed match eligible for display when it's
   // navigable and there's a reasonable chance the user intended to do
   // something other than search.  We use a variety of heuristics to determine
   // this, e.g. whether the user explicitly typed a scheme, or if omnibox
@@ -706,10 +706,11 @@
       // FRONT_HISTORY_MATCH during the first pass, the second pass will not
       // consider the exact suggestion to be in history and therefore will not
       // suggest the exact input as a better match.  (Note that during the first
-      // pass, this conditional will always succeed since |promote_type| is
+      // pass, this conditional will always succeed since `promote_type` is
       // initialized to NEITHER.)
       (params->promote_type != HistoryURLProviderParams::FRONT_HISTORY_MATCH);
-  params->exact_suggestion_is_in_history = can_check_history_for_exact_match &&
+  params->exact_suggestion_is_in_history =
+      can_check_history_for_exact_match &&
       FixupExactSuggestion(db, classifier, params);
 
   // If we succeeded in fixing up the exact match based on the user's history,
@@ -736,9 +737,9 @@
     params->promote_type = HistoryURLProviderParams::FRONT_HISTORY_MATCH;
   } else {
     // Failed to promote any URLs.  Use the What You Typed match, if we have it.
-    params->promote_type = params->have_what_you_typed_match ?
-        HistoryURLProviderParams::WHAT_YOU_TYPED_MATCH :
-        HistoryURLProviderParams::NEITHER;
+    params->promote_type = params->have_what_you_typed_match
+                               ? HistoryURLProviderParams::WHAT_YOU_TYPED_MATCH
+                               : HistoryURLProviderParams::NEITHER;
   }
 
   const size_t max_results =
@@ -759,9 +760,8 @@
   if (params.promote_type == HistoryURLProviderParams::NEITHER)
     return;
   if (params.promote_type == HistoryURLProviderParams::FRONT_HISTORY_MATCH) {
-    matches_.push_back(
-        HistoryMatchToACMatch(params, 0,
-                              CalculateRelevance(INLINE_AUTOCOMPLETE, 0)));
+    matches_.push_back(HistoryMatchToACMatch(
+        params, 0, CalculateRelevance(INLINE_AUTOCOMPLETE, 0)));
   }
   // There are two cases where we need to add the what-you-typed-match:
   //   * If params.promote_type is WHAT_YOU_TYPED_MATCH, we're being explicitly
@@ -786,7 +786,7 @@
 void HistoryURLProvider::QueryComplete(
     HistoryURLProviderParams* params_gets_deleted) {
   TRACE_EVENT0("omnibox", "HistoryURLProvider::QueryComplete");
-  // Ensure |params_gets_deleted| gets deleted on exit.
+  // Ensure `params_gets_deleted` gets deleted on exit.
   std::unique_ptr<HistoryURLProviderParams> params(params_gets_deleted);
 
   // If the user hasn't already started another query, clear our member pointer
@@ -798,24 +798,26 @@
   if (params->cancel_flag.IsSet())
     return;  // Already set done_ when we canceled, no need to set it again.
 
-  // Don't modify |matches_| if the query failed, since it might have a default
-  // match in it, whereas |params->matches| will be empty.
+  // Don't modify `matches_` if the query failed, since it might have a default
+  // match in it, whereas `params->matches` will be empty.
   if (!params->failed) {
     matches_.clear();
     PromoteMatchesIfNecessary(*params);
 
     // Determine relevance of highest scoring match, if any.
-    int relevance = matches_.empty() ?
-        CalculateRelevance(NORMAL,
-                           static_cast<int>(params->matches.size() - 1)) :
-        matches_[0].relevance;
+    int relevance =
+        matches_.empty()
+            ? CalculateRelevance(NORMAL,
+                                 static_cast<int>(params->matches.size() - 1))
+            : matches_[0].relevance;
 
     // Convert the history matches to autocomplete matches.  If we promoted the
     // first match, skip over it.
-    const size_t first_match =
-        (params->exact_suggestion_is_in_history ||
-         (params->promote_type ==
-             HistoryURLProviderParams::FRONT_HISTORY_MATCH)) ? 1 : 0;
+    const size_t first_match = (params->exact_suggestion_is_in_history ||
+                                (params->promote_type ==
+                                 HistoryURLProviderParams::FRONT_HISTORY_MATCH))
+                                   ? 1
+                                   : 0;
     for (size_t i = first_match; i < params->matches.size(); ++i) {
       // All matches score one less than the previous match.
       --relevance;
@@ -975,14 +977,14 @@
   bool can_add_search_base_to_matches = !params->have_what_you_typed_match;
   if (search_base.is_empty()) {
     // Search from what the user typed when we couldn't reduce the best match
-    // to a host.  Careful: use a substring of |match| here, rather than the
-    // first match in |params|, because they might have different prefixes.  If
+    // to a host.  Careful: use a substring of `match` here, rather than the
+    // first match in `params`, because they might have different prefixes.  If
     // the user typed "google.com", params->what_you_typed_match will hold
-    // "http://google.com/", but |match| might begin with
+    // "http://google.com/", but `match` might begin with
     // "http://www.google.com/".
     // TODO: this should be cleaned up, and is probably incorrect for IDN.
-    std::string new_match = match.url_info.url().possibly_invalid_spec().
-        substr(0, match.input_location + params->input.text().length());
+    std::string new_match = match.url_info.url().possibly_invalid_spec().substr(
+        0, match.input_location + params->input.text().length());
     search_base = GURL(new_match);
     if (search_base.is_empty())
       return false;  // Can't construct a URL from which to start a search.
@@ -991,9 +993,9 @@
         (search_base != params->what_you_typed_match.destination_url);
   }
   if (search_base == match.url_info.url())
-    return false;  // Couldn't shorten |match|, so no URLs to search over.
+    return false;  // Couldn't shorten `match`, so no URLs to search over.
 
-  // Search the DB for short URLs between our base and |match|.
+  // Search the DB for short URLs between our base and `match`.
   history::URLRow info(search_base);
   bool promote = true;
   // A short URL is only worth suggesting if it's been visited at least a third
@@ -1007,8 +1009,9 @@
   // autocomplete, unstable.
   const int min_typed_count = match.url_info.typed_count() ? 1 : 0;
   if (!db->FindShortestURLFromBase(search_base.possibly_invalid_spec(),
-          match.url_info.url().possibly_invalid_spec(), min_visit_count,
-          min_typed_count, can_add_search_base_to_matches, &info)) {
+                                   match.url_info.url().possibly_invalid_spec(),
+                                   min_visit_count, min_typed_count,
+                                   can_add_search_base_to_matches, &info)) {
     if (!can_add_search_base_to_matches)
       return false;  // Couldn't find anything and can't add the search base.
 
@@ -1029,7 +1032,7 @@
     HistoryURLProviderParams* params) const {
   const base::Time& threshold(history::AutocompleteAgeThreshold());
   for (history::HistoryMatches::iterator i(params->matches.begin());
-       i != params->matches.end(); ) {
+       i != params->matches.end();) {
     if (RowQualifiesAsSignificant(i->url_info, threshold) &&
         (!params->default_search_provider ||
          !params->default_search_provider->IsSearchURL(
@@ -1045,14 +1048,14 @@
                                        history::HistoryMatches* matches,
                                        size_t max_results) const {
   for (size_t source = 0;
-       (source < matches->size()) && (source < max_results); ) {
+       (source < matches->size()) && (source < max_results);) {
     const GURL& url = (*matches)[source].url_info.url();
     // TODO(brettw) this should go away when everything uses GURL.
     history::RedirectList redirects = backend->QueryRedirectsFrom(url);
     if (!redirects.empty()) {
-      // Remove all but the first occurrence of any of these redirects in the
-      // search results. We also must add the URL we queried for, since it may
-      // not be the first match and we'd want to remove it.
+      // Remove all but the first occurrence of these redirects in the search
+      // results. We also must add the URL we queried for, since it may not be
+      // the first match and we'd want to remove it.
       //
       // For example, when A redirects to B and our matches are [A, X, B],
       // we'll get B as the redirects from, and we want to remove the second
@@ -1078,19 +1081,21 @@
 
   // Find the first occurrence of any URL in the redirect chain. We want to
   // keep this one since it is rated the highest.
-  history::HistoryMatches::iterator first(std::find_first_of(
-      matches->begin(), matches->end(), remove.begin(), remove.end(),
-      history::HistoryMatch::EqualsGURL));
+  history::HistoryMatches::iterator first(
+      std::find_first_of(matches->begin(), matches->end(), remove.begin(),
+                         remove.end(), history::HistoryMatch::EqualsGURL));
   DCHECK(first != matches->end()) << "We should have always found at least the "
-      "original URL.";
+                                     "original URL.";
 
   // Find any following occurrences of any URL in the redirect chain, these
   // should be deleted.
-  for (history::HistoryMatches::iterator next(std::find_first_of(first + 1,
-           matches->end(), remove.begin(), remove.end(),
-           history::HistoryMatch::EqualsGURL));
-       next != matches->end(); next = std::find_first_of(next, matches->end(),
-           remove.begin(), remove.end(), history::HistoryMatch::EqualsGURL)) {
+  for (history::HistoryMatches::iterator next(
+           std::find_first_of(first + 1, matches->end(), remove.begin(),
+                              remove.end(), history::HistoryMatch::EqualsGURL));
+       next != matches->end();
+       next = std::find_first_of(next, matches->end(), remove.begin(),
+                                 remove.end(),
+                                 history::HistoryMatch::EqualsGURL)) {
     // Remove this item. When we remove an item before the source index, we
     // need to shift it to the right and remember that so we can return it.
     next = matches->erase(next);
@@ -1148,7 +1153,7 @@
   match.description_class =
       ClassifyDescription(params.input.text(), match.description);
 
-  // |inline_autocomplete_offset| was guaranteed not to be npos before the call
+  // `inline_autocomplete_offset` was guaranteed not to be npos before the call
   // to FormatUrl().  If it is npos now, that means the represented location no
   // longer exists as such in the formatted string, e.g. if the offset pointed
   // into the middle of a punycode sequence fixed up to Unicode.  In this case,
diff --git a/components/omnibox/browser/history_url_provider.h b/components/omnibox/browser/history_url_provider.h
index b3e480be..e07e3cfd 100644
--- a/components/omnibox/browser/history_url_provider.h
+++ b/components/omnibox/browser/history_url_provider.h
@@ -33,7 +33,7 @@
 namespace history {
 class HistoryBackend;
 class URLDatabase;
-}
+}  // namespace history
 
 // How history autocomplete works
 // ==============================
@@ -95,7 +95,7 @@
 // Used to communicate autocomplete parameters between threads via the history
 // service.
 struct HistoryURLProviderParams {
-  // See comments on |promote_type| below.
+  // See comments on `promote_type` below.
   enum PromoteType {
     WHAT_YOU_TYPED_MATCH,
     FRONT_HISTORY_MATCH,
@@ -122,9 +122,9 @@
   // A copy of the autocomplete input. We need the copy since this object will
   // live beyond the original query while it runs on the history thread.
   AutocompleteInput input;
-  // |input_before_fixup| is needed for invoking
-  // |AutocompleteMatch::SetAllowedToBeDefault| which considers
-  // trailing input whitespaces which the fixed up |input| will have trimmed.
+  // `input_before_fixup` is needed for invoking
+  // `AutocompleteMatch::SetAllowedToBeDefault` which considers
+  // trailing input whitespaces which the fixed up `input` will have trimmed.
   AutocompleteInput input_before_fixup;
 
   // Set when "http://" should be trimmed from the beginning of the URLs.
@@ -142,16 +142,16 @@
   // Set by ExecuteWithDB() on the history thread when the query could not be
   // performed because the history system failed to properly init the database.
   // If this is set when the main thread is called back, it avoids changing
-  // |matches_| at all, so it won't delete the default match Start() creates.
+  // `matches_` at all, so it won't delete the default match Start() creates.
   bool failed;
 
   // List of matches written by DoAutocomplete().  Upon its return the provider
-  // converts this list to ACMatches and places them in |matches_|.
+  // converts this list to ACMatches and places them in `matches_`.
   history::HistoryMatches matches;
 
   // True if the suggestion for exactly what the user typed appears as a known
   // URL in the user's history.  In this case, this will also be the first match
-  // in |matches|.
+  // in `matches`.
   //
   // NOTE: There are some complications related to keeping things consistent
   // between passes and how we deal with intranet URLs, which are too complex to
@@ -159,19 +159,19 @@
   // FixupExactSuggestion() for specific comments.
   bool exact_suggestion_is_in_history;
 
-  // Tells the provider whether to promote the what you typed match, the first
-  // element of |matches|, or neither as the first AutocompleteMatch.  If
-  // |exact_suggestion_is_in_history| is true (and thus "the what you typed
-  // match" and "the first element of |matches|" represent the same thing), this
+  // Tells the provider whether to promote the what-you-typed match, the first
+  // element of `matches`, or neither as the first AutocompleteMatch.  If
+  // `exact_suggestion_is_in_history` is true (and thus the what-you-typed
+  // match and the first element of `matches` represent the same thing), this
   // will be set to WHAT_YOU_TYPED_MATCH.
   //
   // NOTE: The second pass of DoAutocomplete() checks what the first pass set
   // this to.  See comments in DoAutocomplete().
   PromoteType promote_type;
 
-  // True if |what_you_typed_match| is eligible for display.  If this is true,
-  // PromoteMatchesIfNecessary() may choose to place |what_you_typed_match| on
-  // |matches_| even when |promote_type| is not WHAT_YOU_TYPED_MATCH.
+  // True if `what_you_typed_match` is eligible for display.  If this is true,
+  // PromoteMatchesIfNecessary() may choose to place `what_you_typed_match` on
+  // `matches_` even when `promote_type` is not WHAT_YOU_TYPED_MATCH.
   bool have_what_you_typed_match;
 
   // The default search provider and search terms data necessary to cull results
@@ -243,15 +243,15 @@
 
   ~HistoryURLProvider() override;
 
-  // Determines the relevance for a match, given its type.  If |match_type| is
-  // NORMAL, |match_number| is a number indicating the relevance of the match
-  // (higher == more relevant).  For other values of |match_type|,
-  // |match_number| is ignored.  Only called some of the time; for some matches,
+  // Determines the relevance for a match, given its type.  If `match_type` is
+  // NORMAL, `match_number` is a number indicating the relevance of the match
+  // (higher == more relevant).  For other values of `match_type`,
+  // `match_number` is ignored.  Only called sometimes; for some matches,
   // relevancy scores are assigned consecutively decreasing (1416, 1415, ...).
   static int CalculateRelevance(MatchType match_type, int match_number);
 
   // Returns a set of classifications that highlight all the occurrences of
-  // |input_text| at word breaks in |description|.
+  // `input_text` at word breaks in `description`.
   static ACMatchClassifications ClassifyDescription(
       const std::u16string& input_text,
       const std::u16string& description);
@@ -263,8 +263,8 @@
                       history::URLDatabase* db,
                       HistoryURLProviderParams* params);
 
-  // May promote the what you typed match, the first history match in
-  // params->matches, or both to the front of |matches_|, depending on the
+  // May promote the what-you-typed match, the first history match in
+  // params->matches, or both to the front of `matches_`, depending on the
   // values of params->promote_type, params->have_what_you_typed_match, and
   // params->prevent_inline_autocomplete.
   void PromoteMatchesIfNecessary(const HistoryURLProviderParams& params);
@@ -286,7 +286,7 @@
   // Helper function for FixupExactSuggestion. If a URL with the same host name
   // has been visited by the user in the past, the function returns a valid URL.
   // The return value is built from the canonicalized version of the
-  // autocomplete input in |params|. The scheme and host format (e.g. prefixed
+  // autocomplete input in `params`. The scheme and host format (e.g. prefixed
   // with "www.") of the return value is the same as one of the corresponding
   // entries in the database.
   GURL AsKnownIntranetURL(history::URLDatabase* db,
@@ -300,9 +300,8 @@
   // once, we'll suggest http://example.com/ even if they've never been to it.
   // Returns true if a match was successfully created/promoted that we're
   // willing to inline autocomplete.
-  bool PromoteOrCreateShorterSuggestion(
-      history::URLDatabase* db,
-      HistoryURLProviderParams* params);
+  bool PromoteOrCreateShorterSuggestion(history::URLDatabase* db,
+                                        HistoryURLProviderParams* params);
 
   // Removes results that have been rarely typed or visited, and not any time
   // recently.  The exact parameters for this heuristic can be found in the
@@ -312,27 +311,27 @@
   // anyway.
   void CullPoorMatches(HistoryURLProviderParams* params) const;
 
-  // Removes results that redirect to each other, leaving at most |max_results|
+  // Removes results that redirect to each other, leaving at most `max_results`
   // results.
   void CullRedirects(history::HistoryBackend* backend,
                      history::HistoryMatches* matches,
                      size_t max_results) const;
 
   // Helper function for CullRedirects, this removes all but the first
-  // occurance of [any of the set of strings in |remove|] from the |matches|
+  // occurrence of [any of the set of strings in `remove`] from the `matches`
   // list.
   //
   // The return value is the index of the item that is after the item in the
-  // input identified by |source_index|. If |source_index| or an item before
+  // input identified by `source_index`. If `source_index` or an item before
   // is removed, the next item will be shifted, and this allows the caller to
   // pick up on the next one when this happens.
   size_t RemoveSubsequentMatchesOf(history::HistoryMatches* matches,
                                    size_t source_index,
                                    const std::vector<GURL>& remove) const;
 
-  // Converts a specified |match_number| from params.matches into an
+  // Converts a specified `match_number` from params.matches into an
   // autocomplete match for display.  If experimental scoring is enabled, the
-  // final relevance score might be different from the given |relevance|.
+  // final relevance score might be different from the given `relevance`.
   // NOTE: This function should only be called on the UI thread.
   AutocompleteMatch HistoryMatchToACMatch(
       const HistoryURLProviderParams& params,
diff --git a/components/omnibox/browser/local_history_zero_suggest_provider.cc b/components/omnibox/browser/local_history_zero_suggest_provider.cc
index 165a4a92..a16db69 100644
--- a/components/omnibox/browser/local_history_zero_suggest_provider.cc
+++ b/components/omnibox/browser/local_history_zero_suggest_provider.cc
@@ -235,6 +235,13 @@
         /*relevance_from_server=*/false,
         /*input_text=*/base::ASCIIToUTF16(std::string()));
 
+    // Only provide a group ID, as the client does not know the header or the
+    // priority for SuggestionGroupId::kPersonalizedZeroSuggest. The suggestion
+    // group info will either be provided by the server (i.e., on SRP/Web) or
+    // this group ID will be dropped (i.e., on NTP).
+    suggestion.set_suggestion_group_id(
+        SuggestionGroupId::kPersonalizedZeroSuggest);
+
     AutocompleteMatch match = BaseSearchProvider::CreateSearchSuggestion(
         this, input, /*in_keyword_mode=*/false, suggestion,
         template_url_service->GetDefaultSearchProvider(),
diff --git a/components/omnibox/browser/omnibox_edit_model.cc b/components/omnibox/browser/omnibox_edit_model.cc
index 03c930d3..a7a3da6 100644
--- a/components/omnibox/browser/omnibox_edit_model.cc
+++ b/components/omnibox/browser/omnibox_edit_model.cc
@@ -802,6 +802,18 @@
                                  const std::u16string& pasted_text,
                                  size_t index,
                                  base::TimeTicks match_selection_timestamp) {
+  // Switch the window disposition to SWITCH_TO_TAB for open tab matches that
+  // originated while in keyword mode.  This is to support the keyword mode
+  // starter pack's tab search (@tabs) feature, which should open all
+  // suggestions in the existing open tab.
+  bool is_open_tab_match =
+      OmniboxFieldTrial::IsSiteSearchStarterPackEnabled() &&
+      match.from_keyword &&
+      match.provider->type() == AutocompleteProvider::TYPE_OPEN_TAB;
+  if (is_open_tab_match) {
+    disposition = WindowOpenDisposition::SWITCH_TO_TAB;
+  }
+
   TRACE_EVENT("omnibox", "OmniboxEditModel::OpenMatch", "match", match,
               "disposition", disposition, "altenate_nav_url", alternate_nav_url,
               "pasted_text", pasted_text);
@@ -1961,7 +1973,8 @@
               ? omnibox::SuggestionGroupVisibility::SHOWN
               : omnibox::SuggestionGroupVisibility::HIDDEN;
       omnibox::SetSuggestionGroupVisibility(
-          GetPrefService(), match.suggestion_group_id.value(), new_value);
+          GetPrefService(), static_cast<int>(match.suggestion_group_id.value()),
+          new_value);
       break;
     }
     case OmniboxPopupSelection::FOCUSED_BUTTON_TAB_SWITCH:
diff --git a/components/omnibox/browser/omnibox_edit_model_unittest.cc b/components/omnibox/browser/omnibox_edit_model_unittest.cc
index f2c99688..0aaee64 100644
--- a/components/omnibox/browser/omnibox_edit_model_unittest.cc
+++ b/components/omnibox/browser/omnibox_edit_model_unittest.cc
@@ -34,6 +34,7 @@
 #include "components/url_formatter/url_fixer.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/metrics_proto/omnibox_event.pb.h"
+#include "ui/base/window_open_disposition.h"
 #include "ui/gfx/geometry/rect.h"
 
 using metrics::OmniboxEventProto;
@@ -57,6 +58,12 @@
 class OmniboxEditModelTest : public testing::Test {
  public:
   void SetUp() override {
+    // The #omnibox-site-search-starter-pack feature flag has to be enabled
+    // before set up in order for the OpenTabProvider to be initialized (needed
+    // for OpenTabMatch test).
+    base::test::ScopedFeatureList feature_list;
+    feature_list.InitAndEnableFeature(omnibox::kSiteSearchStarterPack);
+
     controller_ = std::make_unique<TestOmniboxEditController>();
     view_ = std::make_unique<TestOmniboxView>(controller_.get());
     view_->SetModel(std::make_unique<TestOmniboxEditModel>(
@@ -734,14 +741,15 @@
   matches[3].has_tab_match = true;
   matches[3].deletable = true;
   // Make match index 4 have a suggestion_group_id to test header behavior.
-  matches[4].suggestion_group_id = 7;
+  const auto kNewGroupId = SuggestionGroupId::kNonPersonalizedZeroSuggest1;
+  matches[4].suggestion_group_id = kNewGroupId;
 
   auto* result = &model()->autocomplete_controller()->result_;
   AutocompleteInput input(u"match", metrics::OmniboxEventProto::NTP,
                           TestSchemeClassifier());
   result->AppendMatches(matches);
   SuggestionGroupsMap suggestion_groups_map;
-  suggestion_groups_map[7].header = u"header";
+  suggestion_groups_map[kNewGroupId].header = u"header";
   result->MergeSuggestionGroupsMap(suggestion_groups_map);
   result->SortAndCull(input, nullptr);
   model()->OnPopupResultChanged();
@@ -815,17 +823,19 @@
   }
 
   // Hide the second two matches.
-  matches[2].suggestion_group_id = 7;
-  matches[3].suggestion_group_id = 7;
+  const auto kNewGroupId = SuggestionGroupId::kNonPersonalizedZeroSuggest1;
+  matches[2].suggestion_group_id = kNewGroupId;
+  matches[3].suggestion_group_id = kNewGroupId;
   omnibox::SetSuggestionGroupVisibility(
-      pref_service(), 7, omnibox::SuggestionGroupVisibility::HIDDEN);
+      pref_service(), static_cast<int>(kNewGroupId),
+      omnibox::SuggestionGroupVisibility::HIDDEN);
 
   auto* result = &model()->autocomplete_controller()->result_;
   AutocompleteInput input(u"match", metrics::OmniboxEventProto::NTP,
                           TestSchemeClassifier());
   result->AppendMatches(matches);
   SuggestionGroupsMap suggestion_groups_map;
-  suggestion_groups_map[7].header = u"header";
+  suggestion_groups_map[kNewGroupId].header = u"header";
   result->MergeSuggestionGroupsMap(suggestion_groups_map);
   result->SortAndCull(input, nullptr);
   model()->OnPopupResultChanged();
@@ -886,14 +896,14 @@
   matches[0].inline_autocompletion = u"1";
   matches[1].fill_into_edit = u"a2";
   matches[2].fill_into_edit = u"a3";
-  matches[2].suggestion_group_id = 7;
-
+  const auto kNewGroupId = SuggestionGroupId::kNonPersonalizedZeroSuggest1;
+  matches[2].suggestion_group_id = kNewGroupId;
   auto* result = &model()->autocomplete_controller()->result_;
   AutocompleteInput input(u"a", metrics::OmniboxEventProto::NTP,
                           TestSchemeClassifier());
   result->AppendMatches(matches);
   SuggestionGroupsMap suggestion_groups_map;
-  suggestion_groups_map[7].header = u"header";
+  suggestion_groups_map[kNewGroupId].header = u"header";
   result->MergeSuggestionGroupsMap(suggestion_groups_map);
   result->SortAndCull(input, nullptr);
   model()->OnPopupResultChanged();
@@ -1174,3 +1184,38 @@
     }
   }
 }
+
+#if !(BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID))
+// The keyword mode feature is only available on Desktop. Do not test on mobile.
+TEST_F(OmniboxEditModelTest, OpenTabMatch) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(omnibox::kSiteSearchStarterPack);
+
+  // When the match comes from the Open Tab Provider while in keyword mode,
+  // the disposition should be set to SWITCH_TO_TAB.
+  AutocompleteMatch match(
+      model()->autocomplete_controller()->open_tab_provider(), 0, false,
+      AutocompleteMatchType::OPEN_TAB);
+  match.destination_url = GURL("https://foo/");
+  match.from_keyword = true;
+
+  model()->OnSetFocus(false);  // Avoids DCHECK in OpenMatch().
+  model()->SetUserText(u"http://abcd");
+  model()->OpenMatch(match, WindowOpenDisposition::CURRENT_TAB, GURL(),
+                     std::u16string(), 0);
+  EXPECT_EQ(controller_->disposition(), WindowOpenDisposition::SWITCH_TO_TAB);
+
+  // Suggestions not from the Open Tab Provider or not from keyword mode should
+  // not change the disposition.
+  match.from_keyword = false;
+  model()->OpenMatch(match, WindowOpenDisposition::CURRENT_TAB, GURL(),
+                     std::u16string(), 0);
+  EXPECT_EQ(controller_->disposition(), WindowOpenDisposition::CURRENT_TAB);
+
+  match.provider = model()->autocomplete_controller()->search_provider();
+  match.from_keyword = true;
+  model()->OpenMatch(match, WindowOpenDisposition::CURRENT_TAB, GURL(),
+                     std::u16string(), 0);
+  EXPECT_EQ(controller_->disposition(), WindowOpenDisposition::CURRENT_TAB);
+}
+#endif  // !(BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID))
diff --git a/components/omnibox/browser/omnibox_field_trial.cc b/components/omnibox/browser/omnibox_field_trial.cc
index 7ae0dc89..94b1eb57 100644
--- a/components/omnibox/browser/omnibox_field_trial.cc
+++ b/components/omnibox/browser/omnibox_field_trial.cc
@@ -739,6 +739,10 @@
     &omnibox::kAutocompleteStability,
     "AutocompleteStabilityDontCopyDoneProviders",
     false);
+const base::FeatureParam<bool> kAutocompleteStabilityAsyncProvidersFirst(
+    &omnibox::kAutocompleteStability,
+    "AutocompleteStabilityAsyncProvidersFirst",
+    false);
 
 // Local history zero-prefix (aka zero-suggest) and prefix suggestions:
 
diff --git a/components/omnibox/browser/omnibox_field_trial.h b/components/omnibox/browser/omnibox_field_trial.h
index 3e76b32..7b15a6a 100644
--- a/components/omnibox/browser/omnibox_field_trial.h
+++ b/components/omnibox/browser/omnibox_field_trial.h
@@ -490,6 +490,10 @@
 // providers whose suggestions are pending have their old matches copied over.
 extern const base::FeatureParam<bool>
     kAutocompleteStabilityDontCopyDoneProviders;
+// Begin async providers before sync providers so their async requests can
+// happen in parallel. This effects only the search, history_url, document, and
+// on device head providers.
+extern const base::FeatureParam<bool> kAutocompleteStabilityAsyncProvidersFirst;
 
 // Local history zero-prefix (aka zero-suggest) and prefix suggestions.
 
diff --git a/components/omnibox/browser/omnibox_popup_selection.cc b/components/omnibox/browser/omnibox_popup_selection.cc
index 59c630d..9f4d979 100644
--- a/components/omnibox/browser/omnibox_popup_selection.cc
+++ b/components/omnibox/browser/omnibox_popup_selection.cc
@@ -67,6 +67,12 @@
     case KEYWORD_MODE:
       return match.associated_keyword != nullptr;
     case FOCUSED_BUTTON_TAB_SWITCH:
+      // The default action for suggestions from the open tab provider in
+      // keyword mode is to switch to the open tab so no button is necessary.
+      if (match.from_keyword &&
+          match.provider->type() == AutocompleteProvider::TYPE_OPEN_TAB) {
+        return false;
+      }
       return match.has_tab_match.value_or(false);
     case FOCUSED_BUTTON_ACTION:
       return match.action != nullptr;
diff --git a/components/omnibox/browser/search_suggestion_parser.cc b/components/omnibox/browser/search_suggestion_parser.cc
index fa022b19..5c53f316 100644
--- a/components/omnibox/browser/search_suggestion_parser.cc
+++ b/components/omnibox/browser/search_suggestion_parser.cc
@@ -10,6 +10,8 @@
 #include <memory>
 
 #include "base/check.h"
+#include "base/containers/contains.h"
+#include "base/containers/fixed_flat_map.h"
 #include "base/i18n/icu_string_conversions.h"
 #include "base/json/json_reader.h"
 #include "base/json/json_string_value_serializer.h"
@@ -113,6 +115,38 @@
 // The field number for the string value in ExperimentStatsV2.
 constexpr char kStringValueFieldNumber[] = "2";
 
+// Used to dynamically convert the server-provided group IDs to those known to
+// Chrome based on the 0-based index of the suggestion groups in the server
+// response.
+constexpr auto kReservedGroupIdsMap =
+    base::MakeFixedFlatMap<int, SuggestionGroupId>(
+        {{0, SuggestionGroupId::kNonPersonalizedZeroSuggest1},
+         {1, SuggestionGroupId::kNonPersonalizedZeroSuggest2},
+         {2, SuggestionGroupId::kNonPersonalizedZeroSuggest3},
+         {3, SuggestionGroupId::kNonPersonalizedZeroSuggest4},
+         {4, SuggestionGroupId::kNonPersonalizedZeroSuggest5},
+         {5, SuggestionGroupId::kNonPersonalizedZeroSuggest6},
+         {6, SuggestionGroupId::kNonPersonalizedZeroSuggest7},
+         {7, SuggestionGroupId::kNonPersonalizedZeroSuggest8},
+         {8, SuggestionGroupId::kNonPersonalizedZeroSuggest9},
+         {9, SuggestionGroupId::kNonPersonalizedZeroSuggest10}});
+
+// Used to dynamically convert the order of suggestion groups in the server
+// response to the group priorities known to Chrome based on the 0-based index
+// of the suggestion groups in the server response.
+constexpr auto kReservedGroupPrioritiesMap =
+    base::MakeFixedFlatMap<int, SuggestionGroupPriority>(
+        {{0, SuggestionGroupPriority::kRemoteZeroSuggest1},
+         {1, SuggestionGroupPriority::kRemoteZeroSuggest2},
+         {2, SuggestionGroupPriority::kRemoteZeroSuggest3},
+         {3, SuggestionGroupPriority::kRemoteZeroSuggest4},
+         {4, SuggestionGroupPriority::kRemoteZeroSuggest5},
+         {5, SuggestionGroupPriority::kRemoteZeroSuggest6},
+         {6, SuggestionGroupPriority::kRemoteZeroSuggest7},
+         {7, SuggestionGroupPriority::kRemoteZeroSuggest8},
+         {8, SuggestionGroupPriority::kRemoteZeroSuggest9},
+         {9, SuggestionGroupPriority::kRemoteZeroSuggest10}});
+
 }  // namespace
 
 // SearchSuggestionParser::Result ----------------------------------------------
@@ -493,6 +527,7 @@
   const base::Value* subtype_identifiers = nullptr;
   int prefetch_index = -1;
   int prerender_index = -1;
+  std::unordered_map<int, SuggestionGroup> parsed_suggestion_groups_map;
 
   if (root_list.size() > 4u && root_list[4].is_dict()) {
     const base::Value& extras = root_list[4];
@@ -554,7 +589,7 @@
         for (auto it : headers->DictItems()) {
           int suggestion_group_id;
           base::StringToInt(it.first, &suggestion_group_id);
-          results->suggestion_groups_map[suggestion_group_id].header =
+          parsed_suggestion_groups_map[suggestion_group_id].header =
               base::UTF8ToUTF16(it.second.GetString());
         }
       }
@@ -562,8 +597,9 @@
       const base::Value* hidden_group_ids = header_texts->FindListKey("h");
       if (hidden_group_ids) {
         for (const auto& value : hidden_group_ids->GetListDeprecated()) {
-          if (value.is_int())
-            results->suggestion_groups_map[value.GetInt()].hidden = true;
+          if (value.is_int()) {
+            parsed_suggestion_groups_map[value.GetInt()].hidden = true;
+          }
         }
       }
     }
@@ -610,6 +646,9 @@
   int relevance = default_result_relevance;
   const std::u16string& trimmed_input =
       base::CollapseWhitespace(input.text(), false);
+  int last_suggestion_group_id = static_cast<int>(SuggestionGroupId::kInvalid);
+  int last_suggestion_group_index = -1;
+
   for (size_t index = 0;
        index < results_list.size() && results_list[index].is_string();
        ++index) {
@@ -752,11 +791,51 @@
           trimmed_input));
 
       if (suggestion_group_id) {
+        if (last_suggestion_group_id != *suggestion_group_id) {
+          // Remember the ID and the 0-based index of the last seen group.
+          last_suggestion_group_id = *suggestion_group_id;
+          last_suggestion_group_index++;
+        }
+
+        // Map the group ID to one known to Chrome. With an exception of the
+        // personalized suggestions whose group ID is known to Chrome, the
+        // mapping is dynamically done based on the 0-based index of the group.
+        SuggestionGroupId mapped_suggestion_group_id =
+            SuggestionGroupId::kInvalid;
+        if (*suggestion_group_id ==
+            static_cast<int>(SuggestionGroupId::kPersonalizedZeroSuggest)) {
+          mapped_suggestion_group_id =
+              SuggestionGroupId::kPersonalizedZeroSuggest;
+        } else if (base::Contains(kReservedGroupIdsMap,
+                                  last_suggestion_group_index)) {
+          mapped_suggestion_group_id =
+              kReservedGroupIdsMap.at(last_suggestion_group_index);
+        } else {
+          continue;
+        }
+
+        // Use the mapped group ID in the result.
         results->suggest_results.back().set_suggestion_group_id(
-            *suggestion_group_id);
+            mapped_suggestion_group_id);
+
+        // Use the mapped group ID to update the suggestion group info, if any.
+        // It is possible for a suggestion to specify a group ID for which there
+        // are no header or default visibility information available. Such group
+        // IDs are generally stripped away later on.
+        if (base::Contains(parsed_suggestion_groups_map,
+                           *suggestion_group_id)) {
+          results->suggestion_groups_map[mapped_suggestion_group_id].MergeFrom(
+              parsed_suggestion_groups_map[*suggestion_group_id]);
+          results->suggestion_groups_map[mapped_suggestion_group_id]
+              .original_group_id = *suggestion_group_id;
+          results->suggestion_groups_map[mapped_suggestion_group_id].priority =
+              kReservedGroupPrioritiesMap.at(last_suggestion_group_index);
+        }
       }
-      if (answer_parsed_successfully)
+
+      if (answer_parsed_successfully) {
         results->suggest_results.back().SetAnswer(answer);
+      }
     }
   }
   results->relevances_from_server = relevances != nullptr;
diff --git a/components/omnibox/browser/search_suggestion_parser.h b/components/omnibox/browser/search_suggestion_parser.h
index 3beb8b2..78c377f 100644
--- a/components/omnibox/browser/search_suggestion_parser.h
+++ b/components/omnibox/browser/search_suggestion_parser.h
@@ -163,10 +163,10 @@
       return additional_query_params_;
     }
 
-    void set_suggestion_group_id(int suggestion_group_id) {
+    void set_suggestion_group_id(SuggestionGroupId suggestion_group_id) {
       suggestion_group_id_ = suggestion_group_id;
     }
-    absl::optional<int> suggestion_group_id() const {
+    absl::optional<SuggestionGroupId> suggestion_group_id() const {
       return suggestion_group_id_;
     }
 
@@ -214,9 +214,9 @@
     // suggestion_config.proto. Used to look up the suggestion group info this
     // suggestion belong to such as the header text this suggestion must appear
     // under.
-    // Note: Use kInvalidSuggestionGroupId in place of a missing suggestion
+    // Note: Use SuggestionGroupId::kInvalid in place of a missing suggestion
     // group Id when this is to be converted to a primitive type.
-    absl::optional<int> suggestion_group_id_;
+    absl::optional<SuggestionGroupId> suggestion_group_id_;
 
     // Optional short answer to the input that produced this suggestion.
     absl::optional<SuggestionAnswer> answer_;
diff --git a/components/omnibox/browser/search_suggestion_parser_unittest.cc b/components/omnibox/browser/search_suggestion_parser_unittest.cc
index a9a6923..cd1fd09 100644
--- a/components/omnibox/browser/search_suggestion_parser_unittest.cc
+++ b/components/omnibox/browser/search_suggestion_parser_unittest.cc
@@ -323,11 +323,16 @@
   EXPECT_EQ(kNone, result.match_contents_class());
 }
 
-TEST(SearchSuggestionParserTest, ParseHeaderInfo) {
-  std::string json_data = R"([
+TEST(SearchSuggestionParserTest, ParseSuggestionGroupInfo) {
+  TestSchemeClassifier scheme_classifier;
+  AutocompleteInput input(u"", metrics::OmniboxEventProto::NTP_REALBOX,
+                          scheme_classifier);
+
+  {
+    std::string json_data = R"([
       "",
       ["los angeles", "san diego", "las vegas", "san francisco"],
-      ["history", "", "", ""],
+      ["", "history", "", ""],
       [],
       {
         "google:clientdata": {
@@ -336,8 +341,214 @@
         },
         "google:headertexts":{
           "a":{
-            "40007":"Not recommended for you",
-            "40008":"Recommended for you"
+            "40000":"Recent Searches",
+            "40008":"Recommended for you",
+            "garbage_non_int":"NOT RECOMMENDED FOR YOU"
+          },
+          "h":[40000, "40008", "garbage_non_int"]
+        },
+        "google:suggestdetail":[
+          {
+          },
+          {
+            "zl":40000
+          },
+          {
+            "zl":40008
+          },
+          {
+            "zl":40009
+          }
+        ],
+        "google:suggestrelevance": [607, 606, 605, 604],
+        "google:suggesttype": ["QUERY", "PERSONALIZED_QUERY", "QUERY", "QUERY"]
+      }])";
+    absl::optional<base::Value> root_val = base::JSONReader::Read(json_data);
+    ASSERT_TRUE(root_val);
+
+    SearchSuggestionParser::Results results;
+    ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
+        *root_val, input, scheme_classifier, /*default_result_relevance=*/400,
+        /*is_keyword_result=*/false, &results));
+
+    // Suggestion group headers, original group ids, priorities, and default
+    // visibilities are correctly parsed and populated.
+    ASSERT_EQ(2U, results.suggestion_groups_map.size());
+
+    ASSERT_EQ(
+        u"Recent Searches",
+        results
+            .suggestion_groups_map[SuggestionGroupId::kPersonalizedZeroSuggest]
+            .header);
+    ASSERT_EQ(
+        40000,
+        results
+            .suggestion_groups_map[SuggestionGroupId::kPersonalizedZeroSuggest]
+            .original_group_id.value());
+    ASSERT_EQ(
+        SuggestionGroupPriority::kRemoteZeroSuggest1,
+        results
+            .suggestion_groups_map[SuggestionGroupId::kPersonalizedZeroSuggest]
+            .priority);
+    ASSERT_TRUE(
+        results
+            .suggestion_groups_map[SuggestionGroupId::kPersonalizedZeroSuggest]
+            .hidden);
+
+    ASSERT_EQ(u"Recommended for you",
+              results
+                  .suggestion_groups_map
+                      [SuggestionGroupId::kNonPersonalizedZeroSuggest2]
+                  .header);
+    ASSERT_EQ(40008, results
+                         .suggestion_groups_map
+                             [SuggestionGroupId::kNonPersonalizedZeroSuggest2]
+                         .original_group_id.value());
+    ASSERT_FALSE(results
+                     .suggestion_groups_map
+                         [SuggestionGroupId::kNonPersonalizedZeroSuggest2]
+                     .hidden);
+    ASSERT_EQ(SuggestionGroupPriority::kRemoteZeroSuggest2,
+              results
+                  .suggestion_groups_map
+                      [SuggestionGroupId::kNonPersonalizedZeroSuggest2]
+                  .priority);
+
+    ASSERT_EQ(u"los angeles", results.suggest_results[0].suggestion());
+    // This suggestion does not belong to a group.
+    ASSERT_EQ(absl::nullopt, results.suggest_results[0].suggestion_group_id());
+
+    ASSERT_EQ(u"san diego", results.suggest_results[1].suggestion());
+    ASSERT_EQ(SuggestionGroupId::kPersonalizedZeroSuggest,
+              *results.suggest_results[1].suggestion_group_id());
+
+    ASSERT_EQ(u"las vegas", results.suggest_results[2].suggestion());
+    ASSERT_EQ(SuggestionGroupId::kNonPersonalizedZeroSuggest2,
+              *results.suggest_results[2].suggestion_group_id());
+
+    ASSERT_EQ(u"san francisco", results.suggest_results[3].suggestion());
+    ASSERT_EQ(SuggestionGroupId::kNonPersonalizedZeroSuggest3,
+              results.suggest_results[3].suggestion_group_id());
+  }
+  {
+    std::string json_data = R"([
+      "",
+      ["los angeles", "san diego", "las vegas", "san francisco"],
+      ["", "", "history", ""],
+      [],
+      {
+        "google:clientdata": {
+          "bpc": false,
+          "tlw": false
+        },
+        "google:headertexts":{
+          "a":{
+            "40000":"Recent Searches",
+            "40008":"Recommended for you",
+            "garbage_non_int":"NOT RECOMMENDED FOR YOU"
+          },
+          "h":[40000, "40008", "garbage_non_int"]
+        },
+        "google:suggestdetail":[
+          {
+            "zl":40008
+          },
+          {
+            "zl":40008
+          },
+          {
+            "zl":40000
+          },
+          {
+            "zl":40009
+          }
+        ],
+        "google:suggestrelevance": [607, 606, 605, 604],
+        "google:suggesttype": ["QUERY", "QUERY", "PERSONALIZED_QUERY", "QUERY"]
+      }])";
+    absl::optional<base::Value> root_val = base::JSONReader::Read(json_data);
+    ASSERT_TRUE(root_val);
+
+    SearchSuggestionParser::Results results;
+    ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
+        *root_val, input, scheme_classifier, /*default_result_relevance=*/400,
+        /*is_keyword_result=*/false, &results));
+
+    // Suggestion group headers, original group ids, priorities, and default
+    // visibilities are correctly parsed and populated.
+    ASSERT_EQ(2U, results.suggestion_groups_map.size());
+
+    ASSERT_EQ(u"Recommended for you",
+              results
+                  .suggestion_groups_map
+                      [SuggestionGroupId::kNonPersonalizedZeroSuggest1]
+                  .header);
+    ASSERT_EQ(40008, results
+                         .suggestion_groups_map
+                             [SuggestionGroupId::kNonPersonalizedZeroSuggest1]
+                         .original_group_id.value());
+    ASSERT_FALSE(results
+                     .suggestion_groups_map
+                         [SuggestionGroupId::kNonPersonalizedZeroSuggest1]
+                     .hidden);
+    ASSERT_EQ(SuggestionGroupPriority::kRemoteZeroSuggest1,
+              results
+                  .suggestion_groups_map
+                      [SuggestionGroupId::kNonPersonalizedZeroSuggest1]
+                  .priority);
+
+    ASSERT_EQ(
+        u"Recent Searches",
+        results
+            .suggestion_groups_map[SuggestionGroupId::kPersonalizedZeroSuggest]
+            .header);
+    ASSERT_EQ(
+        40000,
+        results
+            .suggestion_groups_map[SuggestionGroupId::kPersonalizedZeroSuggest]
+            .original_group_id.value());
+    ASSERT_EQ(
+        SuggestionGroupPriority::kRemoteZeroSuggest2,
+        results
+            .suggestion_groups_map[SuggestionGroupId::kPersonalizedZeroSuggest]
+            .priority);
+    ASSERT_TRUE(
+        results
+            .suggestion_groups_map[SuggestionGroupId::kPersonalizedZeroSuggest]
+            .hidden);
+
+    ASSERT_EQ(u"los angeles", results.suggest_results[0].suggestion());
+    ASSERT_EQ(SuggestionGroupId::kNonPersonalizedZeroSuggest1,
+              *results.suggest_results[0].suggestion_group_id());
+
+    ASSERT_EQ(u"san diego", results.suggest_results[1].suggestion());
+    ASSERT_EQ(SuggestionGroupId::kNonPersonalizedZeroSuggest1,
+              *results.suggest_results[1].suggestion_group_id());
+
+    ASSERT_EQ(u"las vegas", results.suggest_results[2].suggestion());
+    ASSERT_EQ(SuggestionGroupId::kPersonalizedZeroSuggest,
+              *results.suggest_results[2].suggestion_group_id());
+
+    ASSERT_EQ(u"san francisco", results.suggest_results[3].suggestion());
+    ASSERT_EQ(SuggestionGroupId::kNonPersonalizedZeroSuggest3,
+              results.suggest_results[3].suggestion_group_id());
+  }
+  {
+    std::string json_data = R"([
+      "",
+      ["los angeles", "san diego", "las vegas", "san francisco"],
+      ["", "", "", "history"],
+      [],
+      {
+        "google:clientdata": {
+          "bpc": false,
+          "tlw": false
+        },
+        "google:headertexts":{
+          "a":{
+            "40007":"Related Searches",
+            "40008":"Recommended for you",
+            "garbage_non_int":"NOT RECOMMENDED FOR YOU"
           },
           "h":[40007, "40008", "garbage_non_int"]
         },
@@ -351,45 +562,77 @@
             "zl":40008
           },
           {
-            "zl":40009
+            "zl":40000
           }
         ],
         "google:suggestrelevance": [607, 606, 605, 604],
-        "google:suggesttype": ["PERSONALIZED_QUERY", "QUERY", "QUERY", "QUERY"]
+        "google:suggesttype": ["QUERY", "QUERY", "QUERY", "PERSONALIZED_QUERY"]
       }])";
-  absl::optional<base::Value> root_val = base::JSONReader::Read(json_data);
-  ASSERT_TRUE(root_val);
-  TestSchemeClassifier scheme_classifier;
-  AutocompleteInput input(u"", metrics::OmniboxEventProto::NTP_REALBOX,
-                          scheme_classifier);
-  SearchSuggestionParser::Results results;
-  ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
-      *root_val, input, scheme_classifier, /*default_result_relevance=*/400,
-      /*is_keyword_result=*/false, &results));
+    absl::optional<base::Value> root_val = base::JSONReader::Read(json_data);
+    ASSERT_TRUE(root_val);
 
-  // Parse integers, and only integers, out of the "h" metadata list.
-  ASSERT_TRUE(results.suggestion_groups_map[40007].hidden);
+    SearchSuggestionParser::Results results;
+    ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
+        *root_val, input, scheme_classifier, /*default_result_relevance=*/400,
+        /*is_keyword_result=*/false, &results));
 
-  {
-    const auto& suggestion_result = results.suggest_results[0];
-    ASSERT_EQ(u"los angeles", suggestion_result.suggestion());
+    // Suggestion group headers, original group ids, priorities, and default
+    // visibilities are correctly parsed and populated.
+    ASSERT_EQ(2U, results.suggestion_groups_map.size());
+
+    ASSERT_EQ(u"Related Searches",
+              results
+                  .suggestion_groups_map
+                      [SuggestionGroupId::kNonPersonalizedZeroSuggest1]
+                  .header);
+    ASSERT_EQ(40007, results
+                         .suggestion_groups_map
+                             [SuggestionGroupId::kNonPersonalizedZeroSuggest1]
+                         .original_group_id.value());
+    ASSERT_EQ(SuggestionGroupPriority::kRemoteZeroSuggest1,
+              results
+                  .suggestion_groups_map
+                      [SuggestionGroupId::kNonPersonalizedZeroSuggest1]
+                  .priority);
+    ASSERT_TRUE(results
+                    .suggestion_groups_map
+                        [SuggestionGroupId::kNonPersonalizedZeroSuggest1]
+                    .hidden);
+
+    ASSERT_EQ(u"Recommended for you",
+              results
+                  .suggestion_groups_map
+                      [SuggestionGroupId::kNonPersonalizedZeroSuggest2]
+                  .header);
+    ASSERT_EQ(40008, results
+                         .suggestion_groups_map
+                             [SuggestionGroupId::kNonPersonalizedZeroSuggest2]
+                         .original_group_id.value());
+    ASSERT_FALSE(results
+                     .suggestion_groups_map
+                         [SuggestionGroupId::kNonPersonalizedZeroSuggest2]
+                     .hidden);
+    ASSERT_EQ(SuggestionGroupPriority::kRemoteZeroSuggest2,
+              results
+                  .suggestion_groups_map
+                      [SuggestionGroupId::kNonPersonalizedZeroSuggest2]
+                  .priority);
+
+    ASSERT_EQ(u"los angeles", results.suggest_results[0].suggestion());
     // This suggestion does not belong to a group.
-    ASSERT_EQ(absl::nullopt, suggestion_result.suggestion_group_id());
-  }
-  {
-    const auto& suggestion_result = results.suggest_results[1];
-    ASSERT_EQ(u"san diego", suggestion_result.suggestion());
-    ASSERT_EQ(40007, *suggestion_result.suggestion_group_id());
-  }
-  {
-    const auto& suggestion_result = results.suggest_results[2];
-    ASSERT_EQ(u"las vegas", suggestion_result.suggestion());
-    ASSERT_EQ(40008, *suggestion_result.suggestion_group_id());
-  }
-  {
-    const auto& suggestion_result = results.suggest_results[3];
-    ASSERT_EQ(u"san francisco", suggestion_result.suggestion());
-    ASSERT_EQ(40009, *suggestion_result.suggestion_group_id());
+    ASSERT_EQ(absl::nullopt, results.suggest_results[0].suggestion_group_id());
+
+    ASSERT_EQ(u"san diego", results.suggest_results[1].suggestion());
+    ASSERT_EQ(SuggestionGroupId::kNonPersonalizedZeroSuggest1,
+              *results.suggest_results[1].suggestion_group_id());
+
+    ASSERT_EQ(u"las vegas", results.suggest_results[2].suggestion());
+    ASSERT_EQ(SuggestionGroupId::kNonPersonalizedZeroSuggest2,
+              *results.suggest_results[2].suggestion_group_id());
+
+    ASSERT_EQ(u"san francisco", results.suggest_results[3].suggestion());
+    ASSERT_EQ(SuggestionGroupId::kPersonalizedZeroSuggest,
+              results.suggest_results[3].suggestion_group_id());
   }
 }
 
diff --git a/components/omnibox/browser/suggestion_group.cc b/components/omnibox/browser/suggestion_group.cc
index 7090588..0f16404 100644
--- a/components/omnibox/browser/suggestion_group.cc
+++ b/components/omnibox/browser/suggestion_group.cc
@@ -4,18 +4,27 @@
 
 #include "components/omnibox/browser/suggestion_group.h"
 
-// Value chosen based on SuggestionGroupIds::INVALID in suggestion_config.proto.
-const int kInvalidSuggestionGroupId = -1;
-
 void SuggestionGroup::MergeFrom(const SuggestionGroup& suggestion_group) {
+  // Only update the priority if not previously set.
+  if (priority == SuggestionGroupPriority::kDefault) {
+    priority = suggestion_group.priority;
+  }
   // Only update the header if not previously set.
   if (header.empty()) {
     header = suggestion_group.header;
   }
+  // Only update the server group ID if not previously set and given group has
+  // a value.
+  if (!original_group_id.has_value() &&
+      suggestion_group.original_group_id.has_value()) {
+    original_group_id = *suggestion_group.original_group_id;
+  }
   hidden = suggestion_group.hidden;
 }
 
 void SuggestionGroup::Clear() {
+  priority = SuggestionGroupPriority::kDefault;
   header.clear();
+  original_group_id.reset();
   hidden = false;
 }
diff --git a/components/omnibox/browser/suggestion_group.h b/components/omnibox/browser/suggestion_group.h
index 36be1b0..2c2954d 100644
--- a/components/omnibox/browser/suggestion_group.h
+++ b/components/omnibox/browser/suggestion_group.h
@@ -5,11 +5,73 @@
 #ifndef COMPONENTS_OMNIBOX_BROWSER_SUGGESTION_GROUP_H_
 #define COMPONENTS_OMNIBOX_BROWSER_SUGGESTION_GROUP_H_
 
-#include <map>
 #include <string>
+#include <unordered_map>
 
-// Indicates an invalid suggestion group Id.
-extern const int kInvalidSuggestionGroupId;
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+// Determines the order in which suggestion groups appear in the final displayed
+// list relative to one another. A higher numeric value places a given group
+// towards the bottom of the suggestion list relative to the other groups with
+// lower priority numeric values.
+//
+// Use a fixed underlying int type for this enum to ensure that the 0-based
+// integer indices of the remote zero-prefix suggestions can be safely converted
+// to this enum type.
+enum class SuggestionGroupPriority : int {
+  // The default suggestion group priority. Any suggestion with this priority is
+  // placed above the remote zero-prefix suggestions (see below).
+  kDefault = 0,
+  // Reserved for remote zero-prefix suggestions. The priorities are dynamically
+  // assigned to the groups found in the server response based on the 0-based
+  // index of the first zero-prefix suggestion in the group.
+  kRemoteZeroSuggest1 = 1,
+  kRemoteZeroSuggest2 = 2,
+  kRemoteZeroSuggest3 = 3,
+  kRemoteZeroSuggest4 = 4,
+  kRemoteZeroSuggest5 = 5,
+  kRemoteZeroSuggest6 = 6,
+  kRemoteZeroSuggest7 = 7,
+  kRemoteZeroSuggest8 = 8,
+  kRemoteZeroSuggest9 = 9,
+  kRemoteZeroSuggest10 = 10,
+};
+
+// These values uniquely identify the suggestion groups in SuggestionGroupsMap.
+//
+// Use a fixed underlying int type for this enum to ensure its values can be
+// safely converted to the equivalent Java and Mojom types in Android and WebUI.
+enum class SuggestionGroupId : int {
+  // SuggestionGroupIds::INVALID in suggestion_config.proto.
+  kInvalid = -1,
+  // Reserved for non-personalized zero-prefix suggestions. These values don't
+  // match the reserved range for these suggestions in suggestion_config.proto.
+  // Produced by SearchSuggestionParser.
+  kNonPersonalizedZeroSuggest1 = 10000,
+  kNonPersonalizedZeroSuggest2 = 10001,
+  kNonPersonalizedZeroSuggest3 = 10002,
+  kNonPersonalizedZeroSuggest4 = 10003,
+  kNonPersonalizedZeroSuggest5 = 10004,
+  kNonPersonalizedZeroSuggest6 = 10005,
+  kNonPersonalizedZeroSuggest7 = 10006,
+  kNonPersonalizedZeroSuggest8 = 10007,
+  kNonPersonalizedZeroSuggest9 = 10008,
+  kNonPersonalizedZeroSuggest10 = 10009,
+  // SuggestionGroupIds::PERSONALIZED_HISTORY_GROUP in suggestion_config.proto.
+  // Found in server response. Also Produced by LocalHistoryZeroSuggestProvider.
+  kPersonalizedZeroSuggest = 40000,
+
+  // Produced by HistoryClusterProvider.
+  kHistoryCluster = 100000,
+};
+
+// This allows using SuggestionGroupId as the key in SuggestionGroupsMap.
+struct SuggestionGroupIdHash {
+  template <typename T>
+  int operator()(T t) const {
+    return static_cast<int>(t);
+  }
+};
 
 // Contains the information about the suggestion groups.
 struct SuggestionGroup {
@@ -21,12 +83,20 @@
   void MergeFrom(const SuggestionGroup& suggestion_group);
   void Clear();
 
-  // Group header provided by the server.
+  // Determines how this group is placed in the final list of suggestions with
+  // relative to the other groups.
+  // Inferred from the server response for remote zero-prefix suggestions.
+  SuggestionGroupPriority priority{SuggestionGroupPriority::kDefault};
+  // The original group ID provided by the server, if applicable.
+  absl::optional<int> original_group_id;
+  // Group header provided by the server, if applicable.
   std::u16string header{u""};
-  // Whether the group should be hidden by default.
+  // Default visibility provided by the server, if applicable.
   bool hidden{false};
 };
 
-using SuggestionGroupsMap = std::map<int, SuggestionGroup>;
+// A map of SuggestionGroupId to SuggestionGroup.
+using SuggestionGroupsMap = std::
+    unordered_map<SuggestionGroupId, SuggestionGroup, SuggestionGroupIdHash>;
 
 #endif  // COMPONENTS_OMNIBOX_BROWSER_SUGGESTION_GROUP_H_
diff --git a/components/omnibox/browser/test_omnibox_edit_controller.cc b/components/omnibox/browser/test_omnibox_edit_controller.cc
index 3b6c414..4597e7d 100644
--- a/components/omnibox/browser/test_omnibox_edit_controller.cc
+++ b/components/omnibox/browser/test_omnibox_edit_controller.cc
@@ -30,4 +30,5 @@
       match, alternative_nav_match);
 
   alternate_nav_match_ = alternative_nav_match;
+  disposition_ = disposition;
 }
diff --git a/components/omnibox/browser/test_omnibox_edit_controller.h b/components/omnibox/browser/test_omnibox_edit_controller.h
index ab5cc30..54b0ee4 100644
--- a/components/omnibox/browser/test_omnibox_edit_controller.h
+++ b/components/omnibox/browser/test_omnibox_edit_controller.h
@@ -8,6 +8,7 @@
 #include "components/omnibox/browser/autocomplete_match.h"
 #include "components/omnibox/browser/omnibox_edit_controller.h"
 #include "components/omnibox/browser/test_location_bar_model.h"
+#include "ui/base/window_open_disposition.h"
 
 class TestOmniboxEditController : public OmniboxEditController {
  public:
@@ -35,11 +36,14 @@
     return alternate_nav_match_;
   }
 
+  const WindowOpenDisposition& disposition() const { return disposition_; }
+
   using OmniboxEditController::destination_url;
 
  private:
   TestLocationBarModel location_bar_model_;
   AutocompleteMatch alternate_nav_match_;
+  WindowOpenDisposition disposition_;
 };
 
 #endif  // COMPONENTS_OMNIBOX_BROWSER_TEST_OMNIBOX_EDIT_CONTROLLER_H_
diff --git a/components/omnibox/browser/zero_suggest_provider.cc b/components/omnibox/browser/zero_suggest_provider.cc
index 5f8b621..670b69994 100644
--- a/components/omnibox/browser/zero_suggest_provider.cc
+++ b/components/omnibox/browser/zero_suggest_provider.cc
@@ -175,6 +175,23 @@
          (!check_authentication_state || client->IsAuthenticated());
 }
 
+// Returns a sanitized copy of |input|. For zero-suggest, input is expected to
+// empty, as it is checked against the suggest response which always has an
+// empty query. If those don't match, the response is dropped. Ensures the input
+// text is empty. However copies over the URL. Zero-suggest on Web/SRP on Mobile
+// relies on the URL to be set.
+// TODO(crbug.com/1344004): Find out if the other fields also need to be set.
+AutocompleteInput GetSanitizedInput(const AutocompleteInput& input,
+                                    const AutocompleteProviderClient* client) {
+  AutocompleteInput sanitized_input(u"", input.current_page_classification(),
+                                    client->GetSchemeClassifier());
+  sanitized_input.set_current_url(input.current_url());
+  sanitized_input.set_current_title(input.current_title());
+  sanitized_input.set_prevent_inline_autocomplete(true);
+  sanitized_input.set_allow_exact_keyword_match(false);
+  return sanitized_input;
+}
+
 }  // namespace
 
 // static
@@ -274,18 +291,44 @@
                                std::string());
 }
 
+void ZeroSuggestProvider::StartPrefetch(const AutocompleteInput& input) {
+  TRACE_EVENT0("omnibox", "ZeroSuggestProvider::StartPrefetch");
+
+  ResultType result_type;
+  if (!AllowZeroPrefixSuggestions(client(), input, &result_type)) {
+    return;
+  }
+
+  // Do not start a request if async requests are disallowed.
+  if (input.omit_asynchronous_matches()) {
+    return;
+  }
+
+  if (prefetch_loader_) {
+    LogEvent(Event::kRequestInvalidated, result_type, /*is_prefetch=*/true);
+  }
+
+  // Create a loader for the request and take ownership of it.
+  TemplateURLRef::SearchTermsArgs search_terms_args;
+  search_terms_args.page_classification = input.current_page_classification();
+  search_terms_args.focus_type = input.focus_type();
+  search_terms_args.current_page_url = result_type == REMOTE_SEND_URL
+                                           ? input.current_url().spec()
+                                           : std::string();
+  prefetch_loader_ =
+      client()
+          ->GetRemoteSuggestionsService(/*create_if_necessary=*/true)
+          ->StartSuggestionsRequest(
+              search_terms_args, client()->GetTemplateURLService(),
+              base::BindOnce(&ZeroSuggestProvider::OnPrefetchURLLoadComplete,
+                             weak_ptr_factory_.GetWeakPtr(),
+                             GetSanitizedInput(input, client()), result_type));
+
+  LogEvent(Event::kRequestSent, result_type, /*is_prefetch=*/true);
+}
+
 void ZeroSuggestProvider::Start(const AutocompleteInput& input,
                                 bool minimal_changes) {
-  Start(input, minimal_changes, /*is_prefetch=*/false);
-}
-
-void ZeroSuggestProvider::StartPrefetch(const AutocompleteInput& input) {
-  Start(input, /*minimal_changes=*/false, /*is_prefetch=*/true);
-}
-
-void ZeroSuggestProvider::Start(const AutocompleteInput& input,
-                                bool minimal_changes,
-                                bool is_prefetch) {
   TRACE_EVENT0("omnibox", "ZeroSuggestProvider::Start");
   Stop(true, false);
 
@@ -298,52 +341,38 @@
   set_field_trial_triggered(false);
   set_field_trial_triggered_in_session(false);
 
-  TemplateURLRef::SearchTermsArgs search_terms_args;
-  search_terms_args.page_classification = input.current_page_classification();
-  search_terms_args.focus_type = input.focus_type();
-  GURL suggest_url = RemoteSuggestionsService::EndpointUrl(
-      search_terms_args, client()->GetTemplateURLService());
-  if (!suggest_url.is_valid())
-    return;
-
-  if (is_prefetch) {
-    prefetch_done_ = false;
-  } else {
-    done_ = false;
-
-    // Prefetching is only meant to update the stored response with a fresh
-    // response. It is unnecessary to convert it to the displayed matches when
-    // prefetching; as they will get cleared on the next call to `Start()`.
-    auto response_data = ReadStoredResponse();
-    if (response_data) {
-      if (ConvertResponseToAutocompleteMatches(std::move(response_data))) {
-        LogEvent(Event::KCachedResponseConvertedToMatches, result_type_running_,
-                 is_prefetch);
-      }
+  // Convert the stored response to |matches_|, if applicable.
+  auto response_data = ReadStoredResponse(result_type_running_);
+  if (response_data) {
+    if (ConvertResponseToAutocompleteMatches(std::move(response_data))) {
+      LogEvent(Event::KCachedResponseConvertedToMatches, result_type_running_,
+               /*is_prefetch=*/false);
     }
   }
 
+  // Do not start a request if async requests are disallowed.
   if (input.omit_asynchronous_matches()) {
-    // Asynchronous provider logic should only be omitted during non-prefetch
-    // ZPS requests.
-    DCHECK(!is_prefetch);
-    Stop(true, false);
     return;
   }
 
+  done_ = false;
+
+  // Create a loader for the request and take ownership of it.
+  TemplateURLRef::SearchTermsArgs search_terms_args;
+  search_terms_args.page_classification = input.current_page_classification();
+  search_terms_args.focus_type = input.focus_type();
   search_terms_args.current_page_url = result_type_running_ == REMOTE_SEND_URL
                                            ? input.current_url().spec()
                                            : std::string();
-  loader_ =
-      client()
-          ->GetRemoteSuggestionsService(/*create_if_necessary=*/true)
-          ->StartSuggestionsRequest(
-              search_terms_args, client()->GetTemplateURLService(),
-              base::BindOnce(&ZeroSuggestProvider::OnURLLoadComplete,
-                             weak_ptr_factory_.GetWeakPtr(), is_prefetch));
+  loader_ = client()
+                ->GetRemoteSuggestionsService(/*create_if_necessary=*/true)
+                ->StartSuggestionsRequest(
+                    search_terms_args, client()->GetTemplateURLService(),
+                    base::BindOnce(&ZeroSuggestProvider::OnURLLoadComplete,
+                                   weak_ptr_factory_.GetWeakPtr(),
+                                   result_type_running_));
 
-  LogEvent(Event::kRequestSent, result_type_running_,
-           /*is_prefetch=*/!prefetch_done_);
+  LogEvent(Event::kRequestSent, result_type_running_, /*is_prefetch=*/false);
 }
 
 void ZeroSuggestProvider::Stop(bool clear_cached_results,
@@ -352,11 +381,9 @@
 
   if (loader_) {
     LogEvent(Event::kRequestInvalidated, result_type_running_,
-             /*is_prefetch=*/!prefetch_done_);
+             /*is_prefetch=*/false);
+    loader_.reset();
   }
-  loader_.reset();
-  prefetch_done_ = true;
-  result_type_running_ = NONE;
 
   if (clear_cached_results) {
     experiment_stats_v2s_.clear();
@@ -389,8 +416,7 @@
 
 ZeroSuggestProvider::ZeroSuggestProvider(AutocompleteProviderClient* client,
                                          AutocompleteProviderListener* listener)
-    : BaseSearchProvider(AutocompleteProvider::TYPE_ZERO_SUGGEST, client),
-      result_type_running_(NONE) {
+    : BaseSearchProvider(AutocompleteProvider::TYPE_ZERO_SUGGEST, client) {
   AddListener(listener);
 }
 
@@ -403,19 +429,7 @@
 }
 
 const AutocompleteInput ZeroSuggestProvider::GetInput(bool is_keyword) const {
-  // In zero-suggest, input is expected to empty, as it is checked against the
-  // suggest response which always has an empty query. If those don't match,
-  // the response is dropped. Ensure the input text is empty. However copy
-  // over the URL. on-focus zero-suggest on Web/SRP on Mobile relies on the
-  // URL to be set.
-  AutocompleteInput input(std::u16string(),
-                          input_.current_page_classification(),
-                          client()->GetSchemeClassifier());
-  input.set_current_url(input_.current_url());
-  input.set_current_title(input_.current_title());
-  input.set_prevent_inline_autocomplete(true);
-  input.set_allow_exact_keyword_match(false);
-  return input;
+  return GetSanitizedInput(input_, client());
 }
 
 bool ZeroSuggestProvider::ShouldAppendExtraParams(
@@ -435,10 +449,10 @@
 }
 
 void ZeroSuggestProvider::OnURLLoadComplete(
-    bool is_prefetch,
+    ResultType result_type,
     const network::SimpleURLLoader* source,
     std::unique_ptr<std::string> response_body) {
-  DCHECK(!done_ || !prefetch_done_);
+  DCHECK(!done_);
   DCHECK_EQ(loader_.get(), source);
 
   std::unique_ptr<base::Value> response_data = nullptr;
@@ -448,35 +462,56 @@
       (source->ResponseInfo() && source->ResponseInfo()->headers &&
        source->ResponseInfo()->headers->response_code() == 200);
   if (response_received) {
-    LogEvent(Event::kRemoteResponseReceived, result_type_running_, is_prefetch);
-    response_data = StoreRemoteResponse(SearchSuggestionParser::ExtractJsonData(
-                                            source, std::move(response_body)),
-                                        is_prefetch);
+    LogEvent(Event::kRemoteResponseReceived, result_type,
+             /*is_prefetch=*/false);
+    response_data =
+        StoreRemoteResponse(SearchSuggestionParser::ExtractJsonData(
+                                source, std::move(response_body)),
+                            GetInput(/*is_keyword=*/false), result_type,
+                            /*is_prefetch=*/false);
   }
 
-  // Prefetching is only meant to update the stored response with a fresh
-  // response. It is unnecessary to convert the prefetched response to the
-  // displayed matches; as they will get cleared on the next call to `Start()`.
-  const bool matches_updated = response_data && !is_prefetch;
-  if (matches_updated) {
+  // Convert the response to |matches_|, if applicable.
+  if (response_data) {
     if (ConvertResponseToAutocompleteMatches(std::move(response_data))) {
-      LogEvent(Event::kRemoteResponseConvertedToMatches, result_type_running_,
-               is_prefetch);
+      LogEvent(Event::kRemoteResponseConvertedToMatches, result_type,
+               /*is_prefetch=*/false);
     }
   }
 
   loader_.reset();
-  prefetch_done_ = true;
   done_ = true;
-  result_type_running_ = NONE;
 
   // Do not notify the provider listener for prefetch requests.
-  if (!is_prefetch)
-    NotifyListeners(matches_updated);
+  NotifyListeners(!!response_data);
+}
+
+void ZeroSuggestProvider::OnPrefetchURLLoadComplete(
+    const AutocompleteInput& input,
+    ResultType result_type,
+    const network::SimpleURLLoader* source,
+    std::unique_ptr<std::string> response_body) {
+  DCHECK_EQ(prefetch_loader_.get(), source);
+
+  const bool response_received =
+      response_body && source->NetError() == net::OK &&
+      (source->ResponseInfo() && source->ResponseInfo()->headers &&
+       source->ResponseInfo()->headers->response_code() == 200);
+  if (response_received) {
+    LogEvent(Event::kRemoteResponseReceived, result_type, /*is_prefetch=*/true);
+    StoreRemoteResponse(SearchSuggestionParser::ExtractJsonData(
+                            source, std::move(response_body)),
+                        input, result_type,
+                        /*is_prefetch=*/true);
+  }
+
+  prefetch_loader_.reset();
 }
 
 std::unique_ptr<base::Value> ZeroSuggestProvider::StoreRemoteResponse(
     const std::string& response_json,
+    const AutocompleteInput& input,
+    ResultType result_type,
     bool is_prefetch) {
   if (response_json.empty()) {
     return nullptr;
@@ -489,16 +524,18 @@
   }
 
   SearchSuggestionParser::Results results;
-  if (!ParseSuggestResults(*response_data, kDefaultZeroSuggestRelevance,
-                           /*is_keyword_result=*/false, &results)) {
+  if (!SearchSuggestionParser::ParseSuggestResults(
+          *response_data, GetSanitizedInput(input, client()),
+          client()->GetSchemeClassifier(), kDefaultZeroSuggestRelevance,
+          /*is_keyword_result=*/false, &results)) {
     return nullptr;
   }
 
   // Store the valid response only if running the REMOTE_NO_URL variant.
-  if (result_type_running_ == REMOTE_NO_URL) {
+  if (result_type == REMOTE_NO_URL) {
     client()->GetPrefs()->SetString(omnibox::kZeroSuggestCachedResults,
                                     response_json);
-    LogEvent(Event::kRemoteResponseCached, result_type_running_, is_prefetch);
+    LogEvent(Event::kRemoteResponseCached, result_type, is_prefetch);
   }
 
   // For display stability reasons, update the displayed results with the remote
@@ -514,9 +551,10 @@
   return nullptr;
 }
 
-std::unique_ptr<base::Value> ZeroSuggestProvider::ReadStoredResponse() {
+std::unique_ptr<base::Value> ZeroSuggestProvider::ReadStoredResponse(
+    ResultType result_type) {
   // Use the stored response only if running the REMOTE_NO_URL variant.
-  if (result_type_running_ != REMOTE_NO_URL) {
+  if (result_type != REMOTE_NO_URL) {
     return nullptr;
   }
 
diff --git a/components/omnibox/browser/zero_suggest_provider.h b/components/omnibox/browser/zero_suggest_provider.h
index 5e078ce..2f779c28 100644
--- a/components/omnibox/browser/zero_suggest_provider.h
+++ b/components/omnibox/browser/zero_suggest_provider.h
@@ -87,8 +87,8 @@
   static void RegisterProfilePrefs(PrefRegistrySimple* registry);
 
   // AutocompleteProvider:
-  void Start(const AutocompleteInput& input, bool minimal_changes) override;
   void StartPrefetch(const AutocompleteInput& input) override;
+  void Start(const AutocompleteInput& input, bool minimal_changes) override;
   void Stop(bool clear_cached_results,
             bool due_to_user_inactivity) override;
   void DeleteMatch(const AutocompleteMatch& match) override;
@@ -117,12 +117,6 @@
   ZeroSuggestProvider(const ZeroSuggestProvider&) = delete;
   ZeroSuggestProvider& operator=(const ZeroSuggestProvider&) = delete;
 
-  // Called by Start() or StartPrefetch() with the appropriate arguments.
-  // Contains the implementation to start a request for suggestions.
-  void Start(const AutocompleteInput& input,
-             bool minimal_changes,
-             bool is_prefetch);
-
   // BaseSearchProvider:
   const TemplateURL* GetTemplateURL(bool is_keyword) const override;
   const AutocompleteInput GetInput(bool is_keyword) const override;
@@ -130,28 +124,38 @@
       const SearchSuggestionParser::SuggestResult& result) const override;
   void RecordDeletionResult(bool success) override;
 
-  // Called when the network request for suggestions has completed.
-  // `is_prefetch` is bound to this callback and indicates if the request is a
-  // prefetch one.
-  void OnURLLoadComplete(bool is_prefetch,
+  // Called when the non-prefetch network request has completed.
+  // `result_type` is bound to this callback and indicate the result type being
+  // received in this callback.
+  void OnURLLoadComplete(ResultType result_type,
                          const network::SimpleURLLoader* source,
                          std::unique_ptr<std::string> response_body);
+  // Called when the prefetch network request has completed.
+  // `input` and `result_type` are bound to this callback. The former is the
+  // input the request was made for and the latter indicates the result type
+  // being received in this callback.
+  void OnPrefetchURLLoadComplete(const AutocompleteInput& input,
+                                 ResultType result_type,
+                                 const network::SimpleURLLoader* source,
+                                 std::unique_ptr<std::string> response_body);
 
   // Called when the remote response is received. Stores the response json in
   // the user prefs, if successfully parsed and if applicable based on
-  // |result_type_running_|.
+  // |result_type|.
   //
   // Returns the successfully parsed response if it is eligible to be converted
   // to |matches_| or nullptr otherwise.
   std::unique_ptr<base::Value> StoreRemoteResponse(
       const std::string& response_json,
+      const AutocompleteInput& input,
+      ResultType result_type,
       bool is_prefetch);
 
   // Called on Start().
   //
   // Returns the response stored in the user prefs, if applicable based on
-  // |result_type_running_| or nullptr otherwise.
-  std::unique_ptr<base::Value> ReadStoredResponse();
+  // |result_type| or nullptr otherwise.
+  std::unique_ptr<base::Value> ReadStoredResponse(ResultType result_type);
 
   // Returns an AutocompleteMatch for a navigational suggestion |navigation|.
   AutocompleteMatch NavigationToMatch(
@@ -171,20 +175,22 @@
   bool ConvertResponseToAutocompleteMatches(
       std::unique_ptr<base::Value> response);
 
-  // The result type that is currently being processed by provider.
-  // When the provider is not running, the result type is set to NONE.
-  ResultType result_type_running_;
+  // The result type that is currently being retrieved and processed for
+  // non-prefetch requests.
+  // Set in Start() and used in Stop() for logging purposes.
+  ResultType result_type_running_{NONE};
 
-  // The user's input for which a suggestion fetch is pending.
+  // The input for which suggestions are being retrieved and processed for both
+  // prefetch and non-prefetch requests.
+  // Set in Start() and StartPrefetch() and used in GetInput() for parsing the
+  // response.
   AutocompleteInput input_;
 
-  // Loader used to retrieve results.
+  // Loader used to retrieve results for non-prefetch requests.
   std::unique_ptr<network::SimpleURLLoader> loader_;
 
-  // Like `AutocompleteProvider::done_`, but for prefetch requests. Used for
-  // metrics when the provider is stopped. `done_` and `prefetch_done_` should
-  // never both be true, a `Start()` request stops ongoing requests.
-  bool prefetch_done_;
+  // Loader used to retrieve results for prefetch requests.
+  std::unique_ptr<network::SimpleURLLoader> prefetch_loader_;
 
   // The list of experiment stats corresponding to |matches_|.
   SearchSuggestionParser::ExperimentStatsV2s experiment_stats_v2s_;
diff --git a/components/omnibox/browser/zero_suggest_provider_unittest.cc b/components/omnibox/browser/zero_suggest_provider_unittest.cc
index f8bc182..381d1bf6 100644
--- a/components/omnibox/browser/zero_suggest_provider_unittest.cc
+++ b/components/omnibox/browser/zero_suggest_provider_unittest.cc
@@ -693,22 +693,17 @@
   GURL suggest_url = GetSuggestURL(metrics::OmniboxEventProto::NTP_REALBOX);
 
   provider_->Start(input, false);
-  // Given that this is a purely synchronous provider run, the running result
-  // type should have been set to NONE.
-  ASSERT_EQ(ZeroSuggestProvider::NONE,
+  ASSERT_EQ(ZeroSuggestProvider::REMOTE_NO_URL,
             provider_->GetResultTypeRunningForTesting());
+  EXPECT_TRUE(provider_->done());
+  EXPECT_TRUE(provider_->matches().empty());
 
   // There should be no pending network requests, given that asynchronous logic
   // has been explicitly disabled (`omit_asynchronous_matches_ == true`).
   ASSERT_FALSE(test_loader_factory()->IsPending(suggest_url.spec()));
-  EXPECT_TRUE(provider_->done());
-  EXPECT_TRUE(provider_->matches().empty());
 
-  // Expect the provider to have not populated the results cache, given that
-  // no asynchronous network requests should have been sent to the suggestions
-  // service.
-  PrefService* prefs = client_->GetPrefs();
-  EXPECT_EQ("", prefs->GetString(omnibox::kZeroSuggestCachedResults));
+  // Expect the provider not to have notified the provider listener.
+  EXPECT_FALSE(provider_did_notify_);
 }
 
 TEST_F(ZeroSuggestProviderTest, TestPsuggestZeroSuggestHasCachedResults) {
@@ -867,8 +862,6 @@
     AutocompleteInput input = PrefetchingInputForNTP();
     provider_->StartPrefetch(input);
     EXPECT_TRUE(provider_->done());
-    ASSERT_EQ(ZeroSuggestProvider::REMOTE_NO_URL,
-              provider_->GetResultTypeRunningForTesting());
 
     // Expect the results to be empty.
     ASSERT_EQ(0U, provider_->matches().size());
diff --git a/components/omnibox/common/omnibox_features.cc b/components/omnibox/common/omnibox_features.cc
index 098b1e8..adfd095 100644
--- a/components/omnibox/common/omnibox_features.cc
+++ b/components/omnibox/common/omnibox_features.cc
@@ -305,7 +305,7 @@
                                           base::FEATURE_DISABLED_BY_DEFAULT};
 
 const base::Feature kBlurWithEscape{"OmniboxBlurWithEscape",
-                                    base::FEATURE_DISABLED_BY_DEFAULT};
+                                    base::FEATURE_ENABLED_BY_DEFAULT};
 
 // When enabled, adds a "starter pack" of @history, @bookmarks, and @settings
 // scopes to Site Search/Keyword Mode.
diff --git a/components/printing/browser/headless/BUILD.gn b/components/printing/browser/headless/BUILD.gn
new file mode 100644
index 0000000..04709c5
--- /dev/null
+++ b/components/printing/browser/headless/BUILD.gn
@@ -0,0 +1,25 @@
+# Copyright 2022 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//printing/buildflags/buildflags.gni")
+
+assert(enable_basic_printing)
+
+static_library("headless") {
+  sources = [
+    "headless_print_manager.cc",
+    "headless_print_manager.h",
+  ]
+
+  deps = [
+    "//base",
+    "//components/printing/browser",
+    "//components/printing/browser/print_to_pdf",
+    "//printing",
+    "//printing/buildflags",
+    "//printing/mojom",
+  ]
+
+  public_deps = [ "//components/printing/common:mojo_interfaces" ]
+}
diff --git a/components/printing/browser/headless/OWNERS b/components/printing/browser/headless/OWNERS
new file mode 100644
index 0000000..d299b1ba
--- /dev/null
+++ b/components/printing/browser/headless/OWNERS
@@ -0,0 +1 @@
+file://headless/OWNERS
diff --git a/components/printing/browser/print_to_pdf/pdf_print_manager.cc b/components/printing/browser/headless/headless_print_manager.cc
similarity index 64%
rename from components/printing/browser/print_to_pdf/pdf_print_manager.cc
rename to components/printing/browser/headless/headless_print_manager.cc
index 3975b60..8d7914a3 100644
--- a/components/printing/browser/print_to_pdf/pdf_print_manager.cc
+++ b/components/printing/browser/headless/headless_print_manager.cc
@@ -2,13 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "components/printing/browser/print_to_pdf/pdf_print_manager.h"
+#include "components/printing/browser/headless/headless_print_manager.h"
 
 #include <utility>
 
 #include "base/bind.h"
 #include "build/build_config.h"
-#include "components/printing/browser/print_to_pdf/pdf_print_result.h"
 #include "components/printing/browser/print_to_pdf/pdf_print_utils.h"
 #include "printing/mojom/print.mojom.h"
 #include "printing/page_range.h"
@@ -18,51 +17,41 @@
 #include "mojo/public/cpp/bindings/message.h"
 #endif
 
-namespace print_to_pdf {
+using print_to_pdf::PageRangeError;
+using print_to_pdf::PdfPrintResult;
+
+namespace headless {
 
 namespace {
 
 #if BUILDFLAG(ENABLE_PRINT_PREVIEW)
-constexpr char kInvalidUpdatePrintSettingsCall[] =
-    "Invalid UpdatePrintSettings Call";
-constexpr char kInvalidSetupScriptedPrintPreviewCall[] =
-    "Invalid SetupScriptedPrintPreview Call";
-constexpr char kInvalidShowScriptedPrintPreviewCall[] =
-    "Invalid ShowScriptedPrintPreview Call";
-constexpr char kInvalidRequestPrintPreviewCall[] =
-    "Invalid RequestPrintPreview Call";
-constexpr char kInvalidCheckForCancelCall[] = "Invalid CheckForCancel Call";
-#endif
-
-#if BUILDFLAG(ENABLE_TAGGED_PDF)
-constexpr char kInvalidSetAccessibilityTreeCall[] =
-    "Invalid SetAccessibilityTree Call";
+constexpr char kUnexpectedPrintManagerCall[] = "Unexpected Print Manager call";
 #endif
 
 }  // namespace
 
-PdfPrintManager::PdfPrintManager(content::WebContents* web_contents)
+HeadlessPrintManager::HeadlessPrintManager(content::WebContents* web_contents)
     : printing::PrintManager(web_contents),
-      content::WebContentsUserData<PdfPrintManager>(*web_contents) {}
+      content::WebContentsUserData<HeadlessPrintManager>(*web_contents) {}
 
-PdfPrintManager::~PdfPrintManager() = default;
+HeadlessPrintManager::~HeadlessPrintManager() = default;
 
 // static
-void PdfPrintManager::BindPrintManagerHost(
+void HeadlessPrintManager::BindPrintManagerHost(
     mojo::PendingAssociatedReceiver<printing::mojom::PrintManagerHost> receiver,
     content::RenderFrameHost* rfh) {
   auto* web_contents = content::WebContents::FromRenderFrameHost(rfh);
   if (!web_contents)
     return;
 
-  auto* print_manager = PdfPrintManager::FromWebContents(web_contents);
+  auto* print_manager = HeadlessPrintManager::FromWebContents(web_contents);
   if (!print_manager)
     return;
 
   print_manager->BindReceiver(std::move(receiver), rfh);
 }
 
-void PdfPrintManager::PrintToPdf(
+void HeadlessPrintManager::PrintToPdf(
     content::RenderFrameHost* rfh,
     const std::string& page_ranges,
     printing::mojom::PrintPagesParamsPtr print_pages_params,
@@ -82,7 +71,7 @@
   }
 
   absl::variant<printing::PageRanges, PageRangeError> parsed_ranges =
-      TextPageRangesToPageRanges(page_ranges);
+      print_to_pdf::TextPageRangesToPageRanges(page_ranges);
   if (absl::holds_alternative<PageRangeError>(parsed_ranges)) {
     PdfPrintResult print_result;
     switch (absl::get<PageRangeError>(parsed_ranges)) {
@@ -107,11 +96,11 @@
   // in the base class. If we're gone, mojo will discard the callback.
   GetPrintRenderFrame(rfh)->PrintWithParams(
       std::move(print_pages_params),
-      base::BindOnce(&PdfPrintManager::OnDidPrintWithParams,
+      base::BindOnce(&HeadlessPrintManager::OnDidPrintWithParams,
                      base::Unretained(this)));
 }
 
-void PdfPrintManager::OnDidPrintWithParams(
+void HeadlessPrintManager::OnDidPrintWithParams(
     printing::mojom::PrintWithParamsResultPtr result) {
   if (result->is_failure_reason()) {
     switch (result->get_failure_reason()) {
@@ -138,13 +127,13 @@
   ReleaseJob(PdfPrintResult::PRINT_SUCCESS);
 }
 
-void PdfPrintManager::GetDefaultPrintSettings(
+void HeadlessPrintManager::GetDefaultPrintSettings(
     GetDefaultPrintSettingsCallback callback) {
   DLOG(ERROR) << "Scripted print is not supported";
   std::move(callback).Run(printing::mojom::PrintParams::New());
 }
 
-void PdfPrintManager::ScriptedPrint(
+void HeadlessPrintManager::ScriptedPrint(
     printing::mojom::ScriptedPrintParamsPtr params,
     ScriptedPrintCallback callback) {
   auto default_param = printing::mojom::PrintPagesParams::New();
@@ -153,64 +142,52 @@
   std::move(callback).Run(std::move(default_param));
 }
 
-void PdfPrintManager::ShowInvalidPrinterSettingsError() {
+void HeadlessPrintManager::ShowInvalidPrinterSettingsError() {
   ReleaseJob(PdfPrintResult::INVALID_PRINTER_SETTINGS);
 }
 
 #if BUILDFLAG(ENABLE_PRINT_PREVIEW)
-void PdfPrintManager::UpdatePrintSettings(
+void HeadlessPrintManager::UpdatePrintSettings(
     int32_t cookie,
     base::Value::Dict job_settings,
     UpdatePrintSettingsCallback callback) {
-  // UpdatePrintSettingsCallback() should never be called on
-  // PdfPrintManager, since it is only triggered by Print Preview.
-  mojo::ReportBadMessage(kInvalidUpdatePrintSettingsCall);
+  mojo::ReportBadMessage(kUnexpectedPrintManagerCall);
 }
 
-void PdfPrintManager::SetupScriptedPrintPreview(
+void HeadlessPrintManager::SetupScriptedPrintPreview(
     SetupScriptedPrintPreviewCallback callback) {
-  // SetupScriptedPrintPreview() should never be called on
-  // PdfPrintManager, since it is only triggered by Print Preview.
-  mojo::ReportBadMessage(kInvalidSetupScriptedPrintPreviewCall);
+  mojo::ReportBadMessage(kUnexpectedPrintManagerCall);
 }
 
-void PdfPrintManager::ShowScriptedPrintPreview(bool source_is_modifiable) {
-  // ShowScriptedPrintPreview() should never be called on
-  // PdfPrintManager, since it is only triggered by Print Preview.
-  mojo::ReportBadMessage(kInvalidShowScriptedPrintPreviewCall);
+void HeadlessPrintManager::ShowScriptedPrintPreview(bool source_is_modifiable) {
+  mojo::ReportBadMessage(kUnexpectedPrintManagerCall);
 }
 
-void PdfPrintManager::RequestPrintPreview(
+void HeadlessPrintManager::RequestPrintPreview(
     printing::mojom::RequestPrintPreviewParamsPtr params) {
-  // RequestPrintPreview() should never be called on PdfPrintManager,
-  // since it is only triggered by Print Preview.
-  mojo::ReportBadMessage(kInvalidRequestPrintPreviewCall);
+  mojo::ReportBadMessage(kUnexpectedPrintManagerCall);
 }
 
-void PdfPrintManager::CheckForCancel(int32_t preview_ui_id,
-                                     int32_t request_id,
-                                     CheckForCancelCallback callback) {
-  // CheckForCancel() should never be called on PdfPrintManager, since it
-  // is only triggered by Print Preview.
-  mojo::ReportBadMessage(kInvalidCheckForCancelCall);
+void HeadlessPrintManager::CheckForCancel(int32_t preview_ui_id,
+                                          int32_t request_id,
+                                          CheckForCancelCallback callback) {
+  mojo::ReportBadMessage(kUnexpectedPrintManagerCall);
 }
 #endif  // BUILDFLAG(ENABLE_PRINT_PREVIEW)
 
 #if BUILDFLAG(ENABLE_TAGGED_PDF)
-void PdfPrintManager::SetAccessibilityTree(
+void HeadlessPrintManager::SetAccessibilityTree(
     int32_t cookie,
     const ui::AXTreeUpdate& accessibility_tree) {
-  // SetAccessibilityTree() should never be called on PdfPrintManager,
-  // since it is only triggered by Print Preview.
-  mojo::ReportBadMessage(kInvalidSetAccessibilityTreeCall);
+  mojo::ReportBadMessage(kUnexpectedPrintManagerCall);
 }
 #endif
 
 #if BUILDFLAG(IS_ANDROID)
-void PdfPrintManager::PdfWritingDone(int page_count) {}
+void HeadlessPrintManager::PdfWritingDone(int page_count) {}
 #endif
 
-void PdfPrintManager::RenderFrameDeleted(
+void HeadlessPrintManager::RenderFrameDeleted(
     content::RenderFrameHost* render_frame_host) {
   PrintManager::RenderFrameDeleted(render_frame_host);
 
@@ -226,13 +203,13 @@
   Reset();
 }
 
-void PdfPrintManager::Reset() {
+void HeadlessPrintManager::Reset() {
   printing_rfh_ = nullptr;
   callback_.Reset();
   data_.clear();
 }
 
-void PdfPrintManager::ReleaseJob(PdfPrintResult result) {
+void HeadlessPrintManager::ReleaseJob(PdfPrintResult result) {
   if (!callback_) {
     DLOG(ERROR) << "ReleaseJob is called when callback_ is null. Check whether "
                    "ReleaseJob is called more than once.";
@@ -254,6 +231,6 @@
   Reset();
 }
 
-WEB_CONTENTS_USER_DATA_KEY_IMPL(PdfPrintManager);
+WEB_CONTENTS_USER_DATA_KEY_IMPL(HeadlessPrintManager);
 
-}  // namespace print_to_pdf
+}  // namespace headless
diff --git a/components/printing/browser/print_to_pdf/pdf_print_manager.h b/components/printing/browser/headless/headless_print_manager.h
similarity index 73%
rename from components/printing/browser/print_to_pdf/pdf_print_manager.h
rename to components/printing/browser/headless/headless_print_manager.h
index b2dcff28..bab23c2 100644
--- a/components/printing/browser/print_to_pdf/pdf_print_manager.h
+++ b/components/printing/browser/headless/headless_print_manager.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef COMPONENTS_PRINTING_BROWSER_PRINT_TO_PDF_PDF_PRINT_MANAGER_H_
-#define COMPONENTS_PRINTING_BROWSER_PRINT_TO_PDF_PDF_PRINT_MANAGER_H_
+#ifndef COMPONENTS_PRINTING_BROWSER_HEADLESS_HEADLESS_PRINT_MANAGER_H_
+#define COMPONENTS_PRINTING_BROWSER_HEADLESS_HEADLESS_PRINT_MANAGER_H_
 
 #include <memory>
 #include <string>
@@ -20,19 +20,23 @@
 #include "content/public/browser/web_contents_user_data.h"
 #include "printing/print_settings.h"
 
-namespace print_to_pdf {
+namespace headless {
 
-class PdfPrintManager : public printing::PrintManager,
-                        public content::WebContentsUserData<PdfPrintManager> {
+// Minimalistic PrintManager implemementation intended for use with Headless
+// Chrome. It shortcuts most of the methods exposing only PrintToPdf()
+// functionality.
+class HeadlessPrintManager
+    : public printing::PrintManager,
+      public content::WebContentsUserData<HeadlessPrintManager> {
  public:
   using PrintToPdfCallback =
-      base::OnceCallback<void(PdfPrintResult,
+      base::OnceCallback<void(print_to_pdf::PdfPrintResult,
                               scoped_refptr<base::RefCountedMemory>)>;
 
-  ~PdfPrintManager() override;
+  ~HeadlessPrintManager() override;
 
-  PdfPrintManager(const PdfPrintManager&) = delete;
-  PdfPrintManager& operator=(const PdfPrintManager&) = delete;
+  HeadlessPrintManager(const HeadlessPrintManager&) = delete;
+  HeadlessPrintManager& operator=(const HeadlessPrintManager&) = delete;
 
   static void BindPrintManagerHost(
       mojo::PendingAssociatedReceiver<printing::mojom::PrintManagerHost>
@@ -45,9 +49,9 @@
                   PrintToPdfCallback callback);
 
  private:
-  friend class content::WebContentsUserData<PdfPrintManager>;
+  friend class content::WebContentsUserData<HeadlessPrintManager>;
 
-  explicit PdfPrintManager(content::WebContents* web_contents);
+  explicit HeadlessPrintManager(content::WebContents* web_contents);
 
   void OnDidPrintWithParams(printing::mojom::PrintWithParamsResultPtr result);
 
@@ -83,7 +87,7 @@
 #endif
 
   void Reset();
-  void ReleaseJob(PdfPrintResult result);
+  void ReleaseJob(print_to_pdf::PdfPrintResult result);
 
   raw_ptr<content::RenderFrameHost> printing_rfh_ = nullptr;
   PrintToPdfCallback callback_;
@@ -92,6 +96,6 @@
   WEB_CONTENTS_USER_DATA_KEY_DECL();
 };
 
-}  // namespace print_to_pdf
+}  // namespace headless
 
-#endif  // COMPONENTS_PRINTING_BROWSER_PRINT_TO_PDF_PDF_PRINT_MANAGER_H_
+#endif  // COMPONENTS_PRINTING_BROWSER_HEADLESS_HEADLESS_PRINT_MANAGER_H_
diff --git a/components/printing/browser/print_to_pdf/BUILD.gn b/components/printing/browser/print_to_pdf/BUILD.gn
index abe92fd9..394f173 100644
--- a/components/printing/browser/print_to_pdf/BUILD.gn
+++ b/components/printing/browser/print_to_pdf/BUILD.gn
@@ -9,8 +9,6 @@
 
 static_library("print_to_pdf") {
   sources = [
-    "pdf_print_manager.cc",
-    "pdf_print_manager.h",
     "pdf_print_result.cc",
     "pdf_print_result.h",
     "pdf_print_utils.cc",
@@ -23,6 +21,7 @@
     "//printing",
     "//printing/buildflags",
     "//printing/mojom",
+    "//url",
   ]
 
   public_deps = [ "//components/printing/common:mojo_interfaces" ]
diff --git a/components/printing/browser/print_to_pdf/pdf_print_result.cc b/components/printing/browser/print_to_pdf/pdf_print_result.cc
index 836cfb1..11b3a61e 100644
--- a/components/printing/browser/print_to_pdf/pdf_print_result.cc
+++ b/components/printing/browser/print_to_pdf/pdf_print_result.cc
@@ -4,8 +4,6 @@
 
 #include "components/printing/browser/print_to_pdf/pdf_print_result.h"
 
-#include "base/notreached.h"
-
 namespace print_to_pdf {
 
 std::string PdfPrintResultToString(PdfPrintResult result) {
diff --git a/components/privacy_sandbox/privacy_sandbox_settings.cc b/components/privacy_sandbox/privacy_sandbox_settings.cc
index 66615fa..02e2b76 100644
--- a/components/privacy_sandbox/privacy_sandbox_settings.cc
+++ b/components/privacy_sandbox/privacy_sandbox_settings.cc
@@ -310,8 +310,6 @@
   if (!IsPrivacySandboxEnabled())
     return {};
 
-  ContentSettingsForOneType cookie_settings;
-  cookie_settings_->GetCookieSettings(&cookie_settings);
   std::vector<GURL> allowed_parties;
   for (const auto& party : auction_parties) {
     if (IsPrivacySandboxEnabledForContext(party, top_frame_origin)) {
diff --git a/components/remote_cocoa/app_shim/native_widget_mac_nswindow.h b/components/remote_cocoa/app_shim/native_widget_mac_nswindow.h
index 67ebc56..77662e8 100644
--- a/components/remote_cocoa/app_shim/native_widget_mac_nswindow.h
+++ b/components/remote_cocoa/app_shim/native_widget_mac_nswindow.h
@@ -69,6 +69,9 @@
 
 // The NativeWidgetNSWindowBridge that this will use to call back to the host.
 @property(assign, nonatomic) remote_cocoa::NativeWidgetNSWindowBridge* bridge;
+
+// Whether this window functions as a tooltip.
+@property(assign, nonatomic) BOOL isTooltip;
 @end
 
 #endif  // COMPONENTS_REMOTE_COCOA_APP_SHIM_NATIVE_WIDGET_MAC_NSWINDOW_H_
diff --git a/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm b/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
index 48c86f5a..75546f5 100644
--- a/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
+++ b/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
@@ -146,9 +146,11 @@
   BOOL _isEnforcingNeverMadeVisible;
   BOOL _preventKeyWindow;
   BOOL _isAddingChildWindow;
+  BOOL _isTooltip;
 }
 @synthesize bridgedNativeWidgetId = _bridgedNativeWidgetId;
 @synthesize bridge = _bridge;
+@synthesize isTooltip = _isTooltip;
 
 - (instancetype)initWithContentRect:(NSRect)contentRect
                           styleMask:(NSUInteger)windowStyle
@@ -279,6 +281,10 @@
   return obj;
 }
 
+- (NSAccessibilityRole)accessibilityRole {
+  return _isTooltip ? NSAccessibilityHelpTagRole : [super accessibilityRole];
+}
+
 // NSWindow overrides.
 
 + (Class)frameViewClassForStyleMask:(NSWindowStyleMask)windowStyle {
diff --git a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
index 34721e5..c8a108c 100644
--- a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
+++ b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
@@ -482,6 +482,7 @@
   // Don't allow dragging sheets.
   if (params->modal_type == ui::MODAL_TYPE_WINDOW)
     [window_ setMovable:NO];
+  [window_ setIsTooltip:params->is_tooltip];
 }
 
 void NativeWidgetNSWindowBridge::SetInitialBounds(
diff --git a/components/remote_cocoa/common/native_widget_ns_window.mojom b/components/remote_cocoa/common/native_widget_ns_window.mojom
index 98292d6..6bb6315 100644
--- a/components/remote_cocoa/common/native_widget_ns_window.mojom
+++ b/components/remote_cocoa/common/native_widget_ns_window.mojom
@@ -81,6 +81,9 @@
   // window's workspace and fullscreen state, and can be retrieved from or
   // applied to a window.
   array<uint8> state_restoration_data;
+  // If true, this window is functionally a tooltip, and shouldn't be presented
+  // as a new window to the accessibility system.
+  bool is_tooltip;
 };
 
 enum WindowControlsOverlayNSViewType {
diff --git a/components/signin/core/browser/cookie_settings_util.cc b/components/signin/core/browser/cookie_settings_util.cc
index 2f9cebf..9bfe742 100644
--- a/components/signin/core/browser/cookie_settings_util.cc
+++ b/components/signin/core/browser/cookie_settings_util.cc
@@ -23,8 +23,7 @@
     const content_settings::CookieSettings* cookie_settings) {
   GURL gaia_url = GaiaUrls::GetInstance()->gaia_url();
   GURL google_url = GaiaUrls::GetInstance()->google_url();
-  ContentSettingsForOneType settings;
-  cookie_settings->GetCookieSettings(&settings);
+  ContentSettingsForOneType settings = cookie_settings->GetCookieSettings();
 
   return !cookie_settings ||
          cookie_settings->ShouldDeleteCookieOnExit(
diff --git a/components/tab_groups/public/mojom/tab_group_types.mojom b/components/tab_groups/public/mojom/tab_group_types.mojom
index 010513de..7b78dcef 100644
--- a/components/tab_groups/public/mojom/tab_group_types.mojom
+++ b/components/tab_groups/public/mojom/tab_group_types.mojom
@@ -6,8 +6,9 @@
 
 // The color Id associated with a given Tab Group. Maps to
 // tab_groups::TabGroupColorId in components/tab_groups/tab_group_color.h.
+[Stable, Extensible]
 enum Color {
-  kGrey,
+  [Default] kGrey,
   kBlue,
   kRed,
   kYellow,
diff --git a/components/viz/service/display/display.cc b/components/viz/service/display/display.cc
index a8092b91..0101429e 100644
--- a/components/viz/service/display/display.cc
+++ b/components/viz/service/display/display.cc
@@ -972,6 +972,11 @@
       "viz,benchmark", "Graphics.Pipeline.DrawAndSwap", last_swap_ack_trace_id_,
       "WaitForPresentation", timings.swap_end);
 
+  if (overlay_processor_)
+    overlay_processor_->OverlayPresentationComplete();
+  if (renderer_)
+    renderer_->SwapBuffersComplete(std::move(release_fence));
+
   DCHECK_GT(pending_swaps_, 0);
   pending_swaps_--;
   if (scheduler_) {
@@ -981,11 +986,6 @@
   if (no_pending_swaps_callback_ && pending_swaps_ == 0)
     std::move(no_pending_swaps_callback_).Run();
 
-  if (overlay_processor_)
-    overlay_processor_->OverlayPresentationComplete();
-  if (renderer_)
-    renderer_->SwapBuffersComplete(std::move(release_fence));
-
   // It's possible to receive multiple calls to DidReceiveSwapBuffersAck()
   // before DidReceivePresentationFeedback(). Ensure that we're not setting
   // |swap_timings_| for the same PresentationGroupTiming multiple times.
diff --git a/components/viz/service/display/overlay_processor_android.cc b/components/viz/service/display/overlay_processor_android.cc
index 11de2ab..fc108b61 100644
--- a/components/viz/service/display/overlay_processor_android.cc
+++ b/components/viz/service/display/overlay_processor_android.cc
@@ -22,6 +22,15 @@
     DisplayCompositorMemoryAndTaskController* display_controller)
     : OverlayProcessorUsingStrategy(),
       gpu_task_scheduler_(display_controller->gpu_task_scheduler()) {
+  // Promoting video to overlay with SurfaceView overlays requires recreation of
+  // main SurfaceView and Display. This leads to the situation when we
+  // consider video overlay not being efficient for the first frame after we
+  // updated SurfaceView and video gets demoted back to composition. To avoid
+  // this, we disable heuristics that filter out not efficient quads but still
+  // sort them by potential power savings.
+  prioritization_config_.changing_threshold = false;
+  prioritization_config_.damage_rate_threshold = false;
+
   // In unittests, we don't have the gpu_task_scheduler_ set up, but still want
   // to test ProcessForOverlays functionalities where we are making overlay
   // candidates correctly.
diff --git a/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc b/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc
index 263909c..00631df 100644
--- a/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc
+++ b/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc
@@ -1607,20 +1607,7 @@
 
 void SkiaOutputSurfaceImplOnGpu::ScheduleOverlays(
     SkiaOutputSurface::OverlayList overlays) {
-  TRACE_EVENT1("viz", "SkiaOutputSurfaceImplOnGpu::ScheduleOverlays",
-               "num_overlays", overlays.size());
-
-  constexpr base::TimeDelta kHistogramMinTime = base::Microseconds(5);
-  constexpr base::TimeDelta kHistogramMaxTime = base::Milliseconds(16);
-  constexpr int kHistogramTimeBuckets = 50;
-  base::TimeTicks start_time = base::TimeTicks::Now();
-
-  output_device_->ScheduleOverlays(std::move(overlays));
-
-  UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
-      "Gpu.OutputSurface.ScheduleOverlaysUs",
-      base::TimeTicks::Now() - start_time, kHistogramMinTime, kHistogramMaxTime,
-      kHistogramTimeBuckets);
+  overlays_ = std::move(overlays);
 }
 
 void SkiaOutputSurfaceImplOnGpu::SetEnableDCLayers(bool enable) {
@@ -2015,6 +2002,23 @@
         output_surface_plane_->damage_rect = frame->sub_buffer_rect;
     }
 
+    if (overlays_.size()) {
+      TRACE_EVENT1("viz", "SkiaOutputDevice->ScheduleOverlays()",
+                   "num_overlays", overlays_.size());
+
+      constexpr base::TimeDelta kHistogramMinTime = base::Microseconds(5);
+      constexpr base::TimeDelta kHistogramMaxTime = base::Milliseconds(16);
+      constexpr int kHistogramTimeBuckets = 50;
+      base::TimeTicks start_time = base::TimeTicks::Now();
+
+      output_device_->ScheduleOverlays(std::move(overlays_));
+
+      UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
+          "Gpu.OutputSurface.ScheduleOverlaysUs",
+          base::TimeTicks::Now() - start_time, kHistogramMinTime,
+          kHistogramMaxTime, kHistogramTimeBuckets);
+    }
+
     output_device_->SetViewportSize(frame->size);
     output_device_->SchedulePrimaryPlane(output_surface_plane_);
 
@@ -2038,8 +2042,9 @@
     }
   }
 
-  // Reset the primary plane information even on skipped swap.
+  // Reset the overlay plane information even on skipped swap.
   output_surface_plane_.reset();
+  overlays_.clear();
 
   destroy_after_swap_.clear();
   context_state_->UpdateSkiaOwnedMemorySize();
diff --git a/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.h b/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.h
index 710b875e..4c123e7 100644
--- a/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.h
+++ b/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.h
@@ -493,6 +493,9 @@
 
   absl::optional<OverlayProcessorInterface::OutputSurfaceOverlayPlane>
       output_surface_plane_;
+  // Overlays are saved when ScheduleOverlays() is called, then passed to
+  // |output_device_| in PostSubmit().
+  SkiaOutputSurface::OverlayList overlays_;
 
   // Micro-optimization to get to issuing GPU SwapBuffers as soon as possible.
   std::vector<sk_sp<SkDeferredDisplayList>> destroy_after_swap_;
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index 1a6ad373..9089c28 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -1446,8 +1446,11 @@
     "private_aggregation/private_aggregation_budget_storage.h",
     "private_aggregation/private_aggregation_budgeter.cc",
     "private_aggregation/private_aggregation_budgeter.h",
+    "private_aggregation/private_aggregation_features.cc",
+    "private_aggregation/private_aggregation_features.h",
     "private_aggregation/private_aggregation_host.cc",
     "private_aggregation/private_aggregation_host.h",
+    "private_aggregation/private_aggregation_manager.cc",
     "private_aggregation/private_aggregation_manager.h",
     "private_aggregation/private_aggregation_manager_impl.cc",
     "private_aggregation/private_aggregation_manager_impl.h",
diff --git a/content/browser/aggregation_service/aggregatable_report_scheduler.h b/content/browser/aggregation_service/aggregatable_report_scheduler.h
index 5ebe9e3..850a5de 100644
--- a/content/browser/aggregation_service/aggregatable_report_scheduler.h
+++ b/content/browser/aggregation_service/aggregatable_report_scheduler.h
@@ -47,22 +47,24 @@
       delete;
   AggregatableReportScheduler& operator=(
       const AggregatableReportScheduler& other) = delete;
-  ~AggregatableReportScheduler();
+  virtual ~AggregatableReportScheduler();
+
+  // Methods are virtual for testing.
 
   // Schedules the `request` to be assembled and sent at
   // `request.shared_info().scheduled_report_time`.
-  void ScheduleRequest(AggregatableReportRequest request);
+  virtual void ScheduleRequest(AggregatableReportRequest request);
 
   // Notifies that the request to assemble and send the report with `request_id`
   // was successfully completed. There must be an in-progress request stored
   // with that `request_id`.
-  void NotifyInProgressRequestSucceeded(
+  virtual void NotifyInProgressRequestSucceeded(
       AggregationServiceStorage::RequestId request_id);
 
   // Notifies that the request to assemble and send the report with `request_id`
   // completed unsuccessfully. There must be an in-progress request stored with
   // that `request_id`.
-  void NotifyInProgressRequestFailed(
+  virtual void NotifyInProgressRequestFailed(
       AggregationServiceStorage::RequestId request_id);
 
   // TODO(crbug.com/1340042): Implement offline and startup handling
diff --git a/content/browser/aggregation_service/aggregation_service.h b/content/browser/aggregation_service/aggregation_service.h
index 550dded3..8c42ab3 100644
--- a/content/browser/aggregation_service/aggregation_service.h
+++ b/content/browser/aggregation_service/aggregation_service.h
@@ -7,7 +7,7 @@
 
 #include "content/browser/aggregation_service/aggregatable_report_assembler.h"
 #include "content/browser/aggregation_service/aggregatable_report_sender.h"
-#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "content/public/browser/storage_partition.h"
 
 class GURL;
 
@@ -56,12 +56,20 @@
                           const base::Value& contents,
                           SendCallback callback) = 0;
 
-  // Deletes all data in storage that were fetched between `delete_begin` and
-  // `delete_end` time (inclusive). Null times are treated as unbounded lower or
-  // upper range.
+  // Deletes all data in storage that were fetched/stored between `delete_begin`
+  // and `delete_end` time (inclusive). Null times are treated as unbounded
+  // lower or upper range. If `!filter.is_null()`, requests with a reporting
+  // origin that does *not* match the `filter` are retained (i.e. not cleared);
+  // `filter` does not affect public key deletion.
   virtual void ClearData(base::Time delete_begin,
                          base::Time delete_end,
+                         StoragePartition::StorageKeyMatcherFunction filter,
                          base::OnceClosure done) = 0;
+
+  // Schedules `report_request` to be assembled and sent at its scheduled report
+  // time. It is stored on disk (unless in incognito) until then. See the
+  // `AggregatableReportScheduler` for details.
+  virtual void ScheduleReport(AggregatableReportRequest report_request) = 0;
 };
 
 }  // namespace content
diff --git a/content/browser/aggregation_service/aggregation_service_impl.cc b/content/browser/aggregation_service/aggregation_service_impl.cc
index 900206b..2ef5d003 100644
--- a/content/browser/aggregation_service/aggregation_service_impl.cc
+++ b/content/browser/aggregation_service/aggregation_service_impl.cc
@@ -8,8 +8,11 @@
 
 #include <memory>
 #include <utility>
+#include <vector>
 
+#include "base/bind.h"
 #include "base/callback.h"
+#include "base/check_op.h"
 #include "base/files/file_path.h"
 #include "base/memory/ptr_util.h"
 #include "base/task/lazy_thread_pool_task_runner.h"
@@ -18,10 +21,12 @@
 #include "base/time/time.h"
 #include "base/values.h"
 #include "content/browser/aggregation_service/aggregatable_report_assembler.h"
+#include "content/browser/aggregation_service/aggregatable_report_scheduler.h"
+#include "content/browser/aggregation_service/aggregatable_report_sender.h"
 #include "content/browser/aggregation_service/aggregation_service_storage_sql.h"
 #include "content/browser/aggregation_service/public_key.h"
 #include "content/browser/storage_partition_impl.h"
-#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "content/public/browser/storage_partition.h"
 #include "url/gurl.h"
 
 namespace content {
@@ -50,6 +55,12 @@
           run_in_memory,
           user_data_directory,
           base::DefaultClock::GetInstance(),
+          std::make_unique<AggregatableReportScheduler>(
+              /*storage_context=*/this,
+              // `base::Unretained` is safe as the scheduler is owned by `this`.
+              base::BindRepeating(
+                  &AggregationServiceImpl::OnScheduledReportTimeReached,
+                  base::Unretained(this))),
           std::make_unique<AggregatableReportAssembler>(this,
                                                         storage_partition),
           std::make_unique<AggregatableReportSender>(storage_partition)) {}
@@ -62,20 +73,23 @@
     bool run_in_memory,
     const base::FilePath& user_data_directory,
     const base::Clock* clock,
+    std::unique_ptr<AggregatableReportScheduler> scheduler,
     std::unique_ptr<AggregatableReportAssembler> assembler,
     std::unique_ptr<AggregatableReportSender> sender) {
-  return base::WrapUnique<AggregationServiceImpl>(
-      new AggregationServiceImpl(run_in_memory, user_data_directory, clock,
-                                 std::move(assembler), std::move(sender)));
+  return base::WrapUnique<AggregationServiceImpl>(new AggregationServiceImpl(
+      run_in_memory, user_data_directory, clock, std::move(scheduler),
+      std::move(assembler), std::move(sender)));
 }
 
 AggregationServiceImpl::AggregationServiceImpl(
     bool run_in_memory,
     const base::FilePath& user_data_directory,
     const base::Clock* clock,
+    std::unique_ptr<AggregatableReportScheduler> scheduler,
     std::unique_ptr<AggregatableReportAssembler> assembler,
     std::unique_ptr<AggregatableReportSender> sender)
-    : storage_(base::SequenceBound<AggregationServiceStorageSql>(
+    : scheduler_(std::move(scheduler)),
+      storage_(base::SequenceBound<AggregationServiceStorageSql>(
           g_storage_task_runner.Get(),
           run_in_memory,
           user_data_directory,
@@ -106,13 +120,62 @@
   return storage_;
 }
 
-void AggregationServiceImpl::ClearData(base::Time delete_begin,
-                                       base::Time delete_end,
-                                       base::OnceClosure done) {
-  storage_.AsyncCall(&AggregationServiceStorage::ClearPublicKeysFetchedBetween)
-      .WithArgs(delete_begin, delete_end)
+void AggregationServiceImpl::ClearData(
+    base::Time delete_begin,
+    base::Time delete_end,
+    StoragePartition::StorageKeyMatcherFunction filter,
+    base::OnceClosure done) {
+  storage_.AsyncCall(&AggregationServiceStorage::ClearDataBetween)
+      .WithArgs(delete_begin, delete_end, std::move(filter))
       .Then(std::move(done));
-  // TODO(crbug.com/1340053): Clear stored report requests as well.
+}
+
+void AggregationServiceImpl::ScheduleReport(
+    AggregatableReportRequest report_request) {
+  scheduler_->ScheduleRequest(std::move(report_request));
+}
+
+void AggregationServiceImpl::OnScheduledReportTimeReached(
+    std::vector<AggregationServiceStorage::RequestAndId> requests_and_ids) {
+  for (AggregationServiceStorage::RequestAndId& elem : requests_and_ids) {
+    GURL reporting_url = elem.request.GetReportingUrl();
+    AssembleReport(
+        std::move(elem.request),
+        base::BindOnce(
+            &AggregationServiceImpl::OnReportAssemblyComplete,
+            // `base::Unretained` is safe as the assembler is owned by `this`.
+            base::Unretained(this), elem.id, std::move(reporting_url)));
+  }
+}
+
+void AggregationServiceImpl::OnReportAssemblyComplete(
+    AggregationServiceStorage::RequestId request_id,
+    GURL reporting_url,
+    absl::optional<AggregatableReport> report,
+    AggregatableReportAssembler::AssemblyStatus status) {
+  DCHECK_EQ(report.has_value(),
+            status == AggregatableReportAssembler::AssemblyStatus::kOk);
+  if (!report.has_value()) {
+    scheduler_->NotifyInProgressRequestFailed(request_id);
+    return;
+  }
+
+  SendReport(reporting_url, report.value(),
+             /*callback=*/
+             base::BindOnce(
+                 &AggregationServiceImpl::OnReportSendingComplete,
+                 // `base::Unretained` is safe as the sender is owned by `this`.
+                 base::Unretained(this), request_id));
+}
+
+void AggregationServiceImpl::OnReportSendingComplete(
+    AggregationServiceStorage::RequestId request_id,
+    AggregatableReportSender::RequestStatus status) {
+  if (status == AggregatableReportSender::RequestStatus::kOk) {
+    scheduler_->NotifyInProgressRequestSucceeded(request_id);
+  } else {
+    scheduler_->NotifyInProgressRequestFailed(request_id);
+  }
 }
 
 void AggregationServiceImpl::SetPublicKeysForTesting(
diff --git a/content/browser/aggregation_service/aggregation_service_impl.h b/content/browser/aggregation_service/aggregation_service_impl.h
index 100eecd5..da14798 100644
--- a/content/browser/aggregation_service/aggregation_service_impl.h
+++ b/content/browser/aggregation_service/aggregation_service_impl.h
@@ -8,14 +8,17 @@
 #include <stdint.h>
 
 #include <memory>
+#include <vector>
 
 #include "base/containers/flat_map.h"
 #include "base/threading/sequence_bound.h"
 #include "content/browser/aggregation_service/aggregatable_report_assembler.h"
+#include "content/browser/aggregation_service/aggregatable_report_scheduler.h"
 #include "content/browser/aggregation_service/aggregatable_report_sender.h"
 #include "content/browser/aggregation_service/aggregation_service.h"
 #include "content/browser/aggregation_service/aggregation_service_storage_context.h"
 #include "content/common/content_export.h"
+#include "content/public/browser/storage_partition.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
 class GURL;
@@ -28,7 +31,9 @@
 namespace content {
 
 struct PublicKeyset;
+class AggregatableReport;
 class AggregationServiceStorage;
+class AggregatableReportScheduler;
 class StoragePartitionImpl;
 
 // UI thread class that manages the lifetime of the underlying storage. Owned by
@@ -42,6 +47,7 @@
       bool run_in_memory,
       const base::FilePath& user_data_directory,
       const base::Clock* clock,
+      std::unique_ptr<AggregatableReportScheduler> scheduler,
       std::unique_ptr<AggregatableReportAssembler> assembler,
       std::unique_ptr<AggregatableReportSender> sender);
 
@@ -66,7 +72,9 @@
                   SendCallback callback) override;
   void ClearData(base::Time delete_begin,
                  base::Time delete_end,
+                 StoragePartition::StorageKeyMatcherFunction filter,
                  base::OnceClosure done) override;
+  void ScheduleReport(AggregatableReportRequest report_request) override;
 
   // AggregationServiceStorageContext:
   const base::SequenceBound<AggregationServiceStorage>& GetStorage() override;
@@ -75,14 +83,29 @@
   void SetPublicKeysForTesting(const GURL& url, const PublicKeyset& keyset);
 
  private:
+  // Allows access to `OnScheduledReportTimeReached()`.
+  friend class AggregationServiceImplTest;
+
   AggregationServiceImpl(bool run_in_memory,
                          const base::FilePath& user_data_directory,
                          const base::Clock* clock,
+                         std::unique_ptr<AggregatableReportScheduler> scheduler,
                          std::unique_ptr<AggregatableReportAssembler> assembler,
                          std::unique_ptr<AggregatableReportSender> sender);
 
-  // TODO(crbug.com/1340050): Hook scheduler up to service and sender.
+  void OnScheduledReportTimeReached(
+      std::vector<AggregationServiceStorage::RequestAndId> requests_and_ids);
 
+  void OnReportAssemblyComplete(
+      AggregationServiceStorage::RequestId request_id,
+      GURL reporting_url,
+      absl::optional<AggregatableReport> report,
+      AggregatableReportAssembler::AssemblyStatus status);
+
+  void OnReportSendingComplete(AggregationServiceStorage::RequestId request_id,
+                               AggregatableReportSender::RequestStatus status);
+
+  std::unique_ptr<AggregatableReportScheduler> scheduler_;
   base::SequenceBound<AggregationServiceStorage> storage_;
   std::unique_ptr<AggregatableReportAssembler> assembler_;
   std::unique_ptr<AggregatableReportSender> sender_;
diff --git a/content/browser/aggregation_service/aggregation_service_impl_unittest.cc b/content/browser/aggregation_service/aggregation_service_impl_unittest.cc
index 0206bda..4db2819 100644
--- a/content/browser/aggregation_service/aggregation_service_impl_unittest.cc
+++ b/content/browser/aggregation_service/aggregation_service_impl_unittest.cc
@@ -11,7 +11,10 @@
 #include <utility>
 #include <vector>
 
+#include "base/callback.h"
+#include "base/callback_helpers.h"
 #include "base/containers/contains.h"
+#include "base/containers/flat_map.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
@@ -19,6 +22,9 @@
 #include "base/time/time.h"
 #include "content/browser/aggregation_service/aggregatable_report.h"
 #include "content/browser/aggregation_service/aggregatable_report_assembler.h"
+#include "content/browser/aggregation_service/aggregatable_report_scheduler.h"
+#include "content/browser/aggregation_service/aggregatable_report_sender.h"
+#include "content/browser/aggregation_service/aggregation_service_storage.h"
 #include "content/browser/aggregation_service/aggregation_service_test_utils.h"
 #include "content/public/test/browser_task_environment.h"
 #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
@@ -29,6 +35,8 @@
 
 namespace content {
 
+// TODO(alexmt): Consider rewriting these tests using gmock.
+
 class TestAggregatableReportAssembler : public AggregatableReportAssembler {
  public:
   explicit TestAggregatableReportAssembler(
@@ -82,10 +90,75 @@
   std::map<int64_t, ReportSentCallback> callbacks_;
 };
 
+class TestAggregatableReportScheduler : public AggregatableReportScheduler {
+ public:
+  TestAggregatableReportScheduler(
+      AggregationServiceStorageContext* storage_context,
+      base::RepeatingCallback<
+          void(std::vector<AggregationServiceStorage::RequestAndId>)>
+          on_scheduled_report_time_reached)
+      : AggregatableReportScheduler(storage_context, base::DoNothing()),
+        on_scheduled_report_time_reached_(
+            std::move(on_scheduled_report_time_reached)) {}
+  ~TestAggregatableReportScheduler() override = default;
+
+  void ScheduleRequest(AggregatableReportRequest request) override {
+    scheduled_reports_.emplace(unique_id_counter_++, std::move(request));
+  }
+
+  void NotifyInProgressRequestSucceeded(
+      AggregationServiceStorage::RequestId request_id) override {
+    completed_requests_status_[request_id] = true;
+  }
+
+  void NotifyInProgressRequestFailed(
+      AggregationServiceStorage::RequestId request_id) override {
+    completed_requests_status_[request_id] = false;
+  }
+
+  void TriggerReportingTime(
+      std::vector<AggregationServiceStorage::RequestId> request_ids) {
+    std::vector<AggregationServiceStorage::RequestAndId> return_value;
+    for (AggregationServiceStorage::RequestId request_id : request_ids) {
+      ASSERT_TRUE(base::Contains(scheduled_reports_, request_id));
+      return_value.push_back(AggregationServiceStorage::RequestAndId{
+          .request = std::move(scheduled_reports_.at(request_id)),
+          .id = request_id});
+      scheduled_reports_.erase(request_id);
+    }
+    on_scheduled_report_time_reached_.Run(std::move(return_value));
+  }
+
+  // Returns a boolean representing whether the request was successfully
+  // completed. Returns absl::nullopt if the request has not yet completed.
+  absl::optional<bool> WasRequestSuccessful(
+      AggregationServiceStorage::RequestId request_id) {
+    if (!base::Contains(completed_requests_status_, request_id)) {
+      return absl::nullopt;
+    }
+    return completed_requests_status_[request_id];
+  }
+
+ private:
+  base::RepeatingCallback<void(
+      std::vector<AggregationServiceStorage::RequestAndId>)>
+      on_scheduled_report_time_reached_;
+  int64_t unique_id_counter_ = 1;
+  base::flat_map<AggregationServiceStorage::RequestId,
+                 AggregatableReportRequest>
+      scheduled_reports_;
+
+  // Each completed request's ID is the key, with value whether it was completed
+  // successfully.
+  base::flat_map<AggregationServiceStorage::RequestId, bool>
+      completed_requests_status_;
+};
+
 class AggregationServiceImplTest : public testing::Test {
  public:
   AggregationServiceImplTest()
-      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
+      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
+        storage_context_(task_environment_.GetMockClock()) {
     EXPECT_TRUE(dir_.CreateUniqueTempDir());
 
     scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory =
@@ -100,10 +173,20 @@
         std::make_unique<TestAggregatableReportSender>(url_loader_factory);
     test_sender_ = sender.get();
 
+    auto scheduler = std::make_unique<TestAggregatableReportScheduler>(
+        &storage_context_,
+        base::BindLambdaForTesting(
+            [this](std::vector<AggregationServiceStorage::RequestAndId>
+                       requests_and_ids) {
+              service_impl_->OnScheduledReportTimeReached(
+                  std::move(requests_and_ids));
+            }));
+    test_scheduler_ = scheduler.get();
+
     service_impl_ = AggregationServiceImpl::CreateForTesting(
         /*run_in_memory=*/true, dir_.GetPath(),
-        task_environment_.GetMockClock(), std::move(assembler),
-        std::move(sender));
+        task_environment_.GetMockClock(), std::move(scheduler),
+        std::move(assembler), std::move(sender));
   }
 
   void AssembleReport(AggregatableReportRequest request) {
@@ -124,9 +207,14 @@
         }));
   }
 
+  void ScheduleReport(AggregatableReportRequest request) {
+    service()->ScheduleReport(std::move(request));
+  }
+
   AggregationServiceImpl* service() { return service_impl_.get(); }
   TestAggregatableReportAssembler* assembler() { return test_assembler_; }
   TestAggregatableReportSender* sender() { return test_sender_; }
+  TestAggregatableReportScheduler* scheduler() { return test_scheduler_; }
 
   // Returns `absl::nullopt` if no report callback has been run or if the last
   // assembly had an error.
@@ -151,8 +239,10 @@
   BrowserTaskEnvironment task_environment_;
   network::TestURLLoaderFactory test_url_loader_factory_;
   std::unique_ptr<AggregationServiceImpl> service_impl_;
+  TestAggregationServiceStorageContext storage_context_;
   raw_ptr<TestAggregatableReportAssembler> test_assembler_ = nullptr;
   raw_ptr<TestAggregatableReportSender> test_sender_ = nullptr;
+  raw_ptr<TestAggregatableReportScheduler> test_scheduler_ = nullptr;
 
   absl::optional<AggregatableReport> last_assembled_report_;
   absl::optional<AggregationService::AssemblyStatus> last_assembly_status_;
@@ -215,4 +305,152 @@
   EXPECT_EQ(last_send_status().value(), AggregationService::SendStatus::kOk);
 }
 
-}  // namespace content
\ No newline at end of file
+TEST_F(AggregationServiceImplTest, ScheduleReport_Success) {
+  AggregatableReportRequest request =
+      aggregation_service::CreateExampleRequest();
+
+  ScheduleReport(std::move(request));
+
+  // Request IDs begin at 1.
+  scheduler()->TriggerReportingTime(
+      /*request_ids=*/{AggregationServiceStorage::RequestId(1)});
+
+  std::vector<AggregatableReport::AggregationServicePayload> payloads;
+  payloads.emplace_back(/*payload=*/kABCD1234AsBytes,
+                        /*key_id=*/"key_1",
+                        /*debug_cleartext_payload=*/absl::nullopt);
+  AggregatableReport report(std::move(payloads), "example_shared_info");
+
+  assembler()->TriggerResponse(
+      /*report_id=*/0, std::move(report),
+      AggregatableReportAssembler::AssemblyStatus::kOk);
+
+  sender()->TriggerResponse(/*report_id=*/0,
+                            AggregatableReportSender::RequestStatus::kOk);
+
+  ASSERT_TRUE(
+      scheduler()
+          ->WasRequestSuccessful(AggregationServiceStorage::RequestId(1))
+          .has_value());
+  EXPECT_TRUE(
+      scheduler()
+          ->WasRequestSuccessful(AggregationServiceStorage::RequestId(1))
+          .value());
+}
+
+TEST_F(AggregationServiceImplTest, ScheduleReport_FailedAssembly) {
+  AggregatableReportRequest request =
+      aggregation_service::CreateExampleRequest();
+
+  ScheduleReport(std::move(request));
+
+  // Request IDs begin at 1.
+  scheduler()->TriggerReportingTime(
+      /*request_ids=*/{AggregationServiceStorage::RequestId(1)});
+
+  std::vector<AggregatableReport::AggregationServicePayload> payloads;
+  payloads.emplace_back(/*payload=*/kABCD1234AsBytes,
+                        /*key_id=*/"key_1",
+                        /*debug_cleartext_payload=*/absl::nullopt);
+  AggregatableReport report(std::move(payloads), "example_shared_info");
+
+  assembler()->TriggerResponse(
+      /*report_id=*/0, absl::nullopt,
+      AggregatableReportAssembler::AssemblyStatus::kAssemblyFailed);
+
+  ASSERT_TRUE(
+      scheduler()
+          ->WasRequestSuccessful(AggregationServiceStorage::RequestId(1))
+          .has_value());
+  EXPECT_FALSE(
+      scheduler()
+          ->WasRequestSuccessful(AggregationServiceStorage::RequestId(1))
+          .value());
+}
+
+TEST_F(AggregationServiceImplTest, ScheduleReport_FailedSending) {
+  AggregatableReportRequest request =
+      aggregation_service::CreateExampleRequest();
+
+  ScheduleReport(std::move(request));
+
+  scheduler()->TriggerReportingTime(
+      /*request_ids=*/{AggregationServiceStorage::RequestId(1)});
+
+  std::vector<AggregatableReport::AggregationServicePayload> payloads;
+  payloads.emplace_back(/*payload=*/kABCD1234AsBytes,
+                        /*key_id=*/"key_1",
+                        /*debug_cleartext_payload=*/absl::nullopt);
+  AggregatableReport report(std::move(payloads), "example_shared_info");
+
+  assembler()->TriggerResponse(
+      /*report_id=*/0, std::move(report),
+      AggregatableReportAssembler::AssemblyStatus::kOk);
+
+  sender()->TriggerResponse(
+      /*report_id=*/0, AggregatableReportSender::RequestStatus::kNetworkError);
+
+  ASSERT_TRUE(
+      scheduler()
+          ->WasRequestSuccessful(AggregationServiceStorage::RequestId(1))
+          .has_value());
+  EXPECT_FALSE(
+      scheduler()
+          ->WasRequestSuccessful(AggregationServiceStorage::RequestId(1))
+          .value());
+}
+
+TEST_F(AggregationServiceImplTest,
+       MultipleReportsReturnedFromScheduler_Success) {
+  AggregatableReportRequest request_1 =
+      aggregation_service::CreateExampleRequest();
+  AggregatableReportRequest request_2 =
+      aggregation_service::CreateExampleRequest();
+
+  ScheduleReport(std::move(request_1));
+  ScheduleReport(std::move(request_2));
+
+  // Request IDs begin at 1.
+  scheduler()->TriggerReportingTime(
+      /*request_ids=*/{AggregationServiceStorage::RequestId(1),
+                       AggregationServiceStorage::RequestId(2)});
+
+  std::vector<AggregatableReport::AggregationServicePayload> payloads;
+  payloads.emplace_back(/*payload=*/kABCD1234AsBytes,
+                        /*key_id=*/"key_1",
+                        /*debug_cleartext_payload=*/absl::nullopt);
+  AggregatableReport report_1(payloads, "example_shared_info");
+  AggregatableReport report_2(payloads, "example_shared_info");
+
+  assembler()->TriggerResponse(
+      /*report_id=*/0, std::move(report_1),
+      AggregatableReportAssembler::AssemblyStatus::kOk);
+  assembler()->TriggerResponse(
+      /*report_id=*/1, std::move(report_2),
+      AggregatableReportAssembler::AssemblyStatus::kOk);
+
+  sender()->TriggerResponse(/*report_id=*/0,
+                            AggregatableReportSender::RequestStatus::kOk);
+  sender()->TriggerResponse(/*report_id=*/1,
+                            AggregatableReportSender::RequestStatus::kOk);
+
+  ASSERT_TRUE(
+      scheduler()
+          ->WasRequestSuccessful(AggregationServiceStorage::RequestId(1))
+          .has_value());
+  EXPECT_TRUE(
+      scheduler()
+          ->WasRequestSuccessful(AggregationServiceStorage::RequestId(1))
+          .value());
+
+  ASSERT_TRUE(
+      scheduler()
+          ->WasRequestSuccessful(AggregationServiceStorage::RequestId(2))
+          .has_value());
+  EXPECT_TRUE(
+      scheduler()
+          ->WasRequestSuccessful(AggregationServiceStorage::RequestId(2))
+          .value());
+}
+
+}  // namespace content
diff --git a/content/browser/aggregation_service/aggregation_service_storage.h b/content/browser/aggregation_service/aggregation_service_storage.h
index 88fc140..b01a244 100644
--- a/content/browser/aggregation_service/aggregation_service_storage.h
+++ b/content/browser/aggregation_service/aggregation_service_storage.h
@@ -11,6 +11,7 @@
 
 #include "base/types/strong_alias.h"
 #include "content/browser/aggregation_service/aggregatable_report.h"
+#include "content/public/browser/storage_partition.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
 class GURL;
@@ -50,12 +51,6 @@
   // Clears the stored public keys for `url`.
   virtual void ClearPublicKeys(const GURL& url) = 0;
 
-  // Clears the stored public keys that were fetched between `delete_begin` and
-  // `delete_end` time (inclusive). Null times are treated as unbounded lower or
-  // upper range.
-  virtual void ClearPublicKeysFetchedBetween(base::Time delete_begin,
-                                             base::Time delete_end) = 0;
-
   // Clears the stored public keys that expire no later than `delete_end`
   // (inclusive).
   virtual void ClearPublicKeysExpiredBy(base::Time delete_end) = 0;
@@ -84,6 +79,19 @@
 
   // TODO(crbug.com/1340042): Add a method to randomly delay all reports in the
   // past (for startup and coming online).
+
+  // == Joint methods =====
+
+  // Clears the stored public keys that were fetched between and the report
+  // requests that were stored between `delete_begin` and `delete_end` time
+  // (inclusive). Null times are treated as unbounded lower or upper range.  If
+  // `!filter.is_null()`, requests with a reporting origin that does *not* match
+  // the `filter` are retained (i.e. not cleared); `filter` does not affect
+  // public key deletion.
+  virtual void ClearDataBetween(
+      base::Time delete_begin,
+      base::Time delete_end,
+      StoragePartition::StorageKeyMatcherFunction filter) = 0;
 };
 
 }  // namespace content
diff --git a/content/browser/aggregation_service/aggregation_service_storage_sql.cc b/content/browser/aggregation_service/aggregation_service_storage_sql.cc
index 70c64e8..fc63592 100644
--- a/content/browser/aggregation_service/aggregation_service_storage_sql.cc
+++ b/content/browser/aggregation_service/aggregation_service_storage_sql.cc
@@ -26,6 +26,7 @@
 #include "content/browser/aggregation_service/aggregation_service_storage.h"
 #include "content/browser/aggregation_service/proto/aggregatable_report.pb.h"
 #include "content/browser/aggregation_service/public_key.h"
+#include "content/public/browser/storage_partition.h"
 #include "services/network/public/cpp/is_potentially_trustworthy.h"
 #include "sql/database.h"
 #include "sql/meta_table.h"
@@ -33,6 +34,7 @@
 #include "sql/transaction.h"
 #include "third_party/abseil-cpp/absl/numeric/int128.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/blink/public/common/storage_key/storage_key.h"
 #include "url/gurl.h"
 #include "url/origin.h"
 
@@ -248,23 +250,9 @@
 void AggregationServiceStorageSql::ClearPublicKeysFetchedBetween(
     base::Time delete_begin,
     base::Time delete_end) {
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-
-  if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent))
-    return;
-
-  // Treat null times as unbounded lower or upper range. This is used by
-  // browsing data remover.
-  if (delete_begin.is_null())
-    delete_begin = base::Time::Min();
-
-  if (delete_end.is_null())
-    delete_end = base::Time::Max();
-
-  if (delete_begin.is_min() && delete_end.is_max()) {
-    ClearAllPublicKeys();
-    return;
-  }
+  DCHECK(!delete_begin.is_null());
+  DCHECK(!delete_end.is_null());
+  DCHECK(!delete_begin.is_min() || !delete_end.is_max());
 
   sql::Transaction transaction(&db_);
   if (!transaction.Begin())
@@ -456,6 +444,10 @@
   if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent))
     return;
 
+  DeleteRequestImpl(request_id);
+}
+
+bool AggregationServiceStorageSql::DeleteRequestImpl(RequestId request_id) {
   static constexpr char kDeleteRequestSql[] =
       "DELETE FROM report_requests WHERE request_id=?";
 
@@ -464,7 +456,7 @@
 
   delete_request_statement.BindInt64(0, request_id.value());
 
-  delete_request_statement.Run();
+  return delete_request_statement.Run();
 }
 
 absl::optional<base::Time> AggregationServiceStorageSql::NextReportTimeAfter(
@@ -528,6 +520,82 @@
   return result;
 }
 
+void AggregationServiceStorageSql::ClearDataBetween(
+    base::Time delete_begin,
+    base::Time delete_end,
+    StoragePartition::StorageKeyMatcherFunction filter) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent))
+    return;
+
+  // Treat null times as unbounded lower or upper range. This is used by
+  // browsing data remover.
+  if (delete_begin.is_null())
+    delete_begin = base::Time::Min();
+
+  if (delete_end.is_null())
+    delete_end = base::Time::Max();
+
+  if (delete_begin.is_min() && delete_end.is_max()) {
+    ClearAllPublicKeys();
+
+    if (filter.is_null()) {
+      ClearAllRequests();
+      return;
+    }
+  } else {
+    ClearPublicKeysFetchedBetween(delete_begin, delete_end);
+  }
+
+  ClearRequestsStoredBetween(delete_begin, delete_end, filter);
+}
+
+void AggregationServiceStorageSql::ClearRequestsStoredBetween(
+    base::Time delete_begin,
+    base::Time delete_end,
+    StoragePartition::StorageKeyMatcherFunction filter) {
+  DCHECK(!delete_begin.is_null());
+  DCHECK(!delete_end.is_null());
+  DCHECK(!delete_begin.is_min() || !delete_end.is_max() || !filter.is_null());
+
+  sql::Transaction transaction(&db_);
+  if (!transaction.Begin())
+    return;
+
+  static constexpr char kSelectRequestsToDeleteSql[] =
+      "SELECT request_id,reporting_origin FROM report_requests "
+      "WHERE creation_time BETWEEN ? AND ?";
+  sql::Statement select_requests_to_delete_statement(
+      db_.GetCachedStatement(SQL_FROM_HERE, kSelectRequestsToDeleteSql));
+  select_requests_to_delete_statement.BindTime(0, delete_begin);
+  select_requests_to_delete_statement.BindTime(1, delete_end);
+
+  while (select_requests_to_delete_statement.Step()) {
+    url::Origin reporting_origin = url::Origin::Create(
+        GURL(select_requests_to_delete_statement.ColumnString(1)));
+    if (filter.is_null() || filter.Run(blink::StorageKey(reporting_origin))) {
+      if (!DeleteRequestImpl(
+              RequestId(select_requests_to_delete_statement.ColumnInt64(0)))) {
+        return;
+      }
+    }
+  }
+
+  if (!select_requests_to_delete_statement.Succeeded())
+    return;
+
+  transaction.Commit();
+}
+
+void AggregationServiceStorageSql::ClearAllRequests() {
+  static constexpr char kClearAllRequests[] = "DELETE FROM report_requests";
+  sql::Statement select_requests_to_delete_statement(
+      db_.GetCachedStatement(SQL_FROM_HERE, kClearAllRequests));
+
+  select_requests_to_delete_statement.Run();
+}
+
 void AggregationServiceStorageSql::HandleInitializationFailure(
     const InitStatus status) {
   RecordInitializationStatus(status);
diff --git a/content/browser/aggregation_service/aggregation_service_storage_sql.h b/content/browser/aggregation_service/aggregation_service_storage_sql.h
index 9eb10e8..ede193c3 100644
--- a/content/browser/aggregation_service/aggregation_service_storage_sql.h
+++ b/content/browser/aggregation_service/aggregation_service_storage_sql.h
@@ -14,6 +14,7 @@
 #include "base/thread_annotations.h"
 #include "content/browser/aggregation_service/aggregation_service_storage.h"
 #include "content/common/content_export.h"
+#include "content/public/browser/storage_partition.h"
 #include "sql/database.h"
 #include "sql/meta_table.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
@@ -57,8 +58,6 @@
   std::vector<PublicKey> GetPublicKeys(const GURL& url) override;
   void SetPublicKeys(const GURL& url, const PublicKeyset& keyset) override;
   void ClearPublicKeys(const GURL& url) override;
-  void ClearPublicKeysFetchedBetween(base::Time delete_begin,
-                                     base::Time delete_end) override;
   void ClearPublicKeysExpiredBy(base::Time delete_end) override;
   void StoreRequest(AggregatableReportRequest request) override;
   void DeleteRequest(AggregationServiceStorage::RequestId request_id) override;
@@ -66,6 +65,10 @@
       base::Time strictly_after_time) override;
   std::vector<AggregationServiceStorage::RequestAndId>
   GetRequestsReportingOnOrBefore(base::Time not_after_time) override;
+  void ClearDataBetween(
+      base::Time delete_begin,
+      base::Time delete_end,
+      StoragePartition::StorageKeyMatcherFunction filter) override;
 
   void set_ignore_errors_for_testing(bool ignore_for_testing)
       VALID_CONTEXT_REQUIRED(sequence_checker_) {
@@ -115,9 +118,33 @@
   bool ClearPublicKeysByUrlId(int64_t url_id)
       VALID_CONTEXT_REQUIRED(sequence_checker_);
 
+  // Clears the stored public keys that were fetched between `delete_begin` and
+  // `delete_end` time (inclusive). Null times are treated as unbounded lower or
+  // upper range.
+  void ClearPublicKeysFetchedBetween(base::Time delete_begin,
+                                     base::Time delete_end)
+      VALID_CONTEXT_REQUIRED(sequence_checker_);
+
   // Clears all stored public keys.
   void ClearAllPublicKeys() VALID_CONTEXT_REQUIRED(sequence_checker_);
 
+  // Deletes the stored request with the given report ID.
+  bool DeleteRequestImpl(RequestId request_id)
+      VALID_CONTEXT_REQUIRED(sequence_checker_);
+
+  // Clears the report requests that were stored between `delete_begin` and
+  // `delete_end` time (inclusive). Null times are treated as unbounded lower or
+  // upper range. If `!filter.is_null()`, only requests with reporting origins
+  // matching the `filter` are cleared.
+  void ClearRequestsStoredBetween(
+      base::Time delete_begin,
+      base::Time delete_end,
+      StoragePartition::StorageKeyMatcherFunction filter)
+      VALID_CONTEXT_REQUIRED(sequence_checker_);
+
+  // Clears all stored report requests;
+  void ClearAllRequests() VALID_CONTEXT_REQUIRED(sequence_checker_);
+
   // Initializes the database if necessary, and returns whether the database is
   // open. `creation_policy` indicates whether the database should be created if
   // it is not already.
diff --git a/content/browser/aggregation_service/aggregation_service_storage_sql_unittest.cc b/content/browser/aggregation_service/aggregation_service_storage_sql_unittest.cc
index 7a65a47..c7d091cf 100644
--- a/content/browser/aggregation_service/aggregation_service_storage_sql_unittest.cc
+++ b/content/browser/aggregation_service/aggregation_service_storage_sql_unittest.cc
@@ -8,10 +8,12 @@
 #include <utility>
 #include <vector>
 
+#include "base/callback_helpers.h"
 #include "base/containers/flat_set.h"
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
+#include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/simple_test_clock.h"
 #include "base/time/time.h"
@@ -24,6 +26,7 @@
 #include "sql/test/test_helpers.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/blink/public/common/storage_key/storage_key.h"
 #include "url/gurl.h"
 
 namespace content {
@@ -248,7 +251,7 @@
 }
 
 TEST_F(AggregationServiceStorageSqlTest,
-       ClearPublicKeysFetchedBetween_RangeDeleted) {
+       ClearDataBetween_PublicKeyRangeDeleted) {
   OpenDatabase();
 
   GURL url_1("https://a.com/keys");
@@ -275,15 +278,19 @@
       keys_2, storage_->GetPublicKeys(url_2)));
 
   base::Time now = clock_.Now();
-  storage_->ClearPublicKeysFetchedBetween(now - base::Days(5),
-                                          now - base::Days(1));
+  storage_->ClearDataBetween(
+      now - base::Days(5), now - base::Days(1),
+      // The filter should be ignored.
+      base::BindLambdaForTesting(
+          [](const blink::StorageKey& storage_key) { return false; }));
 
   EXPECT_TRUE(storage_->GetPublicKeys(url_1).empty());
   EXPECT_TRUE(aggregation_service::PublicKeysEqual(
       keys_2, storage_->GetPublicKeys(url_2)));
 }
 
-TEST_F(AggregationServiceStorageSqlTest, ClearAllPublicKeys_AllDeleted) {
+TEST_F(AggregationServiceStorageSqlTest,
+       ClearAllDataWithFilter_PublicKeysAllDeleted) {
   OpenDatabase();
 
   GURL url_1("https://a.com/keys");
@@ -309,7 +316,45 @@
   EXPECT_TRUE(aggregation_service::PublicKeysEqual(
       keys_2, storage_->GetPublicKeys(url_2)));
 
-  storage_->ClearPublicKeysFetchedBetween(base::Time(), base::Time::Max());
+  storage_->ClearDataBetween(
+      base::Time(), base::Time::Max(),
+      // The filter should be ignored.
+      base::BindLambdaForTesting(
+          [](const blink::StorageKey& storage_key) { return false; }));
+
+  EXPECT_TRUE(storage_->GetPublicKeys(url_1).empty());
+  EXPECT_TRUE(storage_->GetPublicKeys(url_2).empty());
+}
+
+TEST_F(AggregationServiceStorageSqlTest,
+       ClearAllDataWithoutFilter_AllPublicKeysDeleted) {
+  OpenDatabase();
+
+  GURL url_1("https://a.com/keys");
+  std::vector<PublicKey> keys_1{
+      aggregation_service::GenerateKey("abcd").public_key,
+      aggregation_service::GenerateKey("bcde").public_key};
+  storage_->SetPublicKeys(url_1,
+                          PublicKeyset(keys_1, /*fetch_time=*/clock_.Now(),
+                                       /*expiry_time=*/base::Time::Max()));
+
+  clock_.Advance(base::Days(1));
+
+  GURL url_2("https://b.com/keys");
+  std::vector<PublicKey> keys_2{
+      aggregation_service::GenerateKey("abcd").public_key,
+      aggregation_service::GenerateKey("efgh").public_key};
+  storage_->SetPublicKeys(url_2,
+                          PublicKeyset(keys_2, /*fetch_time=*/clock_.Now(),
+                                       /*expiry_time=*/base::Time::Max()));
+
+  EXPECT_TRUE(aggregation_service::PublicKeysEqual(
+      keys_1, storage_->GetPublicKeys(url_1)));
+  EXPECT_TRUE(aggregation_service::PublicKeysEqual(
+      keys_2, storage_->GetPublicKeys(url_2)));
+
+  storage_->ClearDataBetween(base::Time(), base::Time::Max(),
+                             base::NullCallback());
 
   EXPECT_TRUE(storage_->GetPublicKeys(url_1).empty());
   EXPECT_TRUE(storage_->GetPublicKeys(url_2).empty());
@@ -610,6 +655,91 @@
             3u);
 }
 
+TEST_F(AggregationServiceStorageSqlTest,
+       ClearAllDataWithoutFilter_AllRequestsDeleted) {
+  OpenDatabase();
+
+  storage_->StoreRequest(aggregation_service::CreateExampleRequest());
+  storage_->StoreRequest(aggregation_service::CreateExampleRequest());
+
+  EXPECT_EQ(storage_->GetRequestsReportingOnOrBefore(base::Time::Max()).size(),
+            2u);
+
+  storage_->ClearDataBetween(base::Time(), base::Time(), base::NullCallback());
+
+  EXPECT_EQ(storage_->GetRequestsReportingOnOrBefore(base::Time::Max()).size(),
+            0u);
+}
+
+TEST_F(AggregationServiceStorageSqlTest,
+       ClearDataBetween_RequestsTimeRangeDeleted) {
+  OpenDatabase();
+
+  const base::Time kExampleTime = base::Time::FromJavaTime(1652984901234);
+
+  clock_.SetNow(kExampleTime);
+  storage_->StoreRequest(aggregation_service::CreateExampleRequest());
+
+  clock_.Advance(base::Hours(1));
+  storage_->StoreRequest(aggregation_service::CreateExampleRequest());
+
+  clock_.Advance(base::Hours(1));
+  storage_->StoreRequest(aggregation_service::CreateExampleRequest());
+
+  EXPECT_EQ(storage_->GetRequestsReportingOnOrBefore(base::Time::Max()).size(),
+            3u);
+
+  // As the times are inclusive, this should delete the first two requests.
+  storage_->ClearDataBetween(kExampleTime, kExampleTime + base::Hours(1),
+                             base::NullCallback());
+
+  std::vector<AggregationServiceStorage::RequestAndId> stored_reports =
+      storage_->GetRequestsReportingOnOrBefore(base::Time::Max());
+  ASSERT_EQ(stored_reports.size(), 1u);
+
+  // Only the last request should be left. Request IDs start from 1.
+  EXPECT_EQ(stored_reports[0].id, AggregationServiceStorage::RequestId(3));
+}
+
+TEST_F(AggregationServiceStorageSqlTest,
+       ClearDataAllTimesWithFilter_OnlyRequestsSpecifiedAreDeleted) {
+  const url::Origin reporting_origins[] = {
+      url::Origin::Create(GURL("https://a.example")),
+      url::Origin::Create(GURL("https://b.example")),
+      url::Origin::Create(GURL("https://c.example"))};
+
+  OpenDatabase();
+
+  for (const url::Origin& reporting_origin : reporting_origins) {
+    AggregatableReportRequest example_request =
+        aggregation_service::CreateExampleRequest();
+    AggregatableReportSharedInfo shared_info =
+        example_request.shared_info().Clone();
+    shared_info.reporting_origin = reporting_origin;
+    storage_->StoreRequest(
+        AggregatableReportRequest::Create(example_request.payload_contents(),
+                                          std::move(shared_info))
+            .value());
+  }
+
+  EXPECT_EQ(storage_->GetRequestsReportingOnOrBefore(base::Time::Max()).size(),
+            3u);
+
+  storage_->ClearDataBetween(
+      base::Time::Min(), base::Time::Max(),
+      base::BindLambdaForTesting(
+          [&reporting_origins](const blink::StorageKey& storage_key) {
+            return storage_key != blink::StorageKey(reporting_origins[2]);
+          }));
+
+  std::vector<AggregationServiceStorage::RequestAndId> stored_reports =
+      storage_->GetRequestsReportingOnOrBefore(base::Time::Max());
+  ASSERT_EQ(stored_reports.size(), 1u);
+
+  // Only the last request should be left. Request IDs start from 1.
+  EXPECT_EQ(stored_reports[0].id, AggregationServiceStorage::RequestId(3));
+}
+
 TEST_F(AggregationServiceStorageSqlInMemoryTest,
        DatabaseInMemoryReopened_RequestsNotPersisted) {
   OpenDatabase();
diff --git a/content/browser/back_forward_cache_features_browsertest.cc b/content/browser/back_forward_cache_features_browsertest.cc
index c3c3bae..9eaefee 100644
--- a/content/browser/back_forward_cache_features_browsertest.cc
+++ b/content/browser/back_forward_cache_features_browsertest.cc
@@ -3535,7 +3535,7 @@
 // alpha=1. While on the b-page it captures 3 more events. If the a-page is
 // still receiving events it should receive one or more of these. Finally it
 // resets the reasing back to have alpha=0 and navigates back to the a-page and
-// catpures 3 more events and verifies that all events on the a-page have
+// captures 3 more events and verifies that all events on the a-page have
 // alpha=1.
 IN_PROC_BROWSER_TEST_F(SensorBackForwardCacheBrowserTest,
                        SensorPausedWhileCached) {
diff --git a/content/browser/devtools/protocol/storage_handler.cc b/content/browser/devtools/protocol/storage_handler.cc
index 32a6df4..466ea42 100644
--- a/content/browser/devtools/protocol/storage_handler.cc
+++ b/content/browser/devtools/protocol/storage_handler.cc
@@ -378,13 +378,8 @@
   return Response::Success();
 }
 
-void StorageHandler::ClearDataForOrigin(
-    const std::string& origin,
-    const std::string& storage_types,
-    std::unique_ptr<ClearDataForOriginCallback> callback) {
-  if (!storage_partition_)
-    return callback->sendFailure(Response::InternalError());
-
+namespace {
+uint32_t GetRemoveDataMask(const std::string& storage_types) {
   std::vector<std::string> types = base::SplitString(
       storage_types, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
   std::unordered_set<std::string> set(types.begin(), types.end());
@@ -409,6 +404,18 @@
     remove_mask |= StoragePartition::REMOVE_DATA_MASK_INTEREST_GROUPS;
   if (set.count(Storage::StorageTypeEnum::All))
     remove_mask |= StoragePartition::REMOVE_DATA_MASK_ALL;
+  return remove_mask;
+}
+}  // namespace
+
+void StorageHandler::ClearDataForOrigin(
+    const std::string& origin,
+    const std::string& storage_types,
+    std::unique_ptr<ClearDataForOriginCallback> callback) {
+  if (!storage_partition_)
+    return callback->sendFailure(Response::InternalError());
+
+  uint32_t remove_mask = GetRemoveDataMask(storage_types);
 
   if (!remove_mask) {
     return callback->sendFailure(
@@ -423,6 +430,29 @@
                      std::move(callback)));
 }
 
+void StorageHandler::ClearDataForStorageKey(
+    const std::string& storage_key,
+    const std::string& storage_types,
+    std::unique_ptr<ClearDataForStorageKeyCallback> callback) {
+  if (!storage_partition_)
+    return callback->sendFailure(Response::InternalError());
+
+  uint32_t remove_mask = GetRemoveDataMask(storage_types);
+
+  if (!remove_mask) {
+    return callback->sendFailure(
+        Response::InvalidParams("No valid storage type specified"));
+  }
+
+  absl::optional<blink::StorageKey> key =
+      blink::StorageKey::Deserialize(storage_key);
+  storage_partition_->ClearData(
+      remove_mask, StoragePartition::QUOTA_MANAGED_STORAGE_MASK_ALL, *key,
+      base::Time(), base::Time::Max(),
+      base::BindOnce(&ClearDataForStorageKeyCallback::sendSuccess,
+                     std::move(callback)));
+}
+
 void StorageHandler::GetUsageAndQuota(
     const String& origin_string,
     std::unique_ptr<GetUsageAndQuotaCallback> callback) {
diff --git a/content/browser/devtools/protocol/storage_handler.h b/content/browser/devtools/protocol/storage_handler.h
index c54feda..203bbb55 100644
--- a/content/browser/devtools/protocol/storage_handler.h
+++ b/content/browser/devtools/protocol/storage_handler.h
@@ -49,6 +49,10 @@
       const std::string& origin,
       const std::string& storage_types,
       std::unique_ptr<ClearDataForOriginCallback> callback) override;
+  void ClearDataForStorageKey(
+      const std::string& storage_key,
+      const std::string& storage_types,
+      std::unique_ptr<ClearDataForStorageKeyCallback> callback) override;
   void GetUsageAndQuota(
       const String& origin,
       std::unique_ptr<GetUsageAndQuotaCallback> callback) override;
diff --git a/content/browser/devtools/protocol_config.json b/content/browser/devtools/protocol_config.json
index c2d36723..9c0af74 100644
--- a/content/browser/devtools/protocol_config.json
+++ b/content/browser/devtools/protocol_config.json
@@ -91,7 +91,7 @@
             },
             {
                 "domain": "Storage",
-                "async": ["getUsageAndQuota", "clearDataForOrigin", "getCookies", "setCookies", "clearCookies", "overrideQuotaForOrigin", "getTrustTokens", "clearTrustTokens", "getInterestGroupDetails"]
+                "async": ["getUsageAndQuota", "clearDataForOrigin", "clearDataForStorageKey", "getCookies", "setCookies", "clearCookies", "overrideQuotaForOrigin", "getTrustTokens", "clearTrustTokens", "getInterestGroupDetails"]
             },
             {
                 "domain": "SystemInfo",
diff --git a/content/browser/first_party_sets/database/first_party_sets_database.cc b/content/browser/first_party_sets/database/first_party_sets_database.cc
index 18a9034..ed0c4c3 100644
--- a/content/browser/first_party_sets/database/first_party_sets_database.cc
+++ b/content/browser/first_party_sets/database/first_party_sets_database.cc
@@ -48,18 +48,18 @@
   if (!db.Execute(kMarkedAtRunSitesSql))
     return false;
 
-  static constexpr char kProfilesClearedSql[] =
-      "CREATE TABLE IF NOT EXISTS profiles_cleared("
-      "profile TEXT PRIMARY KEY NOT NULL,"
+  static constexpr char kBrowserContextsClearedSql[] =
+      "CREATE TABLE IF NOT EXISTS browser_contexts_cleared("
+      "browser_context_id TEXT PRIMARY KEY NOT NULL,"
       "cleared_at_run INTEGER NOT NULL"
       ")WITHOUT ROWID";
-  if (!db.Execute(kProfilesClearedSql))
+  if (!db.Execute(kBrowserContextsClearedSql))
     return false;
 
-  static constexpr char kClearedAtRunProfilesSql[] =
-      "CREATE INDEX IF NOT EXISTS idx_cleared_at_run_profiles "
-      "ON profiles_cleared(cleared_at_run)";
-  if (!db.Execute(kClearedAtRunProfilesSql))
+  static constexpr char kClearedAtRunBrowserContextsSql[] =
+      "CREATE INDEX IF NOT EXISTS idx_cleared_at_run_browser_contexts "
+      "ON browser_contexts_cleared(cleared_at_run)";
+  if (!db.Execute(kClearedAtRunBrowserContextsSql))
     return false;
 
   return true;
@@ -94,8 +94,10 @@
   for (const auto& site : sites) {
     DCHECK(!site.opaque());
     static constexpr char kInsertSitesToClearSql[] =
+        // clang-format off
         "INSERT OR REPLACE INTO sites_to_clear(site,marked_at_run) "
         "VALUES(?,?)";
+    // clang-format on
     sql::Statement statement(
         db_->GetCachedStatement(SQL_FROM_HERE, kInsertSitesToClearSql));
     statement.BindString(0, site.Serialize());
@@ -107,34 +109,37 @@
   return transaction.Commit();
 }
 
-bool FirstPartySetsDatabase::InsertProfileCleared(const std::string& profile) {
+bool FirstPartySetsDatabase::InsertBrowserContextCleared(
+    const std::string& browser_context_id) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(!profile.empty());
+  DCHECK(!browser_context_id.empty());
 
   if (!LazyInit())
     return false;
 
-  static constexpr char kInsertProfilesClearedSql[] =
-      "INSERT OR REPLACE INTO profiles_cleared(profile,cleared_at_run) "
+  static constexpr char kInsertBrowserContextsClearedSql[] =
+      // clang-format off
+      "INSERT OR REPLACE INTO browser_contexts_cleared(browser_context_id,cleared_at_run) "
       "VALUES(?,?)";
+  // clang-format on
   sql::Statement statement(
-      db_->GetCachedStatement(SQL_FROM_HERE, kInsertProfilesClearedSql));
-  statement.BindString(0, profile);
+      db_->GetCachedStatement(SQL_FROM_HERE, kInsertBrowserContextsClearedSql));
+  statement.BindString(0, browser_context_id);
   statement.BindInt64(1, run_count_);
 
   return statement.Run();
 }
 
 std::vector<net::SchemefulSite> FirstPartySetsDatabase::FetchSitesToClear(
-    const std::string& profile) {
+    const std::string& browser_context_id) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(!profile.empty());
+  DCHECK(!browser_context_id.empty());
 
   if (!LazyInit())
     return {};
 
-  // No-op if the `profile` does not exist before.
-  if (!HasEntryFor(profile))
+  // No-op if the `browser_context_id` does not exist before.
+  if (!HasEntryFor(browser_context_id))
     return {};
 
   std::vector<net::SchemefulSite> results;
@@ -142,13 +147,13 @@
       // clang-format off
       "SELECT site FROM sites_to_clear "
       "WHERE marked_at_run>"
-        "(SELECT cleared_at_run FROM profiles_cleared "
-         "WHERE profile=?)";
+        "(SELECT cleared_at_run FROM browser_contexts_cleared "
+         "WHERE browser_context_id=?)";
   // clang-format on
 
   sql::Statement statement(
       db_->GetCachedStatement(SQL_FROM_HERE, kSelectSitesToClearSql));
-  statement.BindString(0, profile);
+  statement.BindString(0, browser_context_id);
 
   while (statement.Step()) {
     absl::optional<net::SchemefulSite> site =
@@ -299,18 +304,19 @@
   }
 }
 
-bool FirstPartySetsDatabase::HasEntryFor(const std::string& profile) const {
+bool FirstPartySetsDatabase::HasEntryFor(
+    const std::string& browser_context_id) const {
   DCHECK_EQ(db_status_, InitStatus::kSuccess);
-  DCHECK(!profile.empty());
+  DCHECK(!browser_context_id.empty());
 
-  static constexpr char kSelectProfileSql[] =
-      "SELECT 1 FROM profiles_cleared "
-      "WHERE profile=?"
+  static constexpr char kSelectBrowserContextSql[] =
+      "SELECT 1 FROM browser_contexts_cleared "
+      "WHERE browser_context_id=?"
       "LIMIT 1";
 
   sql::Statement statement(
-      db_->GetCachedStatement(SQL_FROM_HERE, kSelectProfileSql));
-  statement.BindString(0, profile);
+      db_->GetCachedStatement(SQL_FROM_HERE, kSelectBrowserContextSql));
+  statement.BindString(0, browser_context_id);
 
   return statement.Step();
 }
diff --git a/content/browser/first_party_sets/database/first_party_sets_database.h b/content/browser/first_party_sets/database/first_party_sets_database.h
index a7f4aa54..a56dc2b 100644
--- a/content/browser/first_party_sets/database/first_party_sets_database.h
+++ b/content/browser/first_party_sets/database/first_party_sets_database.h
@@ -60,18 +60,20 @@
   FirstPartySetsDatabase& operator=(const FirstPartySetsDatabase&&) = delete;
   ~FirstPartySetsDatabase();
 
-  // Stores the `sites` to be cleared into database, and returns true on
+  // Stores the `sites` into sites_to_clear table, and returns true on
   // success.
   [[nodiscard]] bool InsertSitesToClear(
       const std::vector<net::SchemefulSite>& sites);
 
-  // Stores the `profile` into database, and returns true on success.
-  [[nodiscard]] bool InsertProfileCleared(const std::string& profile);
+  // Stores the `browser_context_id` that has performed clearing into
+  // browser_contexts_cleared table, and returns true on success.
+  [[nodiscard]] bool InsertBrowserContextCleared(
+      const std::string& browser_context_id);
 
-  // Gets the list of sites to clear for `profile`. Returns an empty vector if
-  // `profile` does not exist in the database before.
+  // Gets the list of sites to clear for the `browser_context_id`. Returns an
+  // empty vector if `browser_context_id` does not exist in the database before.
   [[nodiscard]] std::vector<net::SchemefulSite> FetchSitesToClear(
-      const std::string& profile);
+      const std::string& browser_context_id);
 
  private:
   // Called at the start of each public operation, and initializes the database
@@ -95,8 +97,8 @@
   // never be negative.
   void IncreaseRunCount() VALID_CONTEXT_REQUIRED(sequence_checker_);
 
-  // Returns whether an entry exists for `profile`.
-  [[nodiscard]] bool HasEntryFor(const std::string& profile) const
+  // Returns whether an entry exists for the `browser_context_id`.
+  [[nodiscard]] bool HasEntryFor(const std::string& browser_context_id) const
       VALID_CONTEXT_REQUIRED(sequence_checker_);
 
   // Deletes the database and returns whether the operation was successful.
diff --git a/content/browser/first_party_sets/database/first_party_sets_database_unittest.cc b/content/browser/first_party_sets/database/first_party_sets_database_unittest.cc
index f91923f..50b95e20e 100644
--- a/content/browser/first_party_sets/database/first_party_sets_database_unittest.cc
+++ b/content/browser/first_party_sets/database/first_party_sets_database_unittest.cc
@@ -66,9 +66,10 @@
     return size;
   }
 
-  size_t CountProfilesClearedEntries(sql::Database* db) {
+  size_t CountBrowserContextsClearedEntries(sql::Database* db) {
     size_t size = 0;
-    EXPECT_TRUE(sql::test::CountTableRows(db, "profiles_cleared", &size));
+    EXPECT_TRUE(
+        sql::test::CountTableRows(db, "browser_contexts_cleared", &size));
     return size;
   }
 
@@ -105,18 +106,18 @@
   // Create a db handle to the existing db file to verify schemas.
   sql::Database db;
   EXPECT_TRUE(db.Open(db_path()));
-  // [sites_to_clear], [profiles_cleared], and [meta].
+  // [sites_to_clear], [browser_contexts_cleared], and [meta].
   EXPECT_EQ(3u, sql::test::CountSQLTables(&db));
   EXPECT_EQ(1, VersionFromMetaTable(db));
-  // [idx_marked_at_run_sites], [idx_cleared_at_run_profiles], and
+  // [idx_marked_at_run_sites], [idx_cleared_at_run_browser_contexts], and
   // [sqlite_autoindex_meta_1].
   EXPECT_EQ(3u, sql::test::CountSQLIndices(&db));
   // `site`, `marked_at_run`.
   EXPECT_EQ(2u, sql::test::CountTableColumns(&db, "sites_to_clear"));
-  // `profile`, `cleared_at_run`.
-  EXPECT_EQ(2u, sql::test::CountTableColumns(&db, "profiles_cleared"));
+  // `browser_context_id`, `cleared_at_run`.
+  EXPECT_EQ(2u, sql::test::CountTableColumns(&db, "browser_contexts_cleared"));
   EXPECT_EQ(0u, CountSitesToClearEntries(&db));
-  EXPECT_EQ(0u, CountProfilesClearedEntries(&db));
+  EXPECT_EQ(0u, CountBrowserContextsClearedEntries(&db));
 }
 
 TEST_F(FirstPartySetsDatabaseTest, LoadDBFile_CurrentVersion_Success) {
@@ -134,7 +135,7 @@
   EXPECT_EQ(3u, sql::test::CountSQLTables(&db));
   EXPECT_EQ(1, VersionFromMetaTable(db));
   EXPECT_EQ(1u, CountSitesToClearEntries(&db));
-  EXPECT_EQ(1u, CountProfilesClearedEntries(&db));
+  EXPECT_EQ(1u, CountBrowserContextsClearedEntries(&db));
 
   histograms.ExpectUniqueSample("FirstPartySets.Database.InitStatus",
                                 FirstPartySetsDatabase::InitStatus::kSuccess,
@@ -158,7 +159,7 @@
   EXPECT_EQ(3u, sql::test::CountSQLTables(&db));
   EXPECT_EQ(0, VersionFromMetaTable(db));
   EXPECT_EQ(1u, CountSitesToClearEntries(&db));
-  EXPECT_EQ(1u, CountProfilesClearedEntries(&db));
+  EXPECT_EQ(1u, CountBrowserContextsClearedEntries(&db));
 
   histograms.ExpectUniqueSample("FirstPartySets.Database.InitStatus",
                                 FirstPartySetsDatabase::InitStatus::kTooOld, 1);
@@ -181,7 +182,7 @@
   EXPECT_EQ(3u, sql::test::CountSQLTables(&db));
   EXPECT_EQ(2, VersionFromMetaTable(db));
   EXPECT_EQ(1u, CountSitesToClearEntries(&db));
-  EXPECT_EQ(1u, CountProfilesClearedEntries(&db));
+  EXPECT_EQ(1u, CountBrowserContextsClearedEntries(&db));
 
   histograms.ExpectUniqueSample("FirstPartySets.Database.InitStatus",
                                 FirstPartySetsDatabase::InitStatus::kTooNew, 1);
@@ -290,29 +291,30 @@
   EXPECT_FALSE(s.Step());
 }
 
-TEST_F(FirstPartySetsDatabaseTest, InsertProfileCleared_NoPreExistingDB) {
-  const std::string profile = "p";
+TEST_F(FirstPartySetsDatabaseTest,
+       InsertBrowserContextCleared_NoPreExistingDB) {
+  const std::string browser_context_id = "p";
   int64_t expected_run_count = 1;
 
   OpenDatabase();
   // Trigger the lazy-initialization.
-  EXPECT_TRUE(db()->InsertProfileCleared(profile));
+  EXPECT_TRUE(db()->InsertBrowserContextCleared(browser_context_id));
   CloseDatabase();
 
   sql::Database db;
   EXPECT_TRUE(db.Open(db_path()));
-  EXPECT_EQ(1u, CountProfilesClearedEntries(&db));
+  EXPECT_EQ(1u, CountBrowserContextsClearedEntries(&db));
 
   const char kSelectSql[] =
-      "SELECT profile, cleared_at_run FROM profiles_cleared";
+      "SELECT browser_context_id, cleared_at_run FROM browser_contexts_cleared";
   sql::Statement s(db.GetUniqueStatement(kSelectSql));
   EXPECT_TRUE(s.Step());
-  EXPECT_EQ(profile, s.ColumnString(0));
+  EXPECT_EQ(browser_context_id, s.ColumnString(0));
   EXPECT_EQ(expected_run_count, s.ColumnInt64(1));
   EXPECT_FALSE(s.Step());
 }
 
-TEST_F(FirstPartySetsDatabaseTest, InsertProfileCleared_PreExistingDB) {
+TEST_F(FirstPartySetsDatabaseTest, InsertBrowserContextCleared_PreExistingDB) {
   ASSERT_TRUE(
       sql::test::CreateDatabaseFromSQL(db_path(), GetSqlFilePath("v1.sql")));
 
@@ -322,10 +324,11 @@
     sql::Database db;
     EXPECT_TRUE(db.Open(db_path()));
     EXPECT_EQ(3u, sql::test::CountSQLTables(&db));
-    EXPECT_EQ(1u, CountProfilesClearedEntries(&db));
+    EXPECT_EQ(1u, CountBrowserContextsClearedEntries(&db));
 
     const char kSelectSql[] =
-        "SELECT profile, cleared_at_run FROM profiles_cleared";
+        "SELECT browser_context_id, cleared_at_run FROM "
+        "browser_contexts_cleared";
     sql::Statement s(db.GetUniqueStatement(kSelectSql));
     EXPECT_TRUE(s.Step());
     EXPECT_EQ("p", s.ColumnString(0));
@@ -333,36 +336,35 @@
     pre_run_count = s.ColumnInt64(1);
   }
 
-  std::string profile = "p1";
+  std::string browser_context_id = "p1";
 
   OpenDatabase();
   // Trigger the lazy-initialization.
-  EXPECT_TRUE(db()->InsertProfileCleared(profile));
+  EXPECT_TRUE(db()->InsertBrowserContextCleared(browser_context_id));
   CloseDatabase();
 
   // Verify the inserted data.
   sql::Database db;
   EXPECT_TRUE(db.Open(db_path()));
-  EXPECT_EQ(2u, CountProfilesClearedEntries(&db));
+  EXPECT_EQ(2u, CountBrowserContextsClearedEntries(&db));
 
   const char kSelectSql[] =
-      "SELECT profile FROM profiles_cleared "
+      "SELECT browser_context_id FROM browser_contexts_cleared "
       "WHERE cleared_at_run>?";
   sql::Statement s(db.GetUniqueStatement(kSelectSql));
   s.BindInt64(0, pre_run_count);
 
   EXPECT_TRUE(s.Step());
-  EXPECT_EQ(profile, s.ColumnString(0));
+  EXPECT_EQ(browser_context_id, s.ColumnString(0));
   EXPECT_FALSE(s.Step());
 }
 
 TEST_F(FirstPartySetsDatabaseTest, FetchSitesToClear_NoPreExistingDB) {
   OpenDatabase();
-  EXPECT_EQ(std::vector<net::SchemefulSite>(),
-            db()->FetchSitesToClear("profile"));
+  EXPECT_EQ(std::vector<net::SchemefulSite>(), db()->FetchSitesToClear("id"));
 }
 
-TEST_F(FirstPartySetsDatabaseTest, FetchSitesToClear_ProfileNotExist) {
+TEST_F(FirstPartySetsDatabaseTest, FetchSitesToClear_BrowserContextNotExist) {
   ASSERT_TRUE(
       sql::test::CreateDatabaseFromSQL(db_path(), GetSqlFilePath("v1.sql")));
 
@@ -372,9 +374,10 @@
     EXPECT_TRUE(db.Open(db_path()));
     EXPECT_EQ(3u, sql::test::CountSQLTables(&db));
     EXPECT_EQ(1u, CountSitesToClearEntries(&db));
-    EXPECT_EQ(1u, CountProfilesClearedEntries(&db));
+    EXPECT_EQ(1u, CountBrowserContextsClearedEntries(&db));
 
-    const char kSelectSql[] = "SELECT profile FROM profiles_cleared";
+    const char kSelectSql[] =
+        "SELECT browser_context_id FROM browser_contexts_cleared";
     sql::Statement s(db.GetUniqueStatement(kSelectSql));
     EXPECT_TRUE(s.Step());
     EXPECT_EQ("p", s.ColumnString(0));
@@ -395,9 +398,10 @@
     EXPECT_TRUE(db.Open(db_path()));
     EXPECT_EQ(3u, sql::test::CountSQLTables(&db));
     EXPECT_EQ(1u, CountSitesToClearEntries(&db));
-    EXPECT_EQ(1u, CountProfilesClearedEntries(&db));
+    EXPECT_EQ(1u, CountBrowserContextsClearedEntries(&db));
 
-    const char kSelectSql[] = "SELECT profile FROM profiles_cleared";
+    const char kSelectSql[] =
+        "SELECT browser_context_id FROM browser_contexts_cleared";
     sql::Statement s(db.GetUniqueStatement(kSelectSql));
     EXPECT_TRUE(s.Step());
     EXPECT_EQ("p", s.ColumnString(0));
diff --git a/content/browser/gpu/gpu_data_manager_impl_private.cc b/content/browser/gpu/gpu_data_manager_impl_private.cc
index 5100caa..855bd71 100644
--- a/content/browser/gpu/gpu_data_manager_impl_private.cc
+++ b/content/browser/gpu/gpu_data_manager_impl_private.cc
@@ -1118,9 +1118,12 @@
 
 void GpuDataManagerImplPrivate::UpdateDXGIInfo(
     gfx::mojom::DXGIInfoPtr dxgi_info) {
-  // This is running on the main thread;
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
-  HDRProxy::GotResult(std::move(dxgi_info));
+  // Calling out into HDRProxy::GotResult may end up re-entering us via
+  // GpuDataManagerImpl::OnDisplayRemoved/OnDisplayAdded. Both of these
+  // take the owner's lock. To avoid recursive locks, we PostTask
+  // HDRProxy::GotResult so that it runs outside of the lock.
+  GetUIThreadTaskRunner({})->PostTask(
+      FROM_HERE, base::BindOnce(&HDRProxy::GotResult, std::move(dxgi_info)));
 }
 
 void GpuDataManagerImplPrivate::UpdateDxDiagNodeRequestStatus(
diff --git a/content/browser/loader/navigation_url_loader_impl.h b/content/browser/loader/navigation_url_loader_impl.h
index 64fb28a..b3a44d6 100644
--- a/content/browser/loader/navigation_url_loader_impl.h
+++ b/content/browser/loader/navigation_url_loader_impl.h
@@ -19,6 +19,7 @@
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "net/url_request/url_request.h"
+#include "ppapi/buildflags/buildflags.h"
 #include "services/metrics/public/cpp/ukm_source_id.h"
 #include "services/network/public/mojom/accept_ch_frame_observer.mojom.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
diff --git a/content/browser/media/capture/web_contents_video_capture_device_browsertest.cc b/content/browser/media/capture/web_contents_video_capture_device_browsertest.cc
index 7d6580a8..cf3a9a0 100644
--- a/content/browser/media/capture/web_contents_video_capture_device_browsertest.cc
+++ b/content/browser/media/capture/web_contents_video_capture_device_browsertest.cc
@@ -320,16 +320,8 @@
 
 // Tests that the device starts, captures a frame, and then gracefully
 // errors-out because the WebContents is destroyed before the device is stopped.
-// TODO the test is flaky on Mac. See https://crbug.com/1345663.
-#if BUILDFLAG(IS_MAC)
-#define MAYBE_ErrorsOutWhenWebContentsIsDestroyed \
-  DISABLED_ErrorsOutWhenWebContentsIsDestroyed
-#else
-#define MAYBE_ErrorsOutWhenWebContentsIsDestroyed \
-  ErrorsOutWhenWebContentsIsDestroyed
-#endif
 IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest,
-                       MAYBE_ErrorsOutWhenWebContentsIsDestroyed) {
+                       ErrorsOutWhenWebContentsIsDestroyed) {
   NavigateToInitialDocument();
   AllocateAndStartAndWaitForFirstFrame();
   EXPECT_TRUE(shell()->web_contents()->IsBeingCaptured());
@@ -350,14 +342,8 @@
 
 // Tests that capture is re-targetted when the render view of a WebContents
 // changes.
-// TODO the test is flaky on Mac. See https://crbug.com/1345663.
-#if BUILDFLAG(IS_MAC)
-#define MAYBE_ChangesTargettedRenderView DISABLED_ChangesTargettedRenderView
-#else
-#define MAYBE_ChangesTargettedRenderView ChangesTargettedRenderView
-#endif
 IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest,
-                       MAYBE_ChangesTargettedRenderView) {
+                       ChangesTargettedRenderView) {
   NavigateToInitialDocument();
   AllocateAndStartAndWaitForFirstFrame();
   EXPECT_TRUE(shell()->web_contents()->IsBeingCaptured());
@@ -429,14 +415,8 @@
 
 // Tests that capture is re-targetted when a renderer crash is followed by a
 // reload. Regression test for http://crbug.com/916332.
-// TODO the test is flaky on Mac. See https://crbug.com/1345663.
-#if BUILDFLAG(IS_MAC)
-#define MAYBE_RecoversAfterRendererCrash DISABLED_RecoversAfterRendererCrash
-#else
-#define MAYBE_RecoversAfterRendererCrash RecoversAfterRendererCrash
-#endif
 IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest,
-                       MAYBE_RecoversAfterRendererCrash) {
+                       RecoversAfterRendererCrash) {
   NavigateToInitialDocument();
   AllocateAndStartAndWaitForFirstFrame();
   EXPECT_TRUE(shell()->web_contents()->IsBeingCaptured());
@@ -465,14 +445,8 @@
 // Tests that the device stops delivering frames while suspended. When resumed,
 // any content changes that occurred during the suspend should cause a new frame
 // to be delivered, to ensure the client is up-to-date.
-// TODO the test is flaky on Mac. See https://crbug.com/1345663.
-#if BUILDFLAG(IS_MAC)
-#define MAYBE_SuspendsAndResumes DISABLED_SuspendsAndResumes
-#else
-#define MAYBE_SuspendsAndResumes SuspendsAndResumes
-#endif
 IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest,
-                       MAYBE_SuspendsAndResumes) {
+                       SuspendsAndResumes) {
   NavigateToInitialDocument();
   AllocateAndStartAndWaitForFirstFrame();
   EXPECT_TRUE(shell()->web_contents()->IsBeingCaptured());
@@ -506,15 +480,8 @@
 
 // Tests that the device delivers refresh frames when asked, while the source
 // content is not changing.
-// TODO the test is flaky on Mac. See https://crbug.com/1345663.
-#if BUILDFLAG(IS_MAC)
-#define MAYBE_DeliversRefreshFramesUponRequest \
-  DISABLED_DeliversRefreshFramesUponRequest
-#else
-#define MAYBE_DeliversRefreshFramesUponRequest DeliversRefreshFramesUponRequest
-#endif
 IN_PROC_BROWSER_TEST_F(WebContentsVideoCaptureDeviceBrowserTest,
-                       MAYBE_DeliversRefreshFramesUponRequest) {
+                       DeliversRefreshFramesUponRequest) {
   NavigateToInitialDocument();
   AllocateAndStartAndWaitForFirstFrame();
   EXPECT_TRUE(shell()->web_contents()->IsBeingCaptured());
diff --git a/content/browser/plugin_service_impl.cc b/content/browser/plugin_service_impl.cc
index edb9122..602c95af 100644
--- a/content/browser/plugin_service_impl.cc
+++ b/content/browser/plugin_service_impl.cc
@@ -123,7 +123,6 @@
 
 PpapiPluginProcessHost* PluginServiceImpl::FindOrStartPpapiPluginProcess(
     int render_process_id,
-    const url::Origin& embedder_origin,
     const base::FilePath& plugin_path,
     const base::FilePath& profile_data_directory,
     const absl::optional<url::Origin>& origin_lock) {
@@ -142,12 +141,6 @@
     return nullptr;
   }
 
-  // Validate that |embedder_origin| is allowed to embed the plugin.
-  if (!GetContentClient()->browser()->ShouldAllowPluginCreation(embedder_origin,
-                                                                *info)) {
-    return nullptr;
-  }
-
   PpapiPluginProcessHost* plugin_host =
       FindPpapiPluginProcess(plugin_path, profile_data_directory, origin_lock);
   if (plugin_host)
@@ -173,14 +166,12 @@
 
 void PluginServiceImpl::OpenChannelToPpapiPlugin(
     int render_process_id,
-    const url::Origin& embedder_origin,
     const base::FilePath& plugin_path,
     const base::FilePath& profile_data_directory,
     const absl::optional<url::Origin>& origin_lock,
     PpapiPluginProcessHost::PluginClient* client) {
   PpapiPluginProcessHost* plugin_host = FindOrStartPpapiPluginProcess(
-      render_process_id, embedder_origin, plugin_path, profile_data_directory,
-      origin_lock);
+      render_process_id, plugin_path, profile_data_directory, origin_lock);
   if (plugin_host) {
     plugin_host->OpenChannelToPlugin(client);
   } else {
diff --git a/content/browser/plugin_service_impl.h b/content/browser/plugin_service_impl.h
index 9f256a7f..f0e36ba 100644
--- a/content/browser/plugin_service_impl.h
+++ b/content/browser/plugin_service_impl.h
@@ -87,7 +87,6 @@
   // is NULL. Must be called on the IO thread.
   PpapiPluginProcessHost* FindOrStartPpapiPluginProcess(
       int render_process_id,
-      const url::Origin& embedder_origin,
       const base::FilePath& plugin_path,
       const base::FilePath& profile_data_directory,
       const absl::optional<url::Origin>& origin_lock);
@@ -96,7 +95,6 @@
   // a new plugin process if necessary.  This must be called on the IO thread
   // or else a deadlock can occur.
   void OpenChannelToPpapiPlugin(int render_process_id,
-                                const url::Origin& embedder_origin,
                                 const base::FilePath& plugin_path,
                                 const base::FilePath& profile_data_directory,
                                 const absl::optional<url::Origin>& origin_lock,
diff --git a/content/browser/plugin_service_impl_browsertest.cc b/content/browser/plugin_service_impl_browsertest.cc
index 2b2ea99e..25d7001 100644
--- a/content/browser/plugin_service_impl_browsertest.cc
+++ b/content/browser/plugin_service_impl_browsertest.cc
@@ -86,8 +86,7 @@
 
     PluginServiceImpl* service = PluginServiceImpl::GetInstance();
     service->OpenChannelToPpapiPlugin(
-        /*render_process_id=*/0, /*embedder_origin=*/url::Origin(),
-        plugin_path_, profile_dir_, origin, client);
+        /*render_process_id=*/0, plugin_path_, profile_dir_, origin, client);
     client->WaitForQuit();
     client->SetRunLoop(nullptr);
   }
diff --git a/content/browser/private_aggregation/private_aggregation_features.cc b/content/browser/private_aggregation/private_aggregation_features.cc
new file mode 100644
index 0000000..9913b09
--- /dev/null
+++ b/content/browser/private_aggregation/private_aggregation_features.cc
@@ -0,0 +1,12 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/private_aggregation/private_aggregation_features.h"
+
+namespace content {
+
+const base::Feature kPrivateAggregationApi = {
+    "PrivateAggregationApi", base::FEATURE_DISABLED_BY_DEFAULT};
+
+}  // namespace content
diff --git a/content/browser/private_aggregation/private_aggregation_features.h b/content/browser/private_aggregation/private_aggregation_features.h
new file mode 100644
index 0000000..5c0b2d4
--- /dev/null
+++ b/content/browser/private_aggregation/private_aggregation_features.h
@@ -0,0 +1,19 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_PRIVATE_AGGREGATION_PRIVATE_AGGREGATION_FEATURES_H_
+#define CONTENT_BROWSER_PRIVATE_AGGREGATION_PRIVATE_AGGREGATION_FEATURES_H_
+
+#include "base/feature_list.h"
+
+namespace content {
+
+// Enables the Private Aggregation API. Note that this API also requires the
+// `kPrivacySandboxAggregationService` to be enabled to successfully send
+// reports.
+extern const base::Feature kPrivateAggregationApi;
+
+}  // namespace content
+
+#endif  // CONTENT_BROWSER_PRIVATE_AGGREGATION_PRIVATE_AGGREGATION_FEATURES_H_
diff --git a/content/browser/private_aggregation/private_aggregation_manager.cc b/content/browser/private_aggregation/private_aggregation_manager.cc
new file mode 100644
index 0000000..1597b0dc
--- /dev/null
+++ b/content/browser/private_aggregation/private_aggregation_manager.cc
@@ -0,0 +1,20 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/private_aggregation/private_aggregation_manager.h"
+
+#include "content/browser/private_aggregation/private_aggregation_manager_impl.h"
+#include "content/browser/storage_partition_impl.h"
+#include "content/public/browser/browser_context.h"
+
+namespace content {
+
+PrivateAggregationManager* PrivateAggregationManager::GetManager(
+    BrowserContext* browser_context) {
+  return static_cast<StoragePartitionImpl*>(
+             browser_context->GetDefaultStoragePartition())
+      ->GetPrivateAggregationManager();
+}
+
+}  // namespace content
diff --git a/content/browser/private_aggregation/private_aggregation_manager.h b/content/browser/private_aggregation/private_aggregation_manager.h
index f3154ad..c50766c 100644
--- a/content/browser/private_aggregation/private_aggregation_manager.h
+++ b/content/browser/private_aggregation/private_aggregation_manager.h
@@ -15,12 +15,16 @@
 
 namespace content {
 
+class BrowserContext;
+
 // Interface that mediates data flow between the Private Aggregation API
 // component and other APIs using it.
 class PrivateAggregationManager {
  public:
   virtual ~PrivateAggregationManager() = default;
 
+  static PrivateAggregationManager* GetManager(BrowserContext* browser_context);
+
   // Binds a new pending receiver for a worklet, allowing messages to be sent
   // and processed. However, the receiver is not bound if the `worklet_origin`
   // is not potentially trustworthy. The return value indicates whether the
diff --git a/content/browser/private_aggregation/private_aggregation_manager_impl.h b/content/browser/private_aggregation/private_aggregation_manager_impl.h
index 8c2210bb..48a9977 100644
--- a/content/browser/private_aggregation/private_aggregation_manager_impl.h
+++ b/content/browser/private_aggregation/private_aggregation_manager_impl.h
@@ -28,7 +28,8 @@
 class PrivateAggregationHost;
 
 // UI thread class that manages the lifetime of the other classes,
-// coordinates report requests, and interfaces with other directories.
+// coordinates report requests, and interfaces with other directories. Lifetime
+// is bound to lifetime of the `StoragePartitionImpl`.
 // TODO(crbug.com/1323325): Integrate with aggregation service.
 class CONTENT_EXPORT PrivateAggregationManagerImpl
     : public PrivateAggregationManager {
diff --git a/content/browser/renderer_host/navigation_controller_impl.cc b/content/browser/renderer_host/navigation_controller_impl.cc
index 29b9642..40b1947 100644
--- a/content/browser/renderer_host/navigation_controller_impl.cc
+++ b/content/browser/renderer_host/navigation_controller_impl.cc
@@ -354,7 +354,8 @@
     const FrameNavigationEntry& frame_entry,
     bool has_pending_cross_document_commit,
     bool is_currently_error_page,
-    bool is_same_document_history_load) {
+    bool is_same_document_history_load,
+    bool is_unfenced_top_navigation) {
   // Reload navigations
   switch (reload_type) {
     case ReloadType::NORMAL:
@@ -381,7 +382,11 @@
       // If the current document is an error page, we should always treat it as
       // a different-document navigation so that we'll attempt to load the
       // document we're navigating to (and not stay in the current error page).
-      !is_currently_error_page;
+      !is_currently_error_page &&
+      // If the navigation is to _unfencedTop (i.e. escapes a fenced frame),
+      // same-document navigations should be disabled because we want to force
+      // the creation of a new browsing context group.
+      !is_unfenced_top_navigation;
 
   // History navigations.
   if (frame_entry.page_state().IsValid()) {
@@ -2633,7 +2638,8 @@
     bool is_form_submission,
     const absl::optional<blink::Impression>& impression,
     base::TimeTicks navigation_start_time,
-    bool is_embedder_initiated_fenced_frame_navigation) {
+    bool is_embedder_initiated_fenced_frame_navigation,
+    bool is_unfenced_top_navigation) {
   if (is_renderer_initiated)
     DCHECK(initiator_origin.has_value());
 
@@ -2769,7 +2775,8 @@
           node, params, override_user_agent, should_replace_current_entry,
           false /* has_user_gesture */, std::move(source_location),
           ReloadType::NONE, entry.get(), frame_entry.get(),
-          navigation_start_time, is_embedder_initiated_fenced_frame_navigation);
+          navigation_start_time, is_embedder_initiated_fenced_frame_navigation,
+          is_unfenced_top_navigation);
 
   if (!request)
     return;
@@ -3659,7 +3666,8 @@
     NavigationEntryImpl* entry,
     FrameNavigationEntry* frame_entry,
     base::TimeTicks navigation_start_time,
-    bool is_embedder_initiated_fenced_frame_navigation) {
+    bool is_embedder_initiated_fenced_frame_navigation,
+    bool is_unfenced_top_navigation) {
   DCHECK_EQ(-1, GetIndexOfEntry(entry));
   DCHECK(frame_entry);
   // All renderer-initiated navigations must have an initiator_origin.
@@ -3735,7 +3743,7 @@
       /*old_url=*/node->current_url(),
       /*new_url=*/url_to_load, reload_type, entry, *frame_entry,
       has_pending_cross_document_commit, is_currently_error_page,
-      /*is_same_document_history_load=*/false);
+      /*is_same_document_history_load=*/false, is_unfenced_top_navigation);
 
   // Create the NavigationParams based on |params|.
 
@@ -3912,7 +3920,8 @@
       /*old_url=*/frame_tree_node->current_url(),
       /*new_url=*/dest_url, reload_type, entry, *frame_entry,
       has_pending_cross_document_commit, is_currently_error_page,
-      is_same_document_history_load);
+      is_same_document_history_load,
+      /*is_unfenced_top_navigation=*/false);
 
   // A form submission may happen here if the navigation is a
   // back/forward/reload navigation that does a form resubmission.
diff --git a/content/browser/renderer_host/navigation_controller_impl.h b/content/browser/renderer_host/navigation_controller_impl.h
index e7f492d..b6392e7c 100644
--- a/content/browser/renderer_host/navigation_controller_impl.h
+++ b/content/browser/renderer_host/navigation_controller_impl.h
@@ -220,7 +220,8 @@
       bool is_form_submission,
       const absl::optional<blink::Impression>& impression,
       base::TimeTicks navigation_start_time,
-      bool is_embedder_initiated_fenced_frame_navigation = false);
+      bool is_embedder_initiated_fenced_frame_navigation = false,
+      bool is_unfenced_top_navigation = false);
 
   // Navigates to the history entry associated with the given navigation API
   // |key|. Searches |entries_| for a FrameNavigationEntry associated with
@@ -578,7 +579,8 @@
       NavigationEntryImpl* entry,
       FrameNavigationEntry* frame_entry,
       base::TimeTicks navigation_start_time,
-      bool is_embedder_initiated_fenced_frame_navigation = false);
+      bool is_embedder_initiated_fenced_frame_navigation = false,
+      bool is_unfenced_top_navigation = false);
 
   // Creates and returns a NavigationRequest for a navigation to |entry|. Will
   // return nullptr if the parameters are invalid and the navigation cannot
diff --git a/content/browser/renderer_host/navigator.cc b/content/browser/renderer_host/navigator.cc
index 6db78d7..8caea1c 100644
--- a/content/browser/renderer_host/navigator.cc
+++ b/content/browser/renderer_host/navigator.cc
@@ -837,7 +837,8 @@
     bool is_form_submission,
     const absl::optional<blink::Impression>& impression,
     base::TimeTicks navigation_start_time,
-    bool is_embedder_initiated_fenced_frame_navigation) {
+    bool is_embedder_initiated_fenced_frame_navigation,
+    bool is_unfenced_top_navigation) {
   // |method != "POST"| should imply absence of |post_body|.
   if (method != "POST" && post_body) {
     NOTREACHED();
@@ -880,7 +881,8 @@
       download_policy, method, post_body, extra_headers,
       std::move(source_location), std::move(blob_url_loader_factory),
       is_form_submission, impression, navigation_start_time,
-      is_embedder_initiated_fenced_frame_navigation);
+      is_embedder_initiated_fenced_frame_navigation,
+      is_unfenced_top_navigation);
 }
 
 void Navigator::BeforeUnloadCompleted(FrameTreeNode* frame_tree_node,
diff --git a/content/browser/renderer_host/navigator.h b/content/browser/renderer_host/navigator.h
index 301b7fb..52b8b0e 100644
--- a/content/browser/renderer_host/navigator.h
+++ b/content/browser/renderer_host/navigator.h
@@ -163,7 +163,8 @@
       bool is_form_submission,
       const absl::optional<blink::Impression>& impression,
       base::TimeTicks navigation_start_time,
-      bool is_embedder_initiated_fenced_frame_navigation = false);
+      bool is_embedder_initiated_fenced_frame_navigation = false,
+      bool is_unfenced_top_navigation = false);
 
   // Called after BeforeUnloadCompleted callback is invoked from the renderer.
   // If |frame_tree_node| has a NavigationRequest waiting for the renderer
diff --git a/content/browser/renderer_host/pepper/pepper_renderer_connection.cc b/content/browser/renderer_host/pepper/pepper_renderer_connection.cc
index 3e1c2ae1..1123a29 100644
--- a/content/browser/renderer_host/pepper/pepper_renderer_connection.cc
+++ b/content/browser/renderer_host/pepper/pepper_renderer_connection.cc
@@ -353,8 +353,7 @@
   }
 
   plugin_service_->OpenChannelToPpapiPlugin(
-      render_process_id_, embedder_origin, path, profile_data_directory_,
-      origin_lock,
+      render_process_id_, path, profile_data_directory_, origin_lock,
       new OpenChannelToPpapiPluginCallback(this, std::move(callback)));
 }
 
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 586f85a..cb9e54b 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -7166,7 +7166,9 @@
         /*post_body=*/nullptr, params->extra_headers,
         /*blob_url_loader_factory=*/nullptr,
         network::mojom::SourceLocation::New(), /*has_user_gesture=*/false,
-        params->is_form_submission, params->impression, base::TimeTicks::Now());
+        params->is_form_submission, params->impression, base::TimeTicks::Now(),
+        /*is_embedder_initiated_fenced_frame_navigation=*/false,
+        /*is_unfenced_top_navigation=*/true);
     return;
   }
 
diff --git a/content/browser/storage_partition_impl.cc b/content/browser/storage_partition_impl.cc
index ab92124..4aeaaf7 100644
--- a/content/browser/storage_partition_impl.cc
+++ b/content/browser/storage_partition_impl.cc
@@ -78,6 +78,8 @@
 #include "content/browser/notifications/platform_notification_context_impl.h"
 #include "content/browser/payments/payment_app_context_impl.h"
 #include "content/browser/preloading/prerender/prerender_host_registry.h"
+#include "content/browser/private_aggregation/private_aggregation_features.h"
+#include "content/browser/private_aggregation/private_aggregation_manager_impl.h"
 #include "content/browser/push_messaging/push_messaging_context.h"
 #include "content/browser/quota/quota_context.h"
 #include "content/browser/renderer_host/frame_tree_node.h"
@@ -1388,6 +1390,11 @@
     shared_storage_manager_ = std::make_unique<storage::SharedStorageManager>(
         shared_storage_path, special_storage_policy_);
   }
+
+  if (base::FeatureList::IsEnabled(kPrivateAggregationApi)) {
+    private_aggregation_manager_ =
+        std::make_unique<PrivateAggregationManagerImpl>(is_in_memory(), path);
+  }
 }
 
 void StoragePartitionImpl::OnStorageServiceDisconnected() {
@@ -1726,6 +1733,12 @@
   return shared_storage_manager_.get();
 }
 
+PrivateAggregationManagerImpl*
+StoragePartitionImpl::GetPrivateAggregationManager() {
+  DCHECK(initialized_);
+  return private_aggregation_manager_.get();
+}
+
 void StoragePartitionImpl::OpenLocalStorage(
     const blink::StorageKey& storage_key,
     const blink::LocalFrameToken& local_frame_token,
@@ -2534,7 +2547,7 @@
     // TODO(crbug.com/1286173): Consider adding aggregation service origins to
     // `CookiesTreeModel`.
     aggregation_service->ClearData(
-        begin, end,
+        begin, end, filter,
         CreateTaskCompletionClosure(TracingDataType::kAggregationService));
   }
 
diff --git a/content/browser/storage_partition_impl.h b/content/browser/storage_partition_impl.h
index 3eaa89f..46da9dc2b 100644
--- a/content/browser/storage_partition_impl.h
+++ b/content/browser/storage_partition_impl.h
@@ -88,6 +88,7 @@
 class NativeIOContextImpl;
 class PaymentAppContextImpl;
 class PrefetchURLLoaderService;
+class PrivateAggregationManagerImpl;
 class PushMessagingContext;
 class QuotaContext;
 class SharedStorageWorkletHostManager;
@@ -263,6 +264,7 @@
   // Gets the SharedStorageManager for the StoragePartition, or nullptr if it
   // doesn't exist because the feature is disabled.
   storage::SharedStorageManager* GetSharedStorageManager();
+  PrivateAggregationManagerImpl* GetPrivateAggregationManager();
 
   // blink::mojom::DomStorage interface.
   void OpenLocalStorage(
@@ -665,6 +667,8 @@
   std::unique_ptr<SharedStorageWorkletHostManager>
       shared_storage_worklet_host_manager_;
 
+  std::unique_ptr<PrivateAggregationManagerImpl> private_aggregation_manager_;
+
   // ReceiverSet for DomStorage, using the
   // ChildProcessSecurityPolicyImpl::Handle as the binding context type. The
   // handle can subsequently be used during interface method calls to
diff --git a/content/browser/storage_partition_impl_unittest.cc b/content/browser/storage_partition_impl_unittest.cc
index a0865f3..d1c2f28 100644
--- a/content/browser/storage_partition_impl_unittest.cc
+++ b/content/browser/storage_partition_impl_unittest.cc
@@ -550,6 +550,7 @@
               ClearData,
               (base::Time delete_begin,
                base::Time delete_end,
+               StoragePartition::StorageKeyMatcherFunction filter,
                base::OnceClosure done),
               (override));
 };
@@ -2074,51 +2075,98 @@
   const uint32_t kTestQuotaClearMask =
       StoragePartition::QUOTA_MANAGED_STORAGE_MASK_ALL;
   const auto kTestOrigin = GURL("https://example.com");
+  const auto kOtherOrigin = GURL("https://example.net");
   const auto kBeginTime = base::Time() + base::Hours(1);
   const auto kEndTime = base::Time() + base::Hours(2);
   const auto invoke_callback =
       [](base::Time delete_begin, base::Time delete_end,
+         StoragePartition::StorageKeyMatcherFunction filter,
          base::OnceClosure done) { std::move(done).Run(); };
+  const auto is_test_origin_valid =
+      [&kTestOrigin](
+          content::StoragePartition::StorageKeyMatcherFunction filter) {
+        return filter.Run(blink::StorageKey(url::Origin::Create(kTestOrigin)));
+      };
+  const auto is_other_origin_valid =
+      [&kOtherOrigin](
+          content::StoragePartition::StorageKeyMatcherFunction filter) {
+        return filter.Run(blink::StorageKey(url::Origin::Create(kOtherOrigin)));
+      };
+  const auto is_filter_null =
+      [&](content::StoragePartition::StorageKeyMatcherFunction filter) {
+        return filter.is_null();
+      };
 
   // Verify that each of the StoragePartition interfaces for clearing origin
   // based data calls aggregation service appropriately.
+  EXPECT_CALL(
+      *aggregation_service_ptr,
+      ClearData(
+          base::Time(), base::Time::Max(),
+          testing::AllOf(testing::Truly(is_test_origin_valid),
+                         testing::Not(testing::Truly(is_other_origin_valid))),
+          testing::_))
+      .WillOnce(invoke_callback);
+  {
+    base::RunLoop run_loop;
+    partition->ClearDataForOrigin(kTestClearMask, kTestQuotaClearMask,
+                                  kTestOrigin, run_loop.QuitClosure());
+    run_loop.Run();
+    testing::Mock::VerifyAndClearExpectations(aggregation_service_ptr);
+  }
+
+  EXPECT_CALL(
+      *aggregation_service_ptr,
+      ClearData(
+          kBeginTime, kEndTime,
+          testing::AllOf(testing::Truly(is_test_origin_valid),
+                         testing::Not(testing::Truly(is_other_origin_valid))),
+          testing::_))
+      .WillOnce(testing::Invoke(invoke_callback));
+  {
+    base::RunLoop run_loop;
+    partition->ClearData(kTestClearMask, kTestQuotaClearMask,
+                         blink::StorageKey(url::Origin::Create(kTestOrigin)),
+                         kBeginTime, kEndTime, run_loop.QuitClosure());
+    run_loop.Run();
+    testing::Mock::VerifyAndClearExpectations(aggregation_service_ptr);
+  }
+
+  EXPECT_CALL(
+      *aggregation_service_ptr,
+      ClearData(
+          kBeginTime, kEndTime,
+          testing::AllOf(testing::Truly(is_test_origin_valid),
+                         testing::Not(testing::Truly(is_other_origin_valid))),
+          testing::_))
+      .WillOnce(testing::Invoke(invoke_callback));
+  {
+    base::RunLoop run_loop;
+    partition->ClearData(
+        kTestClearMask, kTestQuotaClearMask,
+        base::BindLambdaForTesting([&](const blink::StorageKey& storage_key,
+                                       storage::SpecialStoragePolicy* policy) {
+          return storage_key ==
+                 blink::StorageKey(url::Origin::Create(kTestOrigin));
+        }),
+        /*cookie_deletion_filter=*/nullptr,
+        /*perform_storage_cleanup=*/false, kBeginTime, kEndTime,
+        run_loop.QuitClosure());
+    run_loop.Run();
+    testing::Mock::VerifyAndClearExpectations(aggregation_service_ptr);
+  }
 
   EXPECT_CALL(*aggregation_service_ptr,
-              ClearData(base::Time(), base::Time::Max(), testing::_))
+              ClearData(kBeginTime, kEndTime, testing::Truly(is_filter_null),
+                        testing::_))
       .WillOnce(testing::Invoke(invoke_callback));
-  base::RunLoop run_loop;
-  partition->ClearDataForOrigin(kTestClearMask, kTestQuotaClearMask,
-                                kTestOrigin, run_loop.QuitClosure());
-  run_loop.Run();
-  testing::Mock::VerifyAndClearExpectations(aggregation_service_ptr);
-
-  EXPECT_CALL(*aggregation_service_ptr,
-              ClearData(kBeginTime, kEndTime, testing::_))
-      .WillOnce(testing::Invoke(invoke_callback));
-  partition->ClearData(kTestClearMask, kTestQuotaClearMask,
-                       blink::StorageKey(url::Origin::Create(kTestOrigin)),
-                       kBeginTime, kEndTime, base::DoNothing());
-  testing::Mock::VerifyAndClearExpectations(aggregation_service_ptr);
-
-  EXPECT_CALL(*aggregation_service_ptr,
-              ClearData(kBeginTime, kEndTime, testing::_))
-      .WillOnce(testing::Invoke(invoke_callback));
-  partition->ClearData(
-      kTestClearMask, kTestQuotaClearMask,
-      base::BindLambdaForTesting([&](const blink::StorageKey& storage_key,
-                                     storage::SpecialStoragePolicy* policy) {
-        return storage_key ==
-               blink::StorageKey(url::Origin::Create(kTestOrigin));
-      }),
-      /*cookie_deletion_filter=*/nullptr, /*perform_storage_cleanup=*/false,
-      kBeginTime, kEndTime, base::DoNothing());
-  testing::Mock::VerifyAndClearExpectations(aggregation_service_ptr);
-
-  EXPECT_CALL(*aggregation_service_ptr,
-              ClearData(kBeginTime, kEndTime, testing::_))
-      .WillOnce(testing::Invoke(invoke_callback));
-  partition->ClearData(kTestClearMask, kTestQuotaClearMask, blink::StorageKey(),
-                       kBeginTime, kEndTime, base::DoNothing());
+  {
+    base::RunLoop run_loop;
+    partition->ClearData(kTestClearMask, kTestQuotaClearMask,
+                         blink::StorageKey(), kBeginTime, kEndTime,
+                         run_loop.QuitClosure());
+    run_loop.Run();
+  }
 }
 
 // https://crbug.com/1221382
diff --git a/content/child/runtime_features.cc b/content/child/runtime_features.cc
index 1f91a77..a34397c8 100644
--- a/content/child/runtime_features.cc
+++ b/content/child/runtime_features.cc
@@ -376,6 +376,8 @@
           {"PendingBeaconAPI", blink::features::kPendingBeaconAPI},
           {"PrefersColorSchemeClientHintHeader",
            blink::features::kPrefersColorSchemeClientHintHeader},
+          {"PrivateNetworkAccessPermissionPrompt",
+           blink::features::kPrivateNetworkAccessPermissionPrompt},
           {"FirstPartySets", features::kFirstPartySets},
           {"QuickIntensiveWakeUpThrottlingAfterLoading",
            blink::features::kQuickIntensiveWakeUpThrottlingAfterLoading},
diff --git a/content/common/user_agent.cc b/content/common/user_agent.cc
index 8745028..cdfbe98a 100644
--- a/content/common/user_agent.cc
+++ b/content/common/user_agent.cc
@@ -29,6 +29,13 @@
 
 namespace {
 
+const char kFrozenUserAgentTemplate[] =
+    "Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 "
+#if BUILDFLAG(IS_ANDROID)
+    "%s"
+#endif
+    "Safari/537.36";
+
 std::string GetUserAgentPlatform() {
 #if BUILDFLAG(IS_WIN)
   return "";
@@ -45,26 +52,30 @@
 #endif
 }
 
-}  // namespace
-
 std::string GetUnifiedPlatform() {
 #if BUILDFLAG(IS_ANDROID)
-  return frozen_user_agent_strings::kUnifiedPlatformAndroid;
+  return "Linux; Android 10; K";
 #elif BUILDFLAG(IS_CHROMEOS)
-  return frozen_user_agent_strings::kUnifiedPlatformCrOS;
+  return "X11; CrOS x86_64 14541.0.0";
 #elif BUILDFLAG(IS_MAC)
-  return frozen_user_agent_strings::kUnifiedPlatformMacOS;
+  return "Macintosh; Intel Mac OS X 10_15_7";
 #elif BUILDFLAG(IS_WIN)
-  return frozen_user_agent_strings::kUnifiedPlatformWindows;
+  return "Windows NT 10.0; Win64; x64";
 #elif BUILDFLAG(IS_FUCHSIA)
-  return frozen_user_agent_strings::kUnifiedPlatformFuchsia;
+  return "Fuchsia";
 #elif BUILDFLAG(IS_LINUX)
-  return frozen_user_agent_strings::kUnifiedPlatformLinux;
+  return "X11; Linux x86_64";
 #else
 #error Unsupported platform
 #endif
 }
 
+}  // namespace
+
+std::string GetUnifiedPlatformForTesting() {
+  return GetUnifiedPlatform();
+}
+
 // Inaccurately named for historical reasons
 std::string GetWebKitVersion() {
   return base::StringPrintf("537.36 (%s)", CHROMIUM_GIT_REVISION);
@@ -284,10 +295,10 @@
   device_compat = mobile ? "Mobile " : "";
 #endif
   std::string user_agent =
-      base::StringPrintf(frozen_user_agent_strings::kTemplate,
-                         GetUnifiedPlatform().c_str(), major_version.c_str()
+      base::StringPrintf(kFrozenUserAgentTemplate, GetUnifiedPlatform().c_str(),
+                         major_version.c_str()
 #if BUILDFLAG(IS_ANDROID)
-                                                           ,
+                             ,
                          device_compat.c_str()
 #endif
       );
diff --git a/content/public/browser/content_browser_client.cc b/content/public/browser/content_browser_client.cc
index 6f443c2..5f50e84 100644
--- a/content/public/browser/content_browser_client.cc
+++ b/content/public/browser/content_browser_client.cc
@@ -1260,14 +1260,6 @@
   return false;
 }
 
-#if BUILDFLAG(ENABLE_PLUGINS)
-bool ContentBrowserClient::ShouldAllowPluginCreation(
-    const url::Origin& embedder_origin,
-    const content::PepperPluginInfo& plugin_info) {
-  return true;
-}
-#endif
-
 #if BUILDFLAG(ENABLE_VR)
 XrIntegrationClient* ContentBrowserClient::GetXrIntegrationClient() {
   return nullptr;
diff --git a/content/public/browser/content_browser_client.h b/content/public/browser/content_browser_client.h
index cadbd44..614766cd 100644
--- a/content/public/browser/content_browser_client.h
+++ b/content/public/browser/content_browser_client.h
@@ -45,7 +45,6 @@
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "mojo/public/cpp/system/message_pipe.h"
-#include "ppapi/buildflags/buildflags.h"
 #include "services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom-forward.h"
 #include "services/metrics/public/cpp/ukm_source_id.h"
 #include "services/network/public/mojom/ip_address_space.mojom-forward.h"
@@ -240,7 +239,6 @@
 struct GlobalRenderFrameHostId;
 struct GlobalRequestID;
 struct OpenURLParams;
-struct PepperPluginInfo;
 struct Referrer;
 struct ServiceWorkerVersionBaseInfo;
 struct SocketPermissionRequest;
@@ -2150,14 +2148,6 @@
   // fullscreen when mock screen orientation changes.
   virtual bool CanEnterFullscreenWithoutUserActivation();
 
-#if BUILDFLAG(ENABLE_PLUGINS)
-  // Returns true if |embedder_origin| is allowed to embed a plugin described by
-  // |plugin_info|.  This method allows restricting some internal plugins (like
-  // Chrome's PDF plugin) to specific origins.
-  virtual bool ShouldAllowPluginCreation(const url::Origin& embedder_origin,
-                                         const PepperPluginInfo& plugin_info);
-#endif
-
 #if BUILDFLAG(ENABLE_VR)
   // Allows the embedder to provide mechanisms to integrate with WebXR
   // functionality.
diff --git a/content/public/common/content_switch_dependent_feature_overrides.cc b/content/public/common/content_switch_dependent_feature_overrides.cc
index 8edde5fc..96c1204 100644
--- a/content/public/common/content_switch_dependent_feature_overrides.cc
+++ b/content/public/common/content_switch_dependent_feature_overrides.cc
@@ -66,9 +66,6 @@
      std::cref(features::kBlockInsecurePrivateNetworkRequestsFromUnknown),
      base::FeatureList::OVERRIDE_ENABLE_FEATURE},
     {switches::kEnableExperimentalWebPlatformFeatures,
-     std::cref(features::kPrivateNetworkAccessPermissionPrompt),
-     base::FeatureList::OVERRIDE_ENABLE_FEATURE},
-    {switches::kEnableExperimentalWebPlatformFeatures,
      std::cref(features::kPrivateNetworkAccessForWorkers),
      base::FeatureList::OVERRIDE_ENABLE_FEATURE},
     {switches::kEnableExperimentalWebPlatformFeatures,
diff --git a/content/public/common/user_agent.h b/content/public/common/user_agent.h
index bf6f9af..ead4bc4 100644
--- a/content/public/common/user_agent.h
+++ b/content/public/common/user_agent.h
@@ -13,24 +13,6 @@
 
 namespace content {
 
-namespace frozen_user_agent_strings {
-
-const char kTemplate[] =
-    "Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 "
-#if BUILDFLAG(IS_ANDROID)
-    "%s"
-#endif
-    "Safari/537.36";
-
-const char kUnifiedPlatformAndroid[] = "Linux; Android 10; K";
-const char kUnifiedPlatformCrOS[] = "X11; CrOS x86_64 14541.0.0";
-const char kUnifiedPlatformFuchsia[] = "Fuchsia";
-const char kUnifiedPlatformLinux[] = "X11; Linux x86_64";
-const char kUnifiedPlatformMacOS[] = "Macintosh; Intel Mac OS X 10_15_7";
-const char kUnifiedPlatformWindows[] = "Windows NT 10.0; Win64; x64";
-
-}  // namespace frozen_user_agent_strings
-
 enum class IncludeAndroidBuildNumber { Include, Exclude };
 enum class IncludeAndroidModel { Include, Exclude };
 
@@ -77,9 +59,10 @@
 CONTENT_EXPORT std::string GetReducedUserAgent(bool mobile,
                                                std::string major_version);
 
-// Helper function to return the <unifiedPlatform> token of a reduced
-// User-Agent header
-CONTENT_EXPORT std::string GetUnifiedPlatform();
+// TODO(crbug.com/1257310): Remove this after user agent reduction phase 5 and
+// --force-major-version-to-minor is removed.
+// Return the <unifiedPlatform> token of a reduced User-Agent header.
+CONTENT_EXPORT std::string GetUnifiedPlatformForTesting();
 
 // Helper function to generate a full user agent string from a short
 // product name.
diff --git a/content/renderer/v8_value_converter_impl_unittest.cc b/content/renderer/v8_value_converter_impl_unittest.cc
index 64980ac..b62c2452 100644
--- a/content/renderer/v8_value_converter_impl_unittest.cc
+++ b/content/renderer/v8_value_converter_impl_unittest.cc
@@ -1021,9 +1021,10 @@
     ASSERT_EQ(2u, list_result->GetListDeprecated().size());
     for (size_t i = 0; i < list_result->GetListDeprecated().size(); ++i) {
       ASSERT_FALSE(IsNull(list_result.get(), i));
-      base::DictionaryValue* dict_value = nullptr;
-      ASSERT_TRUE(list_result->GetDictionary(0u, &dict_value));
-      EXPECT_EQ("same value", GetString(dict_value, "key"));
+      base::Value::Dict* dict_value = list_result->GetList()[0].GetIfDict();
+      ;
+      ASSERT_TRUE(dict_value);
+      EXPECT_STREQ("same value", dict_value->FindString("key")->c_str());
     }
   }
 }
diff --git a/content/test/data/first_party_sets/v0.init_too_old.sql b/content/test/data/first_party_sets/v0.init_too_old.sql
index bf8a5ef..e4ffcdb 100644
--- a/content/test/data/first_party_sets/v0.init_too_old.sql
+++ b/content/test/data/first_party_sets/v0.init_too_old.sql
@@ -9,12 +9,12 @@
 
 CREATE INDEX idx_marked_at_run_sites ON sites_to_clear (marked_at_run);
 
-CREATE TABLE IF NOT EXISTS profiles_cleared (
-   profile TEXT PRIMARY KEY NOT NULL,
+CREATE TABLE IF NOT EXISTS browser_contexts_cleared (
+   browser_context_id TEXT PRIMARY KEY NOT NULL,
    cleared_at_run Integer NOT NULL
 ) WITHOUT ROWID;
 
-CREATE INDEX idx_cleared_at_run_profiles ON profiles_cleared (cleared_at_run);
+CREATE INDEX idx_cleared_at_run_browser_contexts ON browser_contexts_cleared (cleared_at_run);
 
 CREATE TABLE meta(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY, value LONGVARCHAR);
 
@@ -23,6 +23,6 @@
 INSERT INTO meta VALUES('run_count','1');
 
 INSERT INTO sites_to_clear VALUES('https://example.test', 1);
-INSERT INTO profiles_cleared VALUES('p', 1);
+INSERT INTO browser_contexts_cleared VALUES('p', 1);
 
 COMMIT;
\ No newline at end of file
diff --git a/content/test/data/first_party_sets/v1.init_invalid_run_count.sql b/content/test/data/first_party_sets/v1.init_invalid_run_count.sql
index e501123..af26f23 100644
--- a/content/test/data/first_party_sets/v1.init_invalid_run_count.sql
+++ b/content/test/data/first_party_sets/v1.init_invalid_run_count.sql
@@ -9,12 +9,12 @@
 
 CREATE INDEX idx_marked_at_run_sites ON sites_to_clear (marked_at_run);
 
-CREATE TABLE IF NOT EXISTS profiles_cleared (
-   profile TEXT PRIMARY KEY NOT NULL,
+CREATE TABLE IF NOT EXISTS browser_contexts_cleared (
+   browser_context_id TEXT PRIMARY KEY NOT NULL,
    cleared_at_run Integer NOT NULL
 ) WITHOUT ROWID;
 
-CREATE INDEX idx_cleared_at_run_profiles ON profiles_cleared (cleared_at_run);
+CREATE INDEX idx_cleared_at_run_browser_contexts ON browser_contexts_cleared (cleared_at_run);
 
 CREATE TABLE meta(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY, value LONGVARCHAR);
 
@@ -23,6 +23,6 @@
 INSERT INTO meta VALUES('run_count','0');
 
 INSERT INTO sites_to_clear VALUES('https://example.test', 2);
-INSERT INTO profiles_cleared VALUES('p', 2);
+INSERT INTO browser_contexts_cleared VALUES('p', 2);
 
 COMMIT;
\ No newline at end of file
diff --git a/content/test/data/first_party_sets/v1.init_too_new.sql b/content/test/data/first_party_sets/v1.init_too_new.sql
index 8a7048f..ef31021 100644
--- a/content/test/data/first_party_sets/v1.init_too_new.sql
+++ b/content/test/data/first_party_sets/v1.init_too_new.sql
@@ -9,12 +9,12 @@
 
 CREATE INDEX idx_marked_at_run_sites ON sites_to_clear (marked_at_run);
 
-CREATE TABLE IF NOT EXISTS profiles_cleared (
-   profile TEXT PRIMARY KEY NOT NULL,
+CREATE TABLE IF NOT EXISTS browser_contexts_cleared (
+   browser_context_id TEXT PRIMARY KEY NOT NULL,
    cleared_at_run Integer NOT NULL
 ) WITHOUT ROWID;
 
-CREATE INDEX idx_cleared_at_run_profiles ON profiles_cleared (cleared_at_run);
+CREATE INDEX idx_cleared_at_run_browser_contexts ON browser_contexts_cleared (cleared_at_run);
 
 CREATE TABLE meta(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY, value LONGVARCHAR);
 
@@ -23,6 +23,6 @@
 INSERT INTO meta VALUES('run_count','2');
 
 INSERT INTO sites_to_clear VALUES('https://example.test', 2);
-INSERT INTO profiles_cleared VALUES('p', 2);
+INSERT INTO browser_contexts_cleared VALUES('p', 2);
 
 COMMIT;
\ No newline at end of file
diff --git a/content/test/data/first_party_sets/v1.sql b/content/test/data/first_party_sets/v1.sql
index 444d75d..4a246f2d 100644
--- a/content/test/data/first_party_sets/v1.sql
+++ b/content/test/data/first_party_sets/v1.sql
@@ -9,12 +9,12 @@
 
 CREATE INDEX idx_marked_at_run_sites ON sites_to_clear (marked_at_run);
 
-CREATE TABLE IF NOT EXISTS profiles_cleared (
-   profile TEXT PRIMARY KEY NOT NULL,
+CREATE TABLE IF NOT EXISTS browser_contexts_cleared (
+   browser_context_id TEXT PRIMARY KEY NOT NULL,
    cleared_at_run Integer NOT NULL
 ) WITHOUT ROWID;
 
-CREATE INDEX idx_cleared_at_run_profiles ON profiles_cleared (cleared_at_run);
+CREATE INDEX idx_cleared_at_run_browser_contexts ON browser_contexts_cleared (cleared_at_run);
 
 CREATE TABLE meta(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY, value LONGVARCHAR);
 
@@ -23,6 +23,6 @@
 INSERT INTO meta VALUES('run_count','1');
 
 INSERT INTO sites_to_clear VALUES('https://example.test', 1);
-INSERT INTO profiles_cleared VALUES('p', 1);
+INSERT INTO browser_contexts_cleared VALUES('p', 1);
 
 COMMIT;
\ No newline at end of file
diff --git a/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt
index 8bc8c6e..765a75f 100644
--- a/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt
@@ -303,6 +303,7 @@
 
 # Still fails on Nexus 5 after all other Pixel_CanvasLowLatency* pass.
 crbug.com/1097752 [ android android-nexus-5 ] Pixel_CanvasLowLatencyWebGLAlphaFalse [ Failure ]
+crbug.com/1097752 [ android android-nexus-5 ] Pixel_VideoStreamFromWebGLCanvas [ Failure ]
 
 # Fails on Fuchsia emulators
 crbug.com/1302427 [ fuchsia-board-qemu-x64 ] Pixel_CanvasLowLatencyWebGLAlphaFalse [ Failure ]
diff --git a/content/test/gpu/gpu_tests/webgl_conformance_integration_test.py b/content/test/gpu/gpu_tests/webgl_conformance_integration_test.py
index df7720c..0e4e9c6 100644
--- a/content/test/gpu/gpu_tests/webgl_conformance_integration_test.py
+++ b/content/test/gpu/gpu_tests/webgl_conformance_integration_test.py
@@ -69,8 +69,11 @@
 SERIAL_TESTS = {}
 
 SERIAL_TEST_GLOBS = {
-    # crbug.com/1345466.
+    # crbug.com/1345466. Can be removed once OpenGL is no longer used on Mac.
     'deqp/functional/gles3/transformfeedback/*',
+    # crbug.com/1345782. Can be removed once OpenGL is no longer used on Mac.
+    'deqp/functional/gles3/texturefiltering/*',
+    'deqp/functional/gles3/texturespecification/*',
 }
 
 
diff --git a/device/bluetooth/dbus/fake_bluetooth_device_client.cc b/device/bluetooth/dbus/fake_bluetooth_device_client.cc
index 2647b85..9ee22add 100644
--- a/device/bluetooth/dbus/fake_bluetooth_device_client.cc
+++ b/device/bluetooth/dbus/fake_bluetooth_device_client.cc
@@ -325,7 +325,8 @@
       &FakeBluetoothDeviceClient::OnPropertyChanged, base::Unretained(this),
       dbus::ObjectPath(kPairedDevicePath)));
   properties->address.ReplaceValue(kPairedDeviceAddress);
-  properties->bluetooth_class.ReplaceValue(kPairedDeviceClass);
+  properties->bluetooth_class.ReplaceValue(
+      static_cast<int>(kPairedDeviceClass));
   properties->name.ReplaceValue(kPairedDeviceName);
   properties->name.set_valid(true);
   properties->alias.ReplaceValue(kPairedDeviceAlias);
@@ -909,238 +910,239 @@
 
 base::Value FakeBluetoothDeviceClient::GetBluetoothDevicesAsDictionaries()
     const {
-  base::Value::ListStorage predefined_devices;
+  base::Value::List predefined_devices;
 
-  base::Value paired_device(base::Value::Type::DICTIONARY);
-  paired_device.SetStringKey("path", kPairedDevicePath);
-  paired_device.SetStringKey("address", kPairedDeviceAddress);
-  paired_device.SetStringKey("name", kPairedDeviceName);
-  paired_device.SetStringKey("alias", kPairedDeviceName);
-  paired_device.SetStringKey("pairingMethod", "");
-  paired_device.SetStringKey("pairingAuthToken", "");
-  paired_device.SetStringKey("pairingAction", "");
-  paired_device.SetIntKey("classValue", kPairedDeviceClass);
-  paired_device.SetBoolKey("discoverable", true);
-  paired_device.SetBoolKey("isTrusted", true);
-  paired_device.SetBoolKey("paired", true);
-  paired_device.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(paired_device));
+  base::Value::Dict paired_device;
+  paired_device.Set("path", kPairedDevicePath);
+  paired_device.Set("address", kPairedDeviceAddress);
+  paired_device.Set("name", kPairedDeviceName);
+  paired_device.Set("alias", kPairedDeviceName);
+  paired_device.Set("pairingMethod", "");
+  paired_device.Set("pairingAuthToken", "");
+  paired_device.Set("pairingAction", "");
+  paired_device.Set("classValue", static_cast<int>(kPairedDeviceClass));
+  paired_device.Set("discoverable", true);
+  paired_device.Set("isTrusted", true);
+  paired_device.Set("paired", true);
+  paired_device.Set("incoming", false);
+  predefined_devices.Append(std::move(paired_device));
 
-  base::Value legacy_device(base::Value::Type::DICTIONARY);
-  legacy_device.SetStringKey("path", kLegacyAutopairPath);
-  legacy_device.SetStringKey("address", kLegacyAutopairAddress);
-  legacy_device.SetStringKey("name", kLegacyAutopairName);
-  legacy_device.SetStringKey("alias", kLegacyAutopairName);
-  legacy_device.SetStringKey("pairingMethod", "");
-  legacy_device.SetStringKey("pairingAuthToken", "");
-  legacy_device.SetStringKey("pairingAction", "");
-  legacy_device.SetIntKey("classValue", kLegacyAutopairClass);
-  legacy_device.SetBoolKey("isTrusted", true);
-  legacy_device.SetBoolKey("discoverable", false);
-  legacy_device.SetBoolKey("paired", false);
-  legacy_device.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(legacy_device));
+  base::Value::Dict legacy_device;
+  legacy_device.Set("path", kLegacyAutopairPath);
+  legacy_device.Set("address", kLegacyAutopairAddress);
+  legacy_device.Set("name", kLegacyAutopairName);
+  legacy_device.Set("alias", kLegacyAutopairName);
+  legacy_device.Set("pairingMethod", "");
+  legacy_device.Set("pairingAuthToken", "");
+  legacy_device.Set("pairingAction", "");
+  legacy_device.Set("classValue", static_cast<int>(kLegacyAutopairClass));
+  legacy_device.Set("isTrusted", true);
+  legacy_device.Set("discoverable", false);
+  legacy_device.Set("paired", false);
+  legacy_device.Set("incoming", false);
+  predefined_devices.Append(std::move(legacy_device));
 
-  base::Value pin(base::Value::Type::DICTIONARY);
-  pin.SetStringKey("path", kDisplayPinCodePath);
-  pin.SetStringKey("address", kDisplayPinCodeAddress);
-  pin.SetStringKey("name", kDisplayPinCodeName);
-  pin.SetStringKey("alias", kDisplayPinCodeName);
-  pin.SetStringKey("pairingMethod", kPairingMethodPinCode);
-  pin.SetStringKey("pairingAuthToken", kTestPinCode);
-  pin.SetStringKey("pairingAction", kPairingActionDisplay);
-  pin.SetIntKey("classValue", kDisplayPinCodeClass);
-  pin.SetBoolKey("isTrusted", false);
-  pin.SetBoolKey("discoverable", false);
-  pin.SetBoolKey("paired", false);
-  pin.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(pin));
+  base::Value::Dict pin;
+  pin.Set("path", kDisplayPinCodePath);
+  pin.Set("address", kDisplayPinCodeAddress);
+  pin.Set("name", kDisplayPinCodeName);
+  pin.Set("alias", kDisplayPinCodeName);
+  pin.Set("pairingMethod", kPairingMethodPinCode);
+  pin.Set("pairingAuthToken", kTestPinCode);
+  pin.Set("pairingAction", kPairingActionDisplay);
+  pin.Set("classValue", static_cast<int>(kDisplayPinCodeClass));
+  pin.Set("isTrusted", false);
+  pin.Set("discoverable", false);
+  pin.Set("paired", false);
+  pin.Set("incoming", false);
+  predefined_devices.Append(std::move(pin));
 
-  base::Value vanishing(base::Value::Type::DICTIONARY);
-  vanishing.SetStringKey("path", kVanishingDevicePath);
-  vanishing.SetStringKey("address", kVanishingDeviceAddress);
-  vanishing.SetStringKey("name", kVanishingDeviceName);
-  vanishing.SetStringKey("alias", kVanishingDeviceName);
-  vanishing.SetStringKey("pairingMethod", "");
-  vanishing.SetStringKey("pairingAuthToken", "");
-  vanishing.SetStringKey("pairingAction", "");
-  vanishing.SetIntKey("classValue", kVanishingDeviceClass);
-  vanishing.SetBoolKey("isTrusted", false);
-  vanishing.SetBoolKey("discoverable", false);
-  vanishing.SetBoolKey("paired", false);
-  vanishing.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(vanishing));
+  base::Value::Dict vanishing;
+  vanishing.Set("path", kVanishingDevicePath);
+  vanishing.Set("address", kVanishingDeviceAddress);
+  vanishing.Set("name", kVanishingDeviceName);
+  vanishing.Set("alias", kVanishingDeviceName);
+  vanishing.Set("pairingMethod", "");
+  vanishing.Set("pairingAuthToken", "");
+  vanishing.Set("pairingAction", "");
+  vanishing.Set("classValue", static_cast<int>(kVanishingDeviceClass));
+  vanishing.Set("isTrusted", false);
+  vanishing.Set("discoverable", false);
+  vanishing.Set("paired", false);
+  vanishing.Set("incoming", false);
+  predefined_devices.Append(std::move(vanishing));
 
-  base::Value connect_unpairable(base::Value::Type::DICTIONARY);
-  connect_unpairable.SetStringKey("path", kConnectUnpairablePath);
-  connect_unpairable.SetStringKey("address", kConnectUnpairableAddress);
-  connect_unpairable.SetStringKey("name", kConnectUnpairableName);
-  connect_unpairable.SetStringKey("pairingMethod", "");
-  connect_unpairable.SetStringKey("pairingAuthToken", "");
-  connect_unpairable.SetStringKey("pairingAction", "");
-  connect_unpairable.SetStringKey("alias", kConnectUnpairableName);
-  connect_unpairable.SetIntKey("classValue", kConnectUnpairableClass);
-  connect_unpairable.SetBoolKey("isTrusted", false);
-  connect_unpairable.SetBoolKey("discoverable", false);
-  connect_unpairable.SetBoolKey("paired", false);
-  connect_unpairable.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(connect_unpairable));
+  base::Value::Dict connect_unpairable;
+  connect_unpairable.Set("path", kConnectUnpairablePath);
+  connect_unpairable.Set("address", kConnectUnpairableAddress);
+  connect_unpairable.Set("name", kConnectUnpairableName);
+  connect_unpairable.Set("pairingMethod", "");
+  connect_unpairable.Set("pairingAuthToken", "");
+  connect_unpairable.Set("pairingAction", "");
+  connect_unpairable.Set("alias", kConnectUnpairableName);
+  connect_unpairable.Set("classValue",
+                         static_cast<int>(kConnectUnpairableClass));
+  connect_unpairable.Set("isTrusted", false);
+  connect_unpairable.Set("discoverable", false);
+  connect_unpairable.Set("paired", false);
+  connect_unpairable.Set("incoming", false);
+  predefined_devices.Append(std::move(connect_unpairable));
 
-  base::Value passkey(base::Value::Type::DICTIONARY);
-  passkey.SetStringKey("path", kDisplayPasskeyPath);
-  passkey.SetStringKey("address", kDisplayPasskeyAddress);
-  passkey.SetStringKey("name", kDisplayPasskeyName);
-  passkey.SetStringKey("alias", kDisplayPasskeyName);
-  passkey.SetStringKey("pairingMethod", kPairingMethodPassKey);
-  passkey.SetIntKey("pairingAuthToken", kTestPassKey);
-  passkey.SetStringKey("pairingAction", kPairingActionDisplay);
-  passkey.SetIntKey("classValue", kDisplayPasskeyClass);
-  passkey.SetBoolKey("isTrusted", false);
-  passkey.SetBoolKey("discoverable", false);
-  passkey.SetBoolKey("paired", false);
-  passkey.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(passkey));
+  base::Value::Dict passkey;
+  passkey.Set("path", kDisplayPasskeyPath);
+  passkey.Set("address", kDisplayPasskeyAddress);
+  passkey.Set("name", kDisplayPasskeyName);
+  passkey.Set("alias", kDisplayPasskeyName);
+  passkey.Set("pairingMethod", kPairingMethodPassKey);
+  passkey.Set("pairingAuthToken", kTestPassKey);
+  passkey.Set("pairingAction", kPairingActionDisplay);
+  passkey.Set("classValue", static_cast<int>(kDisplayPasskeyClass));
+  passkey.Set("isTrusted", false);
+  passkey.Set("discoverable", false);
+  passkey.Set("paired", false);
+  passkey.Set("incoming", false);
+  predefined_devices.Append(std::move(passkey));
 
-  base::Value request_pin(base::Value::Type::DICTIONARY);
-  request_pin.SetStringKey("path", kRequestPinCodePath);
-  request_pin.SetStringKey("address", kRequestPinCodeAddress);
-  request_pin.SetStringKey("name", kRequestPinCodeName);
-  request_pin.SetStringKey("alias", kRequestPinCodeName);
-  request_pin.SetStringKey("pairingMethod", "");
-  request_pin.SetStringKey("pairingAuthToken", "");
-  request_pin.SetStringKey("pairingAction", kPairingActionRequest);
-  request_pin.SetIntKey("classValue", kRequestPinCodeClass);
-  request_pin.SetBoolKey("isTrusted", false);
-  request_pin.SetBoolKey("discoverable", false);
-  request_pin.SetBoolKey("paired", false);
-  request_pin.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(request_pin));
+  base::Value::Dict request_pin;
+  request_pin.Set("path", kRequestPinCodePath);
+  request_pin.Set("address", kRequestPinCodeAddress);
+  request_pin.Set("name", kRequestPinCodeName);
+  request_pin.Set("alias", kRequestPinCodeName);
+  request_pin.Set("pairingMethod", "");
+  request_pin.Set("pairingAuthToken", "");
+  request_pin.Set("pairingAction", kPairingActionRequest);
+  request_pin.Set("classValue", static_cast<int>(kRequestPinCodeClass));
+  request_pin.Set("isTrusted", false);
+  request_pin.Set("discoverable", false);
+  request_pin.Set("paired", false);
+  request_pin.Set("incoming", false);
+  predefined_devices.Append(std::move(request_pin));
 
-  base::Value confirm(base::Value::Type::DICTIONARY);
-  confirm.SetStringKey("path", kConfirmPasskeyPath);
-  confirm.SetStringKey("address", kConfirmPasskeyAddress);
-  confirm.SetStringKey("name", kConfirmPasskeyName);
-  confirm.SetStringKey("alias", kConfirmPasskeyName);
-  confirm.SetStringKey("pairingMethod", "");
-  confirm.SetIntKey("pairingAuthToken", kTestPassKey);
-  confirm.SetStringKey("pairingAction", kPairingActionConfirmation);
-  confirm.SetIntKey("classValue", kConfirmPasskeyClass);
-  confirm.SetBoolKey("isTrusted", false);
-  confirm.SetBoolKey("discoverable", false);
-  confirm.SetBoolKey("paired", false);
-  confirm.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(confirm));
+  base::Value::Dict confirm;
+  confirm.Set("path", kConfirmPasskeyPath);
+  confirm.Set("address", kConfirmPasskeyAddress);
+  confirm.Set("name", kConfirmPasskeyName);
+  confirm.Set("alias", kConfirmPasskeyName);
+  confirm.Set("pairingMethod", "");
+  confirm.Set("pairingAuthToken", kTestPassKey);
+  confirm.Set("pairingAction", kPairingActionConfirmation);
+  confirm.Set("classValue", static_cast<int>(kConfirmPasskeyClass));
+  confirm.Set("isTrusted", false);
+  confirm.Set("discoverable", false);
+  confirm.Set("paired", false);
+  confirm.Set("incoming", false);
+  predefined_devices.Append(std::move(confirm));
 
-  base::Value request_passkey(base::Value::Type::DICTIONARY);
-  request_passkey.SetStringKey("path", kRequestPasskeyPath);
-  request_passkey.SetStringKey("address", kRequestPasskeyAddress);
-  request_passkey.SetStringKey("name", kRequestPasskeyName);
-  request_passkey.SetStringKey("alias", kRequestPasskeyName);
-  request_passkey.SetStringKey("pairingMethod", kPairingMethodPassKey);
-  request_passkey.SetStringKey("pairingAction", kPairingActionRequest);
-  request_passkey.SetIntKey("pairingAuthToken", kTestPassKey);
-  request_passkey.SetIntKey("classValue", kRequestPasskeyClass);
-  request_passkey.SetBoolKey("isTrusted", false);
-  request_passkey.SetBoolKey("discoverable", false);
-  request_passkey.SetBoolKey("paired", false);
-  request_passkey.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(request_passkey));
+  base::Value::Dict request_passkey;
+  request_passkey.Set("path", kRequestPasskeyPath);
+  request_passkey.Set("address", kRequestPasskeyAddress);
+  request_passkey.Set("name", kRequestPasskeyName);
+  request_passkey.Set("alias", kRequestPasskeyName);
+  request_passkey.Set("pairingMethod", kPairingMethodPassKey);
+  request_passkey.Set("pairingAction", kPairingActionRequest);
+  request_passkey.Set("pairingAuthToken", kTestPassKey);
+  request_passkey.Set("classValue", static_cast<int>(kRequestPasskeyClass));
+  request_passkey.Set("isTrusted", false);
+  request_passkey.Set("discoverable", false);
+  request_passkey.Set("paired", false);
+  request_passkey.Set("incoming", false);
+  predefined_devices.Append(std::move(request_passkey));
 
-  base::Value unconnectable(base::Value::Type::DICTIONARY);
-  unconnectable.SetStringKey("path", kUnconnectableDevicePath);
-  unconnectable.SetStringKey("address", kUnconnectableDeviceAddress);
-  unconnectable.SetStringKey("name", kUnconnectableDeviceName);
-  unconnectable.SetStringKey("alias", kUnconnectableDeviceName);
-  unconnectable.SetStringKey("pairingMethod", "");
-  unconnectable.SetStringKey("pairingAuthToken", "");
-  unconnectable.SetStringKey("pairingAction", "");
-  unconnectable.SetIntKey("classValue", kUnconnectableDeviceClass);
-  unconnectable.SetBoolKey("isTrusted", true);
-  unconnectable.SetBoolKey("discoverable", false);
-  unconnectable.SetBoolKey("paired", false);
-  unconnectable.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(unconnectable));
+  base::Value::Dict unconnectable;
+  unconnectable.Set("path", kUnconnectableDevicePath);
+  unconnectable.Set("address", kUnconnectableDeviceAddress);
+  unconnectable.Set("name", kUnconnectableDeviceName);
+  unconnectable.Set("alias", kUnconnectableDeviceName);
+  unconnectable.Set("pairingMethod", "");
+  unconnectable.Set("pairingAuthToken", "");
+  unconnectable.Set("pairingAction", "");
+  unconnectable.Set("classValue", static_cast<int>(kUnconnectableDeviceClass));
+  unconnectable.Set("isTrusted", true);
+  unconnectable.Set("discoverable", false);
+  unconnectable.Set("paired", false);
+  unconnectable.Set("incoming", false);
+  predefined_devices.Append(std::move(unconnectable));
 
-  base::Value unpairable(base::Value::Type::DICTIONARY);
-  unpairable.SetStringKey("path", kUnpairableDevicePath);
-  unpairable.SetStringKey("address", kUnpairableDeviceAddress);
-  unpairable.SetStringKey("name", kUnpairableDeviceName);
-  unpairable.SetStringKey("alias", kUnpairableDeviceName);
-  unpairable.SetStringKey("pairingMethod", "");
-  unpairable.SetStringKey("pairingAuthToken", "");
-  unpairable.SetStringKey("pairingAction", kPairingActionFail);
-  unpairable.SetIntKey("classValue", kUnpairableDeviceClass);
-  unpairable.SetBoolKey("isTrusted", false);
-  unpairable.SetBoolKey("discoverable", false);
-  unpairable.SetBoolKey("paired", false);
-  unpairable.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(unpairable));
+  base::Value::Dict unpairable;
+  unpairable.Set("path", kUnpairableDevicePath);
+  unpairable.Set("address", kUnpairableDeviceAddress);
+  unpairable.Set("name", kUnpairableDeviceName);
+  unpairable.Set("alias", kUnpairableDeviceName);
+  unpairable.Set("pairingMethod", "");
+  unpairable.Set("pairingAuthToken", "");
+  unpairable.Set("pairingAction", kPairingActionFail);
+  unpairable.Set("classValue", static_cast<int>(kUnpairableDeviceClass));
+  unpairable.Set("isTrusted", false);
+  unpairable.Set("discoverable", false);
+  unpairable.Set("paired", false);
+  unpairable.Set("incoming", false);
+  predefined_devices.Append(std::move(unpairable));
 
-  base::Value just_works(base::Value::Type::DICTIONARY);
-  just_works.SetStringKey("path", kJustWorksPath);
-  just_works.SetStringKey("address", kJustWorksAddress);
-  just_works.SetStringKey("name", kJustWorksName);
-  just_works.SetStringKey("alias", kJustWorksName);
-  just_works.SetStringKey("pairingMethod", "");
-  just_works.SetStringKey("pairingAuthToken", "");
-  just_works.SetStringKey("pairingAction", "");
-  just_works.SetIntKey("classValue", kJustWorksClass);
-  just_works.SetBoolKey("isTrusted", false);
-  just_works.SetBoolKey("discoverable", false);
-  just_works.SetBoolKey("paired", false);
-  just_works.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(just_works));
+  base::Value::Dict just_works;
+  just_works.Set("path", kJustWorksPath);
+  just_works.Set("address", kJustWorksAddress);
+  just_works.Set("name", kJustWorksName);
+  just_works.Set("alias", kJustWorksName);
+  just_works.Set("pairingMethod", "");
+  just_works.Set("pairingAuthToken", "");
+  just_works.Set("pairingAction", "");
+  just_works.Set("classValue", static_cast<int>(kJustWorksClass));
+  just_works.Set("isTrusted", false);
+  just_works.Set("discoverable", false);
+  just_works.Set("paired", false);
+  just_works.Set("incoming", false);
+  predefined_devices.Append(std::move(just_works));
 
-  base::Value low_energy(base::Value::Type::DICTIONARY);
-  low_energy.SetStringKey("path", kLowEnergyPath);
-  low_energy.SetStringKey("address", kLowEnergyAddress);
-  low_energy.SetStringKey("name", kLowEnergyName);
-  low_energy.SetStringKey("alias", kLowEnergyName);
-  low_energy.SetStringKey("pairingMethod", "");
-  low_energy.SetStringKey("pairingAuthToken", "");
-  low_energy.SetStringKey("pairingAction", "");
-  low_energy.SetIntKey("classValue", kLowEnergyClass);
-  low_energy.SetBoolKey("isTrusted", false);
-  low_energy.SetBoolKey("discoverable", false);
-  low_energy.SetBoolKey("paireed", false);
-  low_energy.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(low_energy));
+  base::Value::Dict low_energy;
+  low_energy.Set("path", kLowEnergyPath);
+  low_energy.Set("address", kLowEnergyAddress);
+  low_energy.Set("name", kLowEnergyName);
+  low_energy.Set("alias", kLowEnergyName);
+  low_energy.Set("pairingMethod", "");
+  low_energy.Set("pairingAuthToken", "");
+  low_energy.Set("pairingAction", "");
+  low_energy.Set("classValue", static_cast<int>(kLowEnergyClass));
+  low_energy.Set("isTrusted", false);
+  low_energy.Set("discoverable", false);
+  low_energy.Set("paireed", false);
+  low_energy.Set("incoming", false);
+  predefined_devices.Append(std::move(low_energy));
 
-  base::Value paired_unconnectable(base::Value::Type::DICTIONARY);
-  paired_unconnectable.SetStringKey("path", kPairedUnconnectableDevicePath);
-  paired_unconnectable.SetStringKey("address",
-                                    kPairedUnconnectableDeviceAddress);
-  paired_unconnectable.SetStringKey("name", kPairedUnconnectableDeviceName);
-  paired_unconnectable.SetStringKey("pairingMethod", "");
-  paired_unconnectable.SetStringKey("pairingAuthToken", "");
-  paired_unconnectable.SetStringKey("pairingAction", "");
-  paired_unconnectable.SetStringKey("alias", kPairedUnconnectableDeviceName);
-  paired_unconnectable.SetIntKey("classValue", kPairedUnconnectableDeviceClass);
-  paired_unconnectable.SetBoolKey("isTrusted", false);
-  paired_unconnectable.SetBoolKey("discoverable", true);
-  paired_unconnectable.SetBoolKey("paired", true);
-  paired_unconnectable.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(paired_unconnectable));
+  base::Value::Dict paired_unconnectable;
+  paired_unconnectable.Set("path", kPairedUnconnectableDevicePath);
+  paired_unconnectable.Set("address", kPairedUnconnectableDeviceAddress);
+  paired_unconnectable.Set("name", kPairedUnconnectableDeviceName);
+  paired_unconnectable.Set("pairingMethod", "");
+  paired_unconnectable.Set("pairingAuthToken", "");
+  paired_unconnectable.Set("pairingAction", "");
+  paired_unconnectable.Set("alias", kPairedUnconnectableDeviceName);
+  paired_unconnectable.Set("classValue",
+                           static_cast<int>(kPairedUnconnectableDeviceClass));
+  paired_unconnectable.Set("isTrusted", false);
+  paired_unconnectable.Set("discoverable", true);
+  paired_unconnectable.Set("paired", true);
+  paired_unconnectable.Set("incoming", false);
+  predefined_devices.Append(std::move(paired_unconnectable));
 
-  base::Value connected_trusted_not_paired(base::Value::Type::DICTIONARY);
-  connected_trusted_not_paired.SetStringKey(
-      "path", kConnectedTrustedNotPairedDevicePath);
-  connected_trusted_not_paired.SetStringKey(
-      "address", kConnectedTrustedNotPairedDeviceAddress);
-  connected_trusted_not_paired.SetStringKey(
-      "name", kConnectedTrustedNotPairedDeviceName);
-  connected_trusted_not_paired.SetStringKey("pairingMethod", "");
-  connected_trusted_not_paired.SetStringKey("pairingAuthToken", "");
-  connected_trusted_not_paired.SetStringKey("pairingAction", "");
-  connected_trusted_not_paired.SetStringKey(
-      "alias", kConnectedTrustedNotPairedDeviceName);
-  connected_trusted_not_paired.SetIntKey("classValue",
-                                         kConnectedTrustedNotPairedDeviceClass);
-  connected_trusted_not_paired.SetBoolKey("isTrusted", true);
-  connected_trusted_not_paired.SetBoolKey("discoverable", true);
-  connected_trusted_not_paired.SetBoolKey("paired", false);
-  connected_trusted_not_paired.SetBoolKey("incoming", false);
-  predefined_devices.push_back(std::move(connected_trusted_not_paired));
+  base::Value::Dict connected_trusted_not_paired;
+  connected_trusted_not_paired.Set("path",
+                                   kConnectedTrustedNotPairedDevicePath);
+  connected_trusted_not_paired.Set("address",
+                                   kConnectedTrustedNotPairedDeviceAddress);
+  connected_trusted_not_paired.Set("name",
+                                   kConnectedTrustedNotPairedDeviceName);
+  connected_trusted_not_paired.Set("pairingMethod", "");
+  connected_trusted_not_paired.Set("pairingAuthToken", "");
+  connected_trusted_not_paired.Set("pairingAction", "");
+  connected_trusted_not_paired.Set("alias",
+                                   kConnectedTrustedNotPairedDeviceName);
+  connected_trusted_not_paired.Set(
+      "classValue", static_cast<int>(kConnectedTrustedNotPairedDeviceClass));
+  connected_trusted_not_paired.Set("isTrusted", true);
+  connected_trusted_not_paired.Set("discoverable", true);
+  connected_trusted_not_paired.Set("paired", false);
+  connected_trusted_not_paired.Set("incoming", false);
+  predefined_devices.Append(std::move(connected_trusted_not_paired));
 
   return base::Value(std::move(predefined_devices));
 }
diff --git a/device/bluetooth/dbus/fake_bluetooth_device_client.h b/device/bluetooth/dbus/fake_bluetooth_device_client.h
index 8b4ef62..1d84f4cdb 100644
--- a/device/bluetooth/dbus/fake_bluetooth_device_client.h
+++ b/device/bluetooth/dbus/fake_bluetooth_device_client.h
@@ -146,9 +146,8 @@
   void CreateDeviceWithProperties(const dbus::ObjectPath& adapter_path,
                                   const IncomingDeviceProperties& props);
 
-  // Creates and returns a list of std::unique_ptr<base::DictionaryValue>
-  // objects, which contain all the data from the constants for devices with
-  // predefined behavior.
+  // Creates and returns a list of dictionary objects as a Value, which contain
+  // all the data from the constants for devices with predefined behavior.
   base::Value GetBluetoothDevicesAsDictionaries() const;
 
   SimulatedPairingOptions* GetPairingOptions(
diff --git a/extensions/common/extension_builder.h b/extensions/common/extension_builder.h
index bbe2596..227e631b 100644
--- a/extensions/common/extension_builder.h
+++ b/extensions/common/extension_builder.h
@@ -8,6 +8,7 @@
 #include <initializer_list>
 #include <memory>
 #include <string>
+#include <utility>
 
 #include "base/files/file_path.h"
 #include "base/memory/ref_counted.h"
@@ -113,15 +114,15 @@
   // Can be used in conjuction with ListBuilder and DictionaryBuilder for more
   // complex types.
   template <typename T>
-  ExtensionBuilder& SetManifestKey(base::StringPiece key, T value) {
-    SetManifestKeyImpl(key, base::Value(value));
+  ExtensionBuilder& SetManifestKey(base::StringPiece key, T&& value) {
+    SetManifestKeyImpl(key, base::Value(std::forward<T>(value)));
     return *this;
   }
   template <typename T>
   ExtensionBuilder& SetManifestPath(
       std::initializer_list<base::StringPiece> path,
-      T value) {
-    SetManifestPathImpl(path, base::Value(value));
+      T&& value) {
+    SetManifestPathImpl(path, base::Value(std::forward<T>(value)));
     return *this;
   }
   // Specializations for unique_ptr<> to allow passing unique_ptr<base::Value>.
diff --git a/fuchsia_web/webengine/BUILD.gn b/fuchsia_web/webengine/BUILD.gn
index f39a256..76607982 100644
--- a/fuchsia_web/webengine/BUILD.gn
+++ b/fuchsia_web/webengine/BUILD.gn
@@ -17,6 +17,11 @@
   defines = [ "WEB_ENGINE_IMPLEMENTATION" ]
 }
 
+source_set("web_engine_export_from_implementation") {
+  public = [ "web_engine_export.h" ]
+  public_configs = [ ":web_engine_implementation" ]
+}
+
 fidl_library("fidl") {
   library_name = "chromium.internal"
   sources = [ "fidl/dev_tools.fidl" ]
@@ -79,8 +84,10 @@
 
 component("web_engine_core") {
   deps = [
+    ":context_provider",
     ":fidl",
     ":switches",
+    ":web_engine_export_from_implementation",
     "//base",
     "//base:base_static",
     "//components/cast/message_port:message_port_fuchsia",
@@ -120,10 +127,6 @@
     "//content/public/renderer",
     "//fuchsia_web/common",
     "//fuchsia_web/webengine/mojom",
-
-    # TODO(crbug.com/1081525): Move context_provider to its own target and move
-    # this deps there.
-    "//fuchsia_web/webinstance_host",
     "//google_apis",
     "//gpu/command_buffer/service",
     "//media",
@@ -234,8 +237,6 @@
     "//third_party/fuchsia-sdk/sdk/pkg/inspect",
   ]
 
-  configs += [ ":web_engine_implementation" ]
-
   sources = [
     "browser/accessibility_bridge.cc",
     "browser/accessibility_bridge.h",
@@ -301,10 +302,6 @@
     "common/cors_exempt_headers.h",
     "common/web_engine_content_client.cc",
     "common/web_engine_content_client.h",
-    "context_provider_impl.cc",
-    "context_provider_impl.h",
-    "context_provider_main.cc",
-    "context_provider_main.h",
     "renderer/web_engine_audio_device_factory.cc",
     "renderer/web_engine_audio_device_factory.h",
     "renderer/web_engine_audio_renderer.cc",
@@ -317,12 +314,30 @@
     "renderer/web_engine_render_frame_observer.h",
     "renderer/web_engine_url_loader_throttle_provider.cc",
     "renderer/web_engine_url_loader_throttle_provider.h",
-    "web_engine_export.h",
     "web_engine_main_delegate.cc",
     "web_engine_main_delegate.h",
   ]
 }
 
+source_set("context_provider") {
+  sources = [
+    "context_provider_impl.cc",
+    "context_provider_impl.h",
+    "context_provider_main.cc",
+    "context_provider_main.h",
+  ]
+  deps = [
+    ":fidl",
+    ":web_engine_export_from_implementation",
+    "//base",
+    "//components/fuchsia_component_support",
+    "//fuchsia_web/common",
+    "//fuchsia_web/webinstance_host",
+    "//third_party/fuchsia-sdk/sdk/pkg/sys_cpp",
+    "//third_party/fuchsia-sdk/sdk/pkg/sys_inspect_cpp",
+  ]
+}
+
 # TODO(crbug.com/1081525): Rename to features_and_switches or collapse into
 # common. Consider moving these and other files in engine/ to common/ or
 # elsewhere.
@@ -338,6 +353,7 @@
 
 executable("web_engine_exe") {
   deps = [
+    ":context_provider",
     ":switches",
     ":web_engine_core",
     "//base",
@@ -589,6 +605,7 @@
     "test/run_all_unittests.cc",
   ]
   deps = [
+    ":context_provider",
     ":switches",
     ":web_engine_core",
     ":web_engine_unittests_fake_instance_manifest",
diff --git a/fuchsia_web/webinstance_host/BUILD.gn b/fuchsia_web/webinstance_host/BUILD.gn
index d0e156a..4ca22bcfd 100644
--- a/fuchsia_web/webinstance_host/BUILD.gn
+++ b/fuchsia_web/webinstance_host/BUILD.gn
@@ -12,9 +12,7 @@
     # WebEngine clients that instantiate WebInstances directly.
     "//fuchsia_web/runners/*",
     "//fuchsia_web/shell:web_engine_shell_exec",
-
-    # TODO(crbug.com/1081525): Change to the context_provider target when created.
-    "//fuchsia_web/webengine:web_engine_core",
+    "//fuchsia_web/webengine:context_provider",
 
     # TODO(crbug.com/1081525): Move dependent tests into this directory and
     # source_sets that have these deps.
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc
index b23732b..ff99584 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc
@@ -134,18 +134,35 @@
   GLfloat clear_color_[4];
 };
 
-class ScopedColorMaskReset {
+// Reset the color mask for buffer zero only.
+class ScopedColorMaskZeroReset {
  public:
-  explicit ScopedColorMaskReset(gl::GLApi* api) : api_(api) {
-    api_->glGetBooleanvFn(GL_COLOR_WRITEMASK, color_mask_);
+  explicit ScopedColorMaskZeroReset(gl::GLApi* api,
+                                    bool oes_draw_buffers_indexed)
+      : api_(api), oes_draw_buffers_indexed_(oes_draw_buffers_indexed) {
+    if (oes_draw_buffers_indexed_) {
+      GLsizei length = 0;
+      api_->glGetBooleani_vRobustANGLEFn(
+          GL_COLOR_WRITEMASK, 0, sizeof(color_mask_), &length, color_mask_);
+    } else {
+      api_->glGetBooleanvFn(GL_COLOR_WRITEMASK, color_mask_);
+    }
   }
-  ~ScopedColorMaskReset() {
-    api_->glColorMaskFn(color_mask_[0], color_mask_[1], color_mask_[2],
-                        color_mask_[3]);
+  ~ScopedColorMaskZeroReset() {
+    if (oes_draw_buffers_indexed_) {
+      api_->glColorMaskiOESFn(0, color_mask_[0], color_mask_[1], color_mask_[2],
+                              color_mask_[3]);
+    } else {
+      api_->glColorMaskFn(color_mask_[0], color_mask_[1], color_mask_[2],
+                          color_mask_[3]);
+    }
   }
 
  private:
   raw_ptr<gl::GLApi> api_;
+  const bool oes_draw_buffers_indexed_;
+  // The color mask, or the color mask of buffer zero, if
+  // OES_draw_buffers_indexed is enabled.
   GLboolean color_mask_[4];
 };
 
@@ -399,13 +416,14 @@
 PassthroughResources::SharedImageData::SharedImageData() = default;
 PassthroughResources::SharedImageData::SharedImageData(
     std::unique_ptr<GLTexturePassthroughImageRepresentation> representation,
-    gl::GLApi* api)
+    gl::GLApi* api,
+    const FeatureInfo* feature_info)
     : representation_(std::move(representation)) {
   DCHECK(representation_);
 
   // Note, that ideally we could defer clear till BeginAccess, but there is no
   // enforcement that will require clients to call Begin/End access.
-  EnsureClear(api);
+  EnsureClear(api, feature_info);
 }
 PassthroughResources::SharedImageData::SharedImageData(
     SharedImageData&& other) = default;
@@ -418,7 +436,9 @@
   return *this;
 }
 
-void PassthroughResources::SharedImageData::EnsureClear(gl::GLApi* api) {
+void PassthroughResources::SharedImageData::EnsureClear(
+    gl::GLApi* api,
+    const FeatureInfo* feature_info) {
   // To avoid unnessary overhead we don't enable robust initialization on shared
   // gl context where all shared images are created, so we clear image here if
   // necessary.
@@ -432,13 +452,16 @@
       return;
 
     auto texture = representation_->GetTexturePassthrough();
+    const bool use_oes_draw_buffers_indexed =
+        feature_info->feature_flags().oes_draw_buffers_indexed;
 
     // Back up all state we are about to change.
     ScopedFramebufferBindingReset fbo_reset(
         api, false /* supports_seperate_fbo_bindings */);
     ScopedTextureBindingReset texture_reset(api, texture->target());
     ScopedClearColorReset clear_color_reset(api);
-    ScopedColorMaskReset color_mask_reset(api);
+    ScopedColorMaskZeroReset color_mask_reset(api,
+                                              use_oes_draw_buffers_indexed);
     ScopedScissorTestReset scissor_test_reset(api);
 
     // Generate a new framebuffer and bind the shared image's uncleared texture
@@ -452,7 +475,10 @@
                                      0);
     // Clear the bound framebuffer.
     api->glClearColorFn(0, 0, 0, 0);
-    api->glColorMaskFn(true, true, true, true);
+    if (use_oes_draw_buffers_indexed)
+      api->glColorMaskiOESFn(0, true, true, true, true);
+    else
+      api->glColorMaskFn(true, true, true, true);
     api->glDisableFn(GL_SCISSOR_TEST);
     api->glClearFn(GL_COLOR_BUFFER_BIT);
 
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough.h b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough.h
index c09f1a4..6d48a0d 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough.h
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough.h
@@ -99,7 +99,8 @@
     SharedImageData();
     explicit SharedImageData(
         std::unique_ptr<GLTexturePassthroughImageRepresentation> representation,
-        gl::GLApi* api);
+        gl::GLApi* api,
+        const FeatureInfo* feature_info);
     SharedImageData(SharedImageData&& other);
 
     SharedImageData(const SharedImageData&) = delete;
@@ -112,7 +113,7 @@
       return representation_.get();
     }
 
-    void EnsureClear(gl::GLApi* api);
+    void EnsureClear(gl::GLApi* api, const FeatureInfo* feature_info);
 
     bool BeginAccess(GLenum mode, gl::GLApi* api);
 
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 7b23f76..d962cbb 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
@@ -4885,7 +4885,8 @@
   resources_->texture_object_map.RemoveClientID(texture_client_id);
   resources_->texture_object_map.SetIDMapping(texture_client_id, texture);
   resources_->texture_shared_image_map[texture_client_id] =
-      PassthroughResources::SharedImageData(std::move(shared_image), api());
+      PassthroughResources::SharedImageData(std::move(shared_image), api(),
+                                            feature_info_.get());
 
   return error::kNoError;
 }
diff --git a/headless/BUILD.gn b/headless/BUILD.gn
index cb2a88ebc..0bf719b 100644
--- a/headless/BUILD.gn
+++ b/headless/BUILD.gn
@@ -490,7 +490,7 @@
   if (enable_basic_printing) {
     deps += [
       "//components/printing/browser",
-      "//components/printing/browser/print_to_pdf",
+      "//components/printing/browser/headless",
       "//components/printing/common:mojo_interfaces",
       "//printing",
       "//printing/mojom",
@@ -747,7 +747,7 @@
   if (enable_basic_printing) {
     deps += [
       "//components/printing/browser",
-      "//components/printing/browser/print_to_pdf",
+      "//components/printing/browser/headless",
       "//printing",
       "//printing/buildflags",
       "//third_party/blink/public:blink",
diff --git a/headless/lib/browser/headless_content_browser_client.cc b/headless/lib/browser/headless_content_browser_client.cc
index e2b2cd34..9bc8593 100644
--- a/headless/lib/browser/headless_content_browser_client.cc
+++ b/headless/lib/browser/headless_content_browser_client.cc
@@ -61,7 +61,7 @@
 #endif  // defined(HEADLESS_USE_POLICY)
 
 #if BUILDFLAG(ENABLE_PRINTING)
-#include "components/printing/browser/print_to_pdf/pdf_print_manager.h"
+#include "components/printing/browser/headless/headless_print_manager.h"
 #endif  // defined(ENABLE_PRINTING)
 
 namespace headless {
@@ -153,8 +153,8 @@
       [](content::RenderFrameHost* render_frame_host,
          mojo::PendingAssociatedReceiver<printing::mojom::PrintManagerHost>
              receiver) {
-        print_to_pdf::PdfPrintManager::BindPrintManagerHost(std::move(receiver),
-                                                            render_frame_host);
+        HeadlessPrintManager::BindPrintManagerHost(std::move(receiver),
+                                                   render_frame_host);
       },
       &render_frame_host));
 #endif
diff --git a/headless/lib/browser/headless_web_contents_impl.cc b/headless/lib/browser/headless_web_contents_impl.cc
index 807879aa..aa48857 100644
--- a/headless/lib/browser/headless_web_contents_impl.cc
+++ b/headless/lib/browser/headless_web_contents_impl.cc
@@ -51,7 +51,7 @@
 #include "ui/gfx/switches.h"
 
 #if BUILDFLAG(ENABLE_PRINTING)
-#include "components/printing/browser/print_to_pdf/pdf_print_manager.h"
+#include "components/printing/browser/headless/headless_print_manager.h"
 #endif
 
 namespace headless {
@@ -314,8 +314,7 @@
       agent_host_(
           content::DevToolsAgentHost::GetOrCreateFor(web_contents_.get())) {
 #if BUILDFLAG(ENABLE_PRINTING)
-  print_to_pdf::PdfPrintManager::CreateForWebContents(web_contents_.get());
-// TODO(weili): Add support for printing OOPIFs.
+  HeadlessPrintManager::CreateForWebContents(web_contents_.get());
 #endif
   UpdatePrefsFromSystemSettings(web_contents_->GetMutableRendererPrefs());
   web_contents_->GetMutableRendererPrefs()->accept_languages =
diff --git a/headless/lib/browser/protocol/page_handler.cc b/headless/lib/browser/protocol/page_handler.cc
index edcbbcd..aff24baee 100644
--- a/headless/lib/browser/protocol/page_handler.cc
+++ b/headless/lib/browser/protocol/page_handler.cc
@@ -8,7 +8,6 @@
 #include "content/public/browser/web_contents.h"
 
 #if BUILDFLAG(ENABLE_PRINTING)
-#include "components/printing/browser/print_to_pdf/pdf_print_result.h"
 #include "components/printing/browser/print_to_pdf/pdf_print_utils.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #endif
@@ -90,7 +89,7 @@
 
   bool return_as_stream = transfer_mode.fromMaybe("") ==
                           Page::PrintToPDF::TransferModeEnum::ReturnAsStream;
-  print_to_pdf::PdfPrintManager::FromWebContents(web_contents_.get())
+  HeadlessPrintManager::FromWebContents(web_contents_.get())
       ->PrintToPdf(
           web_contents_->GetPrimaryMainFrame(), page_ranges.fromMaybe(""),
           std::move(absl::get<printing::mojom::PrintPagesParamsPtr>(
diff --git a/headless/lib/browser/protocol/page_handler.h b/headless/lib/browser/protocol/page_handler.h
index 59c47475..99172c7 100644
--- a/headless/lib/browser/protocol/page_handler.h
+++ b/headless/lib/browser/protocol/page_handler.h
@@ -12,7 +12,7 @@
 #include "printing/buildflags/buildflags.h"
 
 #if BUILDFLAG(ENABLE_PRINTING)
-#include "components/printing/browser/print_to_pdf/pdf_print_manager.h"
+#include "components/printing/browser/headless/headless_print_manager.h"
 #include "components/printing/browser/print_to_pdf/pdf_print_result.h"
 #include "headless/public/headless_export.h"
 #endif
diff --git "a/infra/config/generated/builders/ci/mac-arm64-rel \050reclient shadow\051/properties.json" "b/infra/config/generated/builders/ci/mac-arm64-rel \050reclient shadow\051/properties.json"
new file mode 100644
index 0000000..da5d62c
--- /dev/null
+++ "b/infra/config/generated/builders/ci/mac-arm64-rel \050reclient shadow\051/properties.json"
@@ -0,0 +1,55 @@
+{
+  "$build/chromium_tests_builder_config": {
+    "builder_config": {
+      "builder_db": {
+        "entries": [
+          {
+            "builder_id": {
+              "bucket": "ci",
+              "builder": "mac-arm64-rel (reclient shadow)",
+              "project": "chromium"
+            },
+            "builder_spec": {
+              "builder_group": "chromium.mac",
+              "execution_mode": "COMPILE_AND_TEST",
+              "legacy_chromium_config": {
+                "apply_configs": [
+                  "mb"
+                ],
+                "build_config": "Release",
+                "config": "chromium",
+                "target_arch": "arm",
+                "target_bits": 64,
+                "target_platform": "mac"
+              },
+              "legacy_gclient_config": {
+                "config": "chromium"
+              }
+            }
+          }
+        ]
+      },
+      "builder_ids": [
+        {
+          "bucket": "ci",
+          "builder": "mac-arm64-rel (reclient shadow)",
+          "project": "chromium"
+        }
+      ]
+    }
+  },
+  "$build/reclient": {
+    "instance": "rbe-chromium-trusted",
+    "jobs": 40,
+    "metrics_project": "chromium-reclient-metrics"
+  },
+  "$recipe_engine/resultdb/test_presentation": {
+    "column_keys": [],
+    "grouping_keys": [
+      "status",
+      "v.test_suite"
+    ]
+  },
+  "builder_group": "chromium.mac",
+  "recipe": "chromium"
+}
\ No newline at end of file
diff --git a/infra/config/generated/luci/cr-buildbucket.cfg b/infra/config/generated/luci/cr-buildbucket.cfg
index ce2d9ee..87799dce 100644
--- a/infra/config/generated/luci/cr-buildbucket.cfg
+++ b/infra/config/generated/luci/cr-buildbucket.cfg
@@ -15866,6 +15866,75 @@
       }
     }
     builders {
+      name: "Mac deterministic (reclient shadow)"
+      swarming_host: "chromium-swarm.appspot.com"
+      dimensions: "builder:Mac deterministic (reclient shadow)"
+      dimensions: "cpu:x86-64"
+      dimensions: "os:Mac-12"
+      dimensions: "pool:luci.chromium.ci"
+      exe {
+        cipd_package: "infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build"
+        cipd_version: "refs/heads/main"
+        cmd: "luciexe"
+      }
+      properties:
+        '{'
+        '  "$build/reclient": {'
+        '    "instance": "rbe-chromium-trusted",'
+        '    "jobs": 40,'
+        '    "metrics_project": "chromium-reclient-metrics"'
+        '  },'
+        '  "$recipe_engine/resultdb/test_presentation": {'
+        '    "column_keys": [],'
+        '    "grouping_keys": ['
+        '      "status",'
+        '      "v.test_suite"'
+        '    ]'
+        '  },'
+        '  "builder_group": "chromium.fyi",'
+        '  "recipe": "swarming/deterministic_build"'
+        '}'
+      execution_timeout_secs: 21600
+      build_numbers: YES
+      service_account: "chromium-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+      resultdb {
+        enable: true
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "ci_test_results"
+          test_results {}
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "gpu_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://chrome/test:telemetry_gpu_integration_test[^/]*/.+"
+            }
+          }
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "blink_web_tests_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://[^/]*blink_web_tests/.+"
+            }
+          }
+        }
+        history_options {
+          use_invocation_timestamp: true
+        }
+      }
+    }
+    builders {
       name: "Mac10.13 Tests"
       swarming_host: "chromium-swarm.appspot.com"
       dimensions: "builderless:1"
@@ -39487,6 +39556,83 @@
       }
     }
     builders {
+      name: "mac-arm64-rel (reclient shadow)"
+      swarming_host: "chromium-swarm.appspot.com"
+      dimensions: "builder:mac-arm64-rel (reclient shadow)"
+      dimensions: "cpu:x86-64"
+      dimensions: "os:Mac-12"
+      dimensions: "pool:luci.chromium.ci"
+      exe {
+        cipd_package: "infra/chromium/bootstrapper/${platform}"
+        cipd_version: "latest"
+        cmd: "bootstrapper"
+      }
+      properties:
+        '{'
+        '  "$bootstrap/exe": {'
+        '    "exe": {'
+        '      "cipd_package": "infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build",'
+        '      "cipd_version": "refs/heads/main",'
+        '      "cmd": ['
+        '        "luciexe"'
+        '      ]'
+        '    }'
+        '  },'
+        '  "$bootstrap/properties": {'
+        '    "properties_file": "infra/config/generated/builders/ci/mac-arm64-rel (reclient shadow)/properties.json",'
+        '    "top_level_project": {'
+        '      "ref": "refs/heads/main",'
+        '      "repo": {'
+        '        "host": "chromium.googlesource.com",'
+        '        "project": "chromium/src"'
+        '      }'
+        '    }'
+        '  },'
+        '  "builder_group": "chromium.mac",'
+        '  "led_builder_is_bootstrapped": true,'
+        '  "recipe": "chromium"'
+        '}'
+      execution_timeout_secs: 10800
+      build_numbers: YES
+      service_account: "chromium-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+      resultdb {
+        enable: true
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "ci_test_results"
+          test_results {}
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "gpu_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://chrome/test:telemetry_gpu_integration_test[^/]*/.+"
+            }
+          }
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "blink_web_tests_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://[^/]*blink_web_tests/.+"
+            }
+          }
+        }
+        history_options {
+          use_invocation_timestamp: true
+        }
+      }
+    }
+    builders {
       name: "mac-arm64-updater-tester-dbg"
       swarming_host: "chromium-swarm.appspot.com"
       dimensions: "builderless:1"
diff --git a/infra/config/generated/luci/luci-milo.cfg b/infra/config/generated/luci/luci-milo.cfg
index c58384ac..5464462 100644
--- a/infra/config/generated/luci/luci-milo.cfg
+++ b/infra/config/generated/luci/luci-milo.cfg
@@ -211,6 +211,11 @@
     short_name: "bld"
   }
   builders {
+    name: "buildbucket/luci.chromium.ci/mac-arm64-rel (reclient shadow)"
+    category: "chromium.mac|release|arm64"
+    short_name: "rec"
+  }
+  builders {
     name: "buildbucket/luci.chromium.ci/Mac Builder (dbg)"
     category: "chromium.mac|debug"
     short_name: "bld"
@@ -7803,6 +7808,11 @@
     short_name: "dbg"
   }
   builders {
+    name: "buildbucket/luci.chromium.ci/Mac deterministic (reclient shadow)"
+    category: "deterministic|mac"
+    short_name: "rec"
+  }
+  builders {
     name: "buildbucket/luci.chromium.ci/fuchsia-fyi-arm64-cfv2-script"
     category: "fuchsia|a64"
     short_name: "cfv2"
@@ -10843,6 +10853,11 @@
     short_name: "bld"
   }
   builders {
+    name: "buildbucket/luci.chromium.ci/mac-arm64-rel (reclient shadow)"
+    category: "release|arm64"
+    short_name: "rec"
+  }
+  builders {
     name: "buildbucket/luci.chromium.ci/Mac Builder (dbg)"
     category: "debug"
     short_name: "bld"
diff --git a/infra/config/generated/luci/luci-notify.cfg b/infra/config/generated/luci/luci-notify.cfg
index f5add8b..1c32a17 100644
--- a/infra/config/generated/luci/luci-notify.cfg
+++ b/infra/config/generated/luci/luci-notify.cfg
@@ -3636,6 +3636,25 @@
 }
 notifiers {
   notifications {
+    on_occurrence: FAILURE
+    failed_step_regexp: "\\b(bot_update|compile|gclient runhooks|runhooks|update|\\w*nocompile_test)\\b"
+    email {
+      rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
+    }
+    template: "tree_closure_email_template"
+  }
+  builders {
+    bucket: "ci"
+    name: "mac-arm64-rel (reclient shadow)"
+    repository: "https://chromium.googlesource.com/chromium/src"
+  }
+  tree_closers {
+    tree_status_host: "chromium-status.appspot.com"
+    failed_step_regexp: "\\b(bot_update|compile|gclient runhooks|runhooks|update|\\w*nocompile_test)\\b"
+  }
+}
+notifiers {
+  notifications {
     on_change: true
     email {
       recipients: "chrome-memory-safety+bots@google.com"
diff --git a/infra/config/generated/luci/luci-scheduler.cfg b/infra/config/generated/luci/luci-scheduler.cfg
index 68d223af..02cf342 100644
--- a/infra/config/generated/luci/luci-scheduler.cfg
+++ b/infra/config/generated/luci/luci-scheduler.cfg
@@ -2685,6 +2685,16 @@
   }
 }
 job {
+  id: "Mac deterministic (reclient shadow)"
+  realm: "ci"
+  acl_sets: "ci"
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "ci"
+    builder: "Mac deterministic (reclient shadow)"
+  }
+}
+job {
   id: "Mac10.13 Tests"
   realm: "ci"
   acls {
@@ -6469,6 +6479,16 @@
   }
 }
 job {
+  id: "mac-arm64-rel (reclient shadow)"
+  realm: "ci"
+  acl_sets: "ci"
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "ci"
+    builder: "mac-arm64-rel (reclient shadow)"
+  }
+}
+job {
   id: "mac-arm64-rel reclient staging"
   realm: "reclient"
   acl_sets: "reclient"
@@ -7374,6 +7394,7 @@
   triggers: "Mac Builder Next"
   triggers: "Mac deterministic"
   triggers: "Mac deterministic (dbg)"
+  triggers: "Mac deterministic (reclient shadow)"
   triggers: "Network Service Linux"
   triggers: "Site Isolation Android"
   triggers: "TSAN Debug"
@@ -7589,6 +7610,7 @@
   triggers: "mac-arm64-on-arm64-rel"
   triggers: "mac-arm64-on-arm64-rel-reclient"
   triggers: "mac-arm64-rel"
+  triggers: "mac-arm64-rel (reclient shadow)"
   triggers: "mac-code-coverage"
   triggers: "mac-fieldtrial-rel"
   triggers: "mac-hermetic-upgrade-rel"
diff --git a/infra/config/subprojects/chromium/ci/chromium.fyi.star b/infra/config/subprojects/chromium/ci/chromium.fyi.star
index d553acd..5133aac7 100644
--- a/infra/config/subprojects/chromium/ci/chromium.fyi.star
+++ b/infra/config/subprojects/chromium/ci/chromium.fyi.star
@@ -1964,6 +1964,20 @@
 )
 
 fyi_mac_builder(
+    name = "Mac deterministic (reclient shadow)",
+    console_view_entry = consoles.console_view_entry(
+        category = "deterministic|mac",
+        short_name = "rec",
+    ),
+    cores = None,
+    executable = "recipe:swarming/deterministic_build",
+    execution_timeout = 6 * time.hour,
+    goma_backend = None,
+    reclient_instance = rbe_instance.DEFAULT,
+    reclient_jobs = 40,
+)
+
+fyi_mac_builder(
     name = "mac-hermetic-upgrade-rel",
     console_view_entry = consoles.console_view_entry(
         category = "mac",
diff --git a/infra/config/subprojects/chromium/ci/chromium.mac.star b/infra/config/subprojects/chromium/ci/chromium.mac.star
index a36e8491..4943025 100644
--- a/infra/config/subprojects/chromium/ci/chromium.mac.star
+++ b/infra/config/subprojects/chromium/ci/chromium.mac.star
@@ -160,6 +160,22 @@
     os = os.MAC_DEFAULT,
 )
 
+ci.builder(
+    name = "mac-arm64-rel (reclient shadow)",
+    builder_spec = builder_config.copy_from(
+        "ci/mac-arm64-rel",
+    ),
+    console_view_entry = consoles.console_view_entry(
+        category = "release|arm64",
+        short_name = "rec",
+    ),
+    os = os.MAC_DEFAULT,
+    sheriff_rotations = args.ignore_default(None),
+    goma_backend = None,
+    reclient_instance = rbe_instance.DEFAULT,
+    reclient_jobs = 40,
+)
+
 ci.thin_tester(
     name = "mac11-arm64-rel-tests",
     branch_selector = branches.DESKTOP_EXTENDED_STABLE_MILESTONE,
diff --git a/ios/chrome/browser/follow/BUILD.gn b/ios/chrome/browser/follow/BUILD.gn
index 8f7a15b..932eeb3 100644
--- a/ios/chrome/browser/follow/BUILD.gn
+++ b/ios/chrome/browser/follow/BUILD.gn
@@ -2,7 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import("//ios/web/public/js_messaging/optimize_js.gni")
+import("//ios/web/public/js_messaging/optimize_ts.gni")
 
 source_set("follow") {
   sources = [
@@ -70,9 +70,9 @@
   configs += [ "//build/config/compiler:enable_arc" ]
 }
 
-optimize_js("rss_link_js") {
+optimize_ts("rss_link_js") {
   visibility = [ ":follow" ]
 
-  primary_script = "resources/rss_link.js"
-  sources = [ "resources/rss_link.js" ]
+  sources = [ "resources/rss_link.ts" ]
+  deps = [ "//ios/web/public/js_messaging:gcrweb" ]
 }
diff --git a/ios/chrome/browser/follow/resources/rss_link.js b/ios/chrome/browser/follow/resources/rss_link.ts
similarity index 81%
rename from ios/chrome/browser/follow/resources/rss_link.js
rename to ios/chrome/browser/follow/resources/rss_link.ts
index a84ef51..c4b21ea4 100644
--- a/ios/chrome/browser/follow/resources/rss_link.js
+++ b/ios/chrome/browser/follow/resources/rss_link.ts
@@ -2,16 +2,18 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import {gCrWeb} from "//ios/web/public/js_messaging/resources/gcrweb.js";
+
 /**
  * @fileoverview Functions used to parse RSS links from a web page.
  */
 
-__gCrWeb['rssLink'] = {};
 
 /* Gets RSS links. */
-__gCrWeb.rssLink.getRSSLinks = function() {
+gCrWeb.rssLink.getRSSLinks = function(): string[] {
   const linkTags = document.head.getElementsByTagName('link');
-  const rssLinks = [];
+  const rssLinks: string[] = [];
+
   for (const linkTag of linkTags) {
       if (linkTag.rel === 'alternate' ||
           linkTag.rel === 'service.feed') {
diff --git a/ios/chrome/browser/ui/bubble/bubble_presenter.mm b/ios/chrome/browser/ui/bubble/bubble_presenter.mm
index ef4f80f5..1163192f 100644
--- a/ios/chrome/browser/ui/bubble/bubble_presenter.mm
+++ b/ios/chrome/browser/ui/bubble/bubble_presenter.mm
@@ -535,10 +535,12 @@
       };
 
   BubbleViewControllerPresenter* bubbleViewControllerPresenter =
-      [[BubbleViewControllerPresenter alloc] initWithText:text
-                                           arrowDirection:direction
-                                                alignment:alignment
-                                        dismissalCallback:dismissalCallback];
+      [[BubbleViewControllerPresenter alloc]
+          initDefaultBubbleWithText:text
+                     arrowDirection:direction
+                          alignment:alignment
+               isLongDurationBubble:[self isLongDurationBubble:feature]
+                  dismissalCallback:dismissalCallback];
 
   return bubbleViewControllerPresenter;
 }
@@ -552,4 +554,11 @@
       ->DismissedWithSnooze(feature, snoozeAction);
 }
 
+// Returns YES if the bubble for `feature` has a long duration.
+- (BOOL)isLongDurationBubble:(const base::Feature&)feature {
+  // Display follow iph bubble with long duration.
+  return feature.name ==
+         feature_engagement::kIPHFollowWhileBrowsingFeature.name;
+}
+
 @end
diff --git a/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.h b/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.h
index cb4c15e..07ff9a2 100644
--- a/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.h
+++ b/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.h
@@ -65,12 +65,15 @@
 // Initializes the presenter with a Default BubbleViewType. `text` is the text
 // displayed by the bubble. `arrowDirection` is the direction the bubble's arrow
 // is pointing. `alignment` is the position of the arrow on the bubble.
+// `isLongDurationBubble` is YES if the bubble presenting time is longer.
 // `dismissalCallback` is a block invoked when the bubble is dismissed (manual
 // and automatic dismissal). `dismissalCallback` is optional.
-- (instancetype)initWithText:(NSString*)text
-              arrowDirection:(BubbleArrowDirection)arrowDirection
-                   alignment:(BubbleAlignment)alignment
-           dismissalCallback:(ProceduralBlockWithSnoozeAction)dismissalCallback;
+- (instancetype)initDefaultBubbleWithText:(NSString*)text
+                           arrowDirection:(BubbleArrowDirection)arrowDirection
+                                alignment:(BubbleAlignment)alignment
+                     isLongDurationBubble:(BOOL)isLongDurationBubble
+                        dismissalCallback:
+                            (ProceduralBlockWithSnoozeAction)dismissalCallback;
 
 - (instancetype)init NS_UNAVAILABLE;
 
diff --git a/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.mm b/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.mm
index ab46e175..8efe706 100644
--- a/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.mm
+++ b/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.mm
@@ -18,6 +18,9 @@
 
 // How long, in seconds, the bubble is visible on the screen.
 const NSTimeInterval kBubbleVisibilityDuration = 5.0;
+// How long, in seconds, the long duration bubble is visible on the screen. Ex.
+// Follow in-product help(IPH) bubble.
+const NSTimeInterval kBubbleVisibilityLongDuration = 8.0;
 // How long, in seconds, the user should be considered engaged with the bubble
 // after the bubble first becomes visible.
 const NSTimeInterval kBubbleEngagementDuration = 30.0;
@@ -68,6 +71,8 @@
 @property(nonatomic, assign) BubbleAlignment alignment;
 // The type of the bubble view's content.
 @property(nonatomic, assign, readonly) BubbleViewType bubbleType;
+// YES if the bubble should present longer.
+@property(nonatomic, assign) BOOL isLongDurationBubble;
 // Whether the bubble view controller is presented or dismissed.
 @property(nonatomic, assign, getter=isPresenting) BOOL presenting;
 // The block invoked when the bubble is dismissed (both via timer and via tap).
@@ -138,11 +143,13 @@
   return self;
 }
 
-- (instancetype)initWithText:(NSString*)text
-              arrowDirection:(BubbleArrowDirection)arrowDirection
-                   alignment:(BubbleAlignment)alignment
-           dismissalCallback:
-               (ProceduralBlockWithSnoozeAction)dismissalCallback {
+- (instancetype)initDefaultBubbleWithText:(NSString*)text
+                           arrowDirection:(BubbleArrowDirection)arrowDirection
+                                alignment:(BubbleAlignment)alignment
+                     isLongDurationBubble:(BOOL)isLongDurationBubble
+                        dismissalCallback:
+                            (ProceduralBlockWithSnoozeAction)dismissalCallback {
+  self.isLongDurationBubble = isLongDurationBubble;
   return [self initWithText:text
                       title:nil
                       image:nil
@@ -184,7 +191,9 @@
   [parentView addGestureRecognizer:self.swipeRecognizer];
 
   self.bubbleDismissalTimer = [NSTimer
-      scheduledTimerWithTimeInterval:kBubbleVisibilityDuration
+      scheduledTimerWithTimeInterval:self.isLongDurationBubble
+                                         ? kBubbleVisibilityLongDuration
+                                         : kBubbleVisibilityDuration
                               target:self
                             selector:@selector(bubbleDismissalTimerFired:)
                             userInfo:nil
diff --git a/ios/chrome/browser/ui/elements/text_view_selection_disabled.mm b/ios/chrome/browser/ui/elements/text_view_selection_disabled.mm
index 7989489a..624b4ac 100644
--- a/ios/chrome/browser/ui/elements/text_view_selection_disabled.mm
+++ b/ios/chrome/browser/ui/elements/text_view_selection_disabled.mm
@@ -8,16 +8,22 @@
 #error "This file requires ARC support."
 #endif
 
+#if !defined(__IPHONE_16_0) || __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_16_0
+@interface UITextView (TextKit)
+// Forward declare iOS 16 `+textViewUsingTextLayoutManager` on iOS 15 SDK
+// builds.
++ (instancetype)textViewUsingTextLayoutManager:(BOOL)usingTextLayoutManager;
+@end
+#endif
+
 @implementation TextViewSelectionDisabled
 
 + (TextViewSelectionDisabled*)textView {
-// TODO(crbug.com/1335912): On iOS 16, EG is unable to tap links in
-// TextKit2-based UITextViews. Fall back to TextKit1 until this issue
-// is resolved.
-#if defined(__IPHONE_16_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0
+  // TODO(crbug.com/1335912): On iOS 16, EG is unable to tap links in
+  // TextKit2-based UITextViews. Fall back to TextKit1 until this issue
+  // is resolved.
   if (@available(iOS 16, *))
     return [TextViewSelectionDisabled textViewUsingTextLayoutManager:NO];
-#endif
   return [[TextViewSelectionDisabled alloc] init];
 }
 
diff --git a/ios/chrome/browser/ui/omnibox/popup/omnibox_popup_egtest.mm b/ios/chrome/browser/ui/omnibox/popup/omnibox_popup_egtest.mm
index 0d504eb8b..5630ee3 100644
--- a/ios/chrome/browser/ui/omnibox/popup/omnibox_popup_egtest.mm
+++ b/ios/chrome/browser/ui/omnibox/popup/omnibox_popup_egtest.mm
@@ -138,7 +138,8 @@
 
 // Tests that tapping the switch to open tab button, switch to the open tab,
 // doesn't close the tab.
-- (void)testSwitchToOpenTab {
+// TOOD(crbug.com/1346362): Test failing regularly.
+- (void)DISABLED_testSwitchToOpenTab {
 // TODO(crbug.com/1067817): Test won't pass on iPad devices.
 #if !TARGET_IPHONE_SIMULATOR
   if ([ChromeEarlGrey isIPadIdiom]) {
diff --git a/ios/chrome/browser/ui/toolbar/BUILD.gn b/ios/chrome/browser/ui/toolbar/BUILD.gn
index 8fdfbf09..681d7dd 100644
--- a/ios/chrome/browser/ui/toolbar/BUILD.gn
+++ b/ios/chrome/browser/ui/toolbar/BUILD.gn
@@ -53,6 +53,8 @@
     "//ios/chrome/browser/ui/icons:infobar_icons",
     "//ios/chrome/browser/ui/icons:symbols",
     "//ios/chrome/browser/ui/location_bar",
+    "//ios/chrome/browser/ui/main:layout_guide_scene_agent",
+    "//ios/chrome/browser/ui/main:scene_state_header",
     "//ios/chrome/browser/ui/menu",
     "//ios/chrome/browser/ui/ntp",
     "//ios/chrome/browser/ui/ntp:util",
diff --git a/ios/chrome/browser/ui/toolbar/adaptive_toolbar_coordinator.mm b/ios/chrome/browser/ui/toolbar/adaptive_toolbar_coordinator.mm
index 376adf7..4c50a55 100644
--- a/ios/chrome/browser/ui/toolbar/adaptive_toolbar_coordinator.mm
+++ b/ios/chrome/browser/ui/toolbar/adaptive_toolbar_coordinator.mm
@@ -15,6 +15,9 @@
 #import "ios/chrome/browser/ui/commands/find_in_page_commands.h"
 #import "ios/chrome/browser/ui/commands/omnibox_commands.h"
 #import "ios/chrome/browser/ui/commands/popup_menu_commands.h"
+#import "ios/chrome/browser/ui/main/layout_guide_scene_agent.h"
+#import "ios/chrome/browser/ui/main/scene_state.h"
+#import "ios/chrome/browser/ui/main/scene_state_browser_agent.h"
 #import "ios/chrome/browser/ui/menu/browser_action_factory.h"
 #import "ios/chrome/browser/ui/ntp/ntp_util.h"
 #import "ios/chrome/browser/ui/toolbar/adaptive_toolbar_coordinator+subclassing.h"
@@ -41,6 +44,8 @@
 @property(nonatomic, strong) ToolbarMediator* mediator;
 // Actions handler for the toolbar buttons.
 @property(nonatomic, strong) ToolbarButtonActionsHandler* actionHandler;
+// The layout guide center to use to coordinate views.
+@property(nonatomic, readonly) LayoutGuideCenter* layoutGuideCenter;
 
 @end
 
@@ -64,6 +69,7 @@
       self.browser->GetBrowserState()->IsOffTheRecord()
           ? UIUserInterfaceStyleDark
           : UIUserInterfaceStyleUnspecified;
+  self.viewController.layoutGuideCenter = self.layoutGuideCenter;
 
   self.mediator = [[ToolbarMediator alloc] init];
   self.mediator.incognito = self.browser->GetBrowserState()->IsOffTheRecord();
@@ -89,6 +95,18 @@
 
 #pragma mark - Properties
 
+- (LayoutGuideCenter*)layoutGuideCenter {
+  SceneState* sceneState =
+      SceneStateBrowserAgent::FromBrowser(self.browser)->GetSceneState();
+  LayoutGuideSceneAgent* layoutGuideSceneAgent =
+      [LayoutGuideSceneAgent agentFromScene:sceneState];
+  if (self.browser->GetBrowserState()->IsOffTheRecord()) {
+    return layoutGuideSceneAgent.incognitoLayoutGuideCenter;
+  } else {
+    return layoutGuideSceneAgent.layoutGuideCenter;
+  }
+}
+
 - (void)setLongPressDelegate:(id<PopupMenuLongPressDelegate>)longPressDelegate {
   _longPressDelegate = longPressDelegate;
   self.viewController.longPressDelegate = longPressDelegate;
diff --git a/ios/chrome/browser/ui/toolbar/adaptive_toolbar_view_controller.h b/ios/chrome/browser/ui/toolbar/adaptive_toolbar_view_controller.h
index dd56d02..639a4e4 100644
--- a/ios/chrome/browser/ui/toolbar/adaptive_toolbar_view_controller.h
+++ b/ios/chrome/browser/ui/toolbar/adaptive_toolbar_view_controller.h
@@ -15,6 +15,7 @@
 @protocol AdaptiveToolbarMenusProvider;
 @class AdaptiveToolbarViewController;
 @protocol BrowserCommands;
+@class LayoutGuideCenter;
 @protocol OmniboxCommands;
 @protocol PopupMenuCommands;
 @protocol PopupMenuLongPressDelegate;
@@ -33,6 +34,8 @@
 
 // Button factory.
 @property(nonatomic, strong) ToolbarButtonFactory* buttonFactory;
+// Layout Guide Center.
+@property(nonatomic, strong) LayoutGuideCenter* layoutGuideCenter;
 // Omnibox commands handler for the ViewController.
 @property(nonatomic, weak) id<OmniboxCommands> omniboxCommandsHandler;
 // Popup menu commands handler for the ViewController.
diff --git a/ios/chrome/browser/ui/toolbar/adaptive_toolbar_view_controller.mm b/ios/chrome/browser/ui/toolbar/adaptive_toolbar_view_controller.mm
index d87333c..06ed171 100644
--- a/ios/chrome/browser/ui/toolbar/adaptive_toolbar_view_controller.mm
+++ b/ios/chrome/browser/ui/toolbar/adaptive_toolbar_view_controller.mm
@@ -85,13 +85,15 @@
            object:nil];
   [self makeViewAccessibilityTraitsContainer];
 
-  // Adds the layout guide to the buttons.
+  // Add the layout guide names to the buttons.
   self.view.toolsMenuButton.guideName = kToolsMenuGuide;
   self.view.tabGridButton.guideName = kTabSwitcherGuide;
   self.view.openNewTabButton.guideName = kNewTabButtonGuide;
   self.view.forwardButton.guideName = kForwardButtonGuide;
   self.view.backButton.guideName = kBackButtonGuide;
 
+  [self addLayoutGuideCenterToButtons];
+
   // Add navigation popup menu triggers.
   if (UseSymbols()) {
     [self configureMenuProviderForButton:self.view.backButton
@@ -136,6 +138,14 @@
   return self.view.toolsMenuButton;
 }
 
+- (void)setLayoutGuideCenter:(LayoutGuideCenter*)layoutGuideCenter {
+  _layoutGuideCenter = layoutGuideCenter;
+
+  if (self.isViewLoaded) {
+    [self addLayoutGuideCenterToButtons];
+  }
+}
+
 #pragma mark - ToolbarConsumer
 
 - (void)setCanGoForward:(BOOL)canGoForward {
@@ -456,4 +466,12 @@
   [button addAction:action forControlEvents:UIControlEventMenuActionTriggered];
 }
 
+- (void)addLayoutGuideCenterToButtons {
+  self.view.toolsMenuButton.layoutGuideCenter = self.layoutGuideCenter;
+  self.view.tabGridButton.layoutGuideCenter = self.layoutGuideCenter;
+  self.view.openNewTabButton.layoutGuideCenter = self.layoutGuideCenter;
+  self.view.forwardButton.layoutGuideCenter = self.layoutGuideCenter;
+  self.view.backButton.layoutGuideCenter = self.layoutGuideCenter;
+}
+
 @end
diff --git a/ios/chrome/browser/ui/toolbar/buttons/toolbar_button.h b/ios/chrome/browser/ui/toolbar/buttons/toolbar_button.h
index bcb2903..6f8e1272 100644
--- a/ios/chrome/browser/ui/toolbar/buttons/toolbar_button.h
+++ b/ios/chrome/browser/ui/toolbar/buttons/toolbar_button.h
@@ -10,6 +10,7 @@
 #import "ios/chrome/browser/ui/toolbar/buttons/toolbar_component_options.h"
 #import "ios/chrome/browser/ui/util/named_guide.h"
 
+@class LayoutGuideCenter;
 @class ToolbarConfiguration;
 
 // UIButton subclass used as a Toolbar component.
@@ -30,6 +31,8 @@
 // rotations. Any view constrained to them is expected to be dismissed on such
 // events.
 @property(nonatomic, strong) GuideName* guideName;
+// The layout guide center for this button.
+@property(nonatomic, strong) LayoutGuideCenter* layoutGuideCenter;
 // Whether this button is spotlighted, having a light gray background. This
 // state should not be used in the same time as the selected state.
 @property(nonatomic, assign) BOOL spotlighted;
diff --git a/ios/chrome/browser/ui/toolbar/buttons/toolbar_button.mm b/ios/chrome/browser/ui/toolbar/buttons/toolbar_button.mm
index b9cf7045..e8acd16 100644
--- a/ios/chrome/browser/ui/toolbar/buttons/toolbar_button.mm
+++ b/ios/chrome/browser/ui/toolbar/buttons/toolbar_button.mm
@@ -8,6 +8,7 @@
 #import "ios/chrome/browser/ui/toolbar/buttons/toolbar_configuration.h"
 #import "ios/chrome/browser/ui/toolbar/public/toolbar_constants.h"
 #import "ios/chrome/browser/ui/util/uikit_ui_util.h"
+#import "ios/chrome/browser/ui/util/util_swift.h"
 #import "ios/chrome/common/ui/util/constraints_ui_util.h"
 
 #if !defined(__has_feature) || !__has_feature(objc_arc)
@@ -158,6 +159,8 @@
     NamedGuide* guide = [NamedGuide guideWithName:self.guideName view:self];
     if (guide.constrainedView != self)
       guide.constrainedView = self;
+
+    [self.layoutGuideCenter referenceView:self underName:self.guideName];
   }
 }
 
diff --git a/ios/chrome/browser/ui/util/layout_guide_center.swift b/ios/chrome/browser/ui/util/layout_guide_center.swift
index e31f87f7..f1d4e8c 100644
--- a/ios/chrome/browser/ui/util/layout_guide_center.swift
+++ b/ios/chrome/browser/ui/util/layout_guide_center.swift
@@ -22,9 +22,12 @@
   /// References a view under a specific `name`.
   @objc(referenceView:underName:)
   func reference(view referenceView: UIView?, under name: String) {
-    if let oldReferenceView = referenceViews.object(forKey: name as NSString) {
-      oldReferenceView.cr_onWindowCoordinatesChanged = nil
+    let oldReferenceView = referenceViews.object(forKey: name as NSString)
+    // Early return if `referenceView` is already set.
+    if referenceView == oldReferenceView {
+      return
     }
+    oldReferenceView?.cr_onWindowCoordinatesChanged = nil
     referenceViews.setObject(referenceView, forKey: name as NSString)
     updateGuides(named: name)
     // Schedule updates to the matching layout guides when the reference view
diff --git a/ios/chrome/browser/ui/util/layout_guide_center_unittest.mm b/ios/chrome/browser/ui/util/layout_guide_center_unittest.mm
index ee75055..729d766d 100644
--- a/ios/chrome/browser/ui/util/layout_guide_center_unittest.mm
+++ b/ios/chrome/browser/ui/util/layout_guide_center_unittest.mm
@@ -153,3 +153,29 @@
   // the elements are released.
   EXPECT_EQ(weak_layout_guide, nil);
 }
+
+// Checks that if `referenceView:underName:` is called twice with the same
+// arguments, there are no changes
+TEST_F(LayoutGuideCenterTest, TestReferenceViewNoChangesIfSameView) {
+  CGRect rect = CGRectMake(10, 20, 30, 40);
+  UIView* reference_view = [[UIView alloc] initWithFrame:rect];
+  [center_ referenceView:reference_view underName:@"view"];
+
+  // Override reference_view's cr_onWindowCoordinatesChanged to later verify
+  // that it hasn't changed.
+  __block BOOL windowCoordinatesChangedCalled = NO;
+  reference_view.cr_onWindowCoordinatesChanged = ^(UIView* view) {
+    windowCoordinatesChangedCalled = YES;
+  };
+
+  UIView* view = [[UIView alloc] init];
+  reference_view.cr_onWindowCoordinatesChanged(view);
+  EXPECT_TRUE(windowCoordinatesChangedCalled);
+  windowCoordinatesChangedCalled = NO;
+
+  // Re-reference reference_view. This should not change the view's
+  // cr_onWindowCoordinatesChanged block.
+  [center_ referenceView:reference_view underName:@"view"];
+  reference_view.cr_onWindowCoordinatesChanged(view);
+  EXPECT_TRUE(windowCoordinatesChangedCalled);
+}
diff --git a/ios/web/js_features/window_error/BUILD.gn b/ios/web/js_features/window_error/BUILD.gn
index be6f29b..53dc308 100644
--- a/ios/web/js_features/window_error/BUILD.gn
+++ b/ios/web/js_features/window_error/BUILD.gn
@@ -3,7 +3,7 @@
 # found in the LICENSE file.
 
 import("//ios/build/config.gni")
-import("//ios/web/public/js_messaging/optimize_js.gni")
+import("//ios/web/public/js_messaging/optimize_ts.gni")
 
 source_set("window_error") {
   configs += [ "//build/config/compiler:enable_arc" ]
@@ -21,11 +21,11 @@
   ]
 }
 
-optimize_js("error_js") {
+optimize_ts("error_js") {
   visibility = [ ":window_error" ]
 
-  primary_script = "resources/error.js"
-  sources = [ "resources/error.js" ]
+  sources = [ "resources/error.ts" ]
+  deps = [ "//ios/web/public/js_messaging:util_scripts" ]
 }
 
 source_set("unittests") {
diff --git a/ios/web/js_features/window_error/resources/error.js b/ios/web/js_features/window_error/resources/error.ts
similarity index 67%
rename from ios/web/js_features/window_error/resources/error.js
rename to ios/web/js_features/window_error/resources/error.ts
index 83c50cb..85a122ea 100644
--- a/ios/web/js_features/window_error/resources/error.js
+++ b/ios/web/js_features/window_error/resources/error.ts
@@ -6,16 +6,19 @@
  * @fileoverview Error listener to report error details to the native app.
  */
 
-// Requires functions from common.js
+import {sendWebKitMessage} from '//ios/web/public/js_messaging/resources/utils.js'
 
 /**
  * JavaScript errors are logged on the main application side. The handler is
  * added ASAP to catch any errors in startup.
  */
-window.addEventListener('error', function(event) {
-  __gCrWeb.common.sendWebKitMessage('WindowErrorResultHandler',
+function errorEventHandler(event: ErrorEvent): void {
+  sendWebKitMessage('WindowErrorResultHandler',
       {'filename' : event.filename,
        'line_number' : event.lineno,
        'message': event.message.toString()
       });
-});
+}
+
+
+window.addEventListener('error', errorEventHandler);
diff --git a/media/gpu/v4l2/test/av1_decoder.cc b/media/gpu/v4l2/test/av1_decoder.cc
index b0e0710..036a1b6 100644
--- a/media/gpu/v4l2/test/av1_decoder.cc
+++ b/media/gpu/v4l2/test/av1_decoder.cc
@@ -617,8 +617,8 @@
     ANALYZER_ALLOW_UNUSED(reference_id);
   }
 
-  // TODO(b/228534730): add changes to prepare parameters for V4L2 AV1 stateless
-  // decoding and make VIDIOC_S_EXT_CTRLS v4l2 ioctl call
+  // TODO(b/239618516): add ext_ctrl for V4L2_CID_STATELESS_AV1_SEQUENCE
+
   struct v4l2_ctrl_av1_frame_header v4l2_frame_params = {};
 
   FillLoopFilterParams(&v4l2_frame_params.loop_filter,
@@ -648,11 +648,15 @@
   FillGlobalMotionParams(&v4l2_frame_params.global_motion,
                          current_frame_header.global_motion);
 
+  // TODO(stevecho): V4L2_CID_STATELESS_AV1_FRAME_HEADER is trending to be
+  // changed to V4L2_CID_STATELESS_AV1_FRAME
   struct v4l2_ext_control ext_ctrl = {.id = V4L2_CID_STATELESS_AV1_FRAME_HEADER,
                                       .size = sizeof(v4l2_frame_params),
                                       .ptr = &v4l2_frame_params};
 
-  if (!v4l2_ioctl_->SetExtCtrls(OUTPUT_queue_, ext_ctrl))
+  struct v4l2_ext_controls ext_ctrls = {.count = 1, .controls = &ext_ctrl};
+
+  if (!v4l2_ioctl_->SetExtCtrls(OUTPUT_queue_, &ext_ctrls))
     LOG(FATAL) << "VIDIOC_S_EXT_CTRLS failed.";
 
   if (!v4l2_ioctl_->MediaRequestIocQueue(OUTPUT_queue_))
diff --git a/media/gpu/v4l2/test/v4l2_ioctl_shim.cc b/media/gpu/v4l2/test/v4l2_ioctl_shim.cc
index 555dbe6..b96167d0 100644
--- a/media/gpu/v4l2/test/v4l2_ioctl_shim.cc
+++ b/media/gpu/v4l2/test/v4l2_ioctl_shim.cc
@@ -482,7 +482,7 @@
 }
 
 bool V4L2IoctlShim::SetExtCtrls(const std::unique_ptr<V4L2Queue>& queue,
-                                v4l2_ext_control& ext_ctrl) const {
+                                v4l2_ext_controls* ext_ctrls) const {
   // TODO(b/230021497): add compressed header probability related change
   // when V4L2_CID_STATELESS_VP9_COMPRESSED_HDR is supported
 
@@ -492,12 +492,10 @@
   // instead are applied by the driver for the buffer associated with
   // the same request.", see:
   // https://www.kernel.org/doc/html/v5.10/userspace-api/media/v4l/vidioc-g-ext-ctrls.html#description
-  struct v4l2_ext_controls ctrls = {.which = V4L2_CTRL_WHICH_REQUEST_VAL,
-                                    .count = 1,
-                                    .request_fd = queue->media_request_fd(),
-                                    .controls = &ext_ctrl};
+  ext_ctrls->which = V4L2_CTRL_WHICH_REQUEST_VAL;
+  ext_ctrls->request_fd = queue->media_request_fd();
 
-  const bool ret = Ioctl(VIDIOC_S_EXT_CTRLS, &ctrls);
+  const bool ret = Ioctl(VIDIOC_S_EXT_CTRLS, ext_ctrls);
 
   return ret;
 }
diff --git a/media/gpu/v4l2/test/v4l2_ioctl_shim.h b/media/gpu/v4l2/test/v4l2_ioctl_shim.h
index d9b83cb..0f0de65 100644
--- a/media/gpu/v4l2/test/v4l2_ioctl_shim.h
+++ b/media/gpu/v4l2/test/v4l2_ioctl_shim.h
@@ -186,10 +186,10 @@
   // Starts streaming |queue| (via VIDIOC_STREAMON).
   [[nodiscard]] bool StreamOn(const enum v4l2_buf_type type) const;
 
-  // Sets the value of a control which specifies decoding parameters
+  // Sets the value of controls which specify decoding parameters
   // for each frame.
   [[nodiscard]] bool SetExtCtrls(const std::unique_ptr<V4L2Queue>& queue,
-                                 v4l2_ext_control& ext_ctrl) const;
+                                 v4l2_ext_controls* ext_ctrls) const;
 
   // Allocates requests (likely one per OUTPUT buffer) via
   // MEDIA_IOC_REQUEST_ALLOC on the media device.
diff --git a/media/gpu/v4l2/test/vp9_decoder.cc b/media/gpu/v4l2/test/vp9_decoder.cc
index 1841104..bd2f35f0 100644
--- a/media/gpu/v4l2/test/vp9_decoder.cc
+++ b/media/gpu/v4l2/test/vp9_decoder.cc
@@ -463,7 +463,9 @@
                                       .size = sizeof(v4l2_frame_params),
                                       .ptr = &v4l2_frame_params};
 
-  if (!v4l2_ioctl_->SetExtCtrls(OUTPUT_queue_, ext_ctrl))
+  struct v4l2_ext_controls ext_ctrls = {.count = 1, .controls = &ext_ctrl};
+
+  if (!v4l2_ioctl_->SetExtCtrls(OUTPUT_queue_, &ext_ctrls))
     LOG(FATAL) << "VIDIOC_S_EXT_CTRLS failed.";
 
   if (!v4l2_ioctl_->MediaRequestIocQueue(OUTPUT_queue_))
diff --git a/services/network/network_context.cc b/services/network/network_context.cc
index 5fcfc9df..cb42918 100644
--- a/services/network/network_context.cc
+++ b/services/network/network_context.cc
@@ -746,13 +746,15 @@
 void NetworkContext::CreateURLLoaderFactory(
     mojo::PendingReceiver<mojom::URLLoaderFactory> receiver,
     mojom::URLLoaderFactoryParamsPtr params) {
-  scoped_refptr<ResourceSchedulerClient> resource_scheduler_client =
-      base::MakeRefCounted<ResourceSchedulerClient>(
-          current_resource_scheduler_client_id_,
-          IsBrowserInitiated(params->process_id == mojom::kBrowserProcessId),
-          resource_scheduler_.get(),
-          url_request_context_->network_quality_estimator());
-  current_resource_scheduler_client_id_.Increment();
+  scoped_refptr<ResourceSchedulerClient> resource_scheduler_client;
+  if (!base::FeatureList::IsEnabled(features::kDisableResourceScheduler)) {
+    resource_scheduler_client = base::MakeRefCounted<ResourceSchedulerClient>(
+        current_resource_scheduler_client_id_,
+        IsBrowserInitiated(params->process_id == mojom::kBrowserProcessId),
+        resource_scheduler_.get(),
+        url_request_context_->network_quality_estimator());
+    current_resource_scheduler_client_id_.Increment();
+  }
   CreateURLLoaderFactory(std::move(receiver), std::move(params),
                          std::move(resource_scheduler_client));
 }
diff --git a/services/network/public/cpp/features.cc b/services/network/public/cpp/features.cc
index 8912c7d6..b0d56aa 100644
--- a/services/network/public/cpp/features.cc
+++ b/services/network/public/cpp/features.cc
@@ -295,5 +295,9 @@
 const base::Feature kReduceAcceptLanguage{"ReduceAcceptLanguage",
                                           base::FEATURE_DISABLED_BY_DEFAULT};
 
+// Disable ResourceScheduler.
+const base::Feature kDisableResourceScheduler{
+    "DisableResourceScheduler", base::FEATURE_DISABLED_BY_DEFAULT};
+
 }  // namespace features
 }  // namespace network
diff --git a/services/network/public/cpp/features.h b/services/network/public/cpp/features.h
index 0bded2a..6f89731c 100644
--- a/services/network/public/cpp/features.h
+++ b/services/network/public/cpp/features.h
@@ -125,6 +125,9 @@
 COMPONENT_EXPORT(NETWORK_CPP)
 extern const base::Feature kReduceAcceptLanguage;
 
+COMPONENT_EXPORT(NETWORK_CPP)
+extern const base::Feature kDisableResourceScheduler;
+
 }  // namespace features
 }  // namespace network
 
diff --git a/testing/buildbot/chromium.mac.json b/testing/buildbot/chromium.mac.json
index c9008f8..2269a72 100644
--- a/testing/buildbot/chromium.mac.json
+++ b/testing/buildbot/chromium.mac.json
@@ -20278,6 +20278,11 @@
       "all"
     ]
   },
+  "mac-arm64-rel (reclient shadow)": {
+    "additional_compile_targets": [
+      "all"
+    ]
+  },
   "mac11-arm64-rel-tests": {
     "gtest_tests": [
       {
diff --git a/testing/buildbot/waterfalls.pyl b/testing/buildbot/waterfalls.pyl
index e7d6773b..9ccab1fb 100644
--- a/testing/buildbot/waterfalls.pyl
+++ b/testing/buildbot/waterfalls.pyl
@@ -5211,6 +5211,11 @@
           'all',
         ],
       },
+      'mac-arm64-rel (reclient shadow)': {
+        'additional_compile_targets': [
+          'all',
+        ],
+      },
       'mac11-arm64-rel-tests': {
         'mixins': [
             # Only run selected test suites on CQ. https://crbug.com/1234525.
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 5a0ab1d..4f89b44 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -2903,6 +2903,21 @@
             ]
         }
     ],
+    "CrOSLateBootMediaDynamicCgroup": [
+        {
+            "platforms": [
+                "chromeos"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "CrOSLateBootMediaDynamicCgroup"
+                    ]
+                }
+            ]
+        }
+    ],
     "CrOSLateBootSchedTrace": [
         {
             "platforms": [
@@ -3820,6 +3835,26 @@
             ]
         }
     ],
+    "EnablePDPMetricsUSDesktopIOS": [
+        {
+            "platforms": [
+                "chromeos",
+                "chromeos_lacros",
+                "ios",
+                "linux",
+                "mac",
+                "windows"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "ShoppingPDPMetrics"
+                    ]
+                }
+            ]
+        }
+    ],
     "EnableSsePathForCopyLCharsX86": [
         {
             "platforms": [
@@ -5432,25 +5467,6 @@
             ]
         }
     ],
-    "KeyPinningComponentUpdater": [
-        {
-            "platforms": [
-                "chromeos",
-                "chromeos_lacros",
-                "linux",
-                "mac",
-                "windows"
-            ],
-            "experiments": [
-                {
-                    "name": "Enabled",
-                    "enable_features": [
-                        "KeyPinningComponentUpdater"
-                    ]
-                }
-            ]
-        }
-    ],
     "KeyboardAccessoryAddressIPH": [
         {
             "platforms": [
diff --git a/third_party/blink/public/devtools_protocol/browser_protocol.pdl b/third_party/blink/public/devtools_protocol/browser_protocol.pdl
index 0d2cacc..73feffd 100644
--- a/third_party/blink/public/devtools_protocol/browser_protocol.pdl
+++ b/third_party/blink/public/devtools_protocol/browser_protocol.pdl
@@ -774,6 +774,7 @@
       NotificationPermissionRequestedIframe
       ObsoleteWebRtcCipherSuite
       OpenWebDatabaseInsecureContext
+      OverflowVisibleOnReplacedElement
       PictureSourceSrc
       PrefixedCancelAnimationFrame
       PrefixedRequestAnimationFrame
@@ -2684,7 +2685,7 @@
       array of NodeId nodeIds
 
   # Returns NodeIds of current top layer elements.
-  # Top layer is rendered closest to the user within a viewport, therefore its elements always 
+  # Top layer is rendered closest to the user within a viewport, therefore its elements always
   # appear on top of all other content.
   experimental command getTopLayerElements
     returns
@@ -8992,6 +8993,14 @@
       # Comma separated list of StorageType to clear.
       string storageTypes
 
+  # Clears storage for storage key.
+  command clearDataForStorageKey
+    parameters
+      # Storage key.
+      string storageKey
+      # Comma separated list of StorageType to clear.
+      string storageTypes
+
   # Returns all browser cookies.
   command getCookies
     parameters
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 8cf860b..b0cc28d 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
@@ -3629,6 +3629,7 @@
   kSendBeaconWithFormData = 4308,
   kSendBeaconWithURLSearchParams = 4309,
   kSendBeaconWithUSVString = 4310,
+  kReplacedElementPaintedWithOverflow = 4311,
 
   // 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/core/frame/deprecation/deprecation.cc b/third_party/blink/renderer/core/frame/deprecation/deprecation.cc
index 9904e81..24a89961 100644
--- a/third_party/blink/renderer/core/frame/deprecation/deprecation.cc
+++ b/third_party/blink/renderer/core/frame/deprecation/deprecation.cc
@@ -247,6 +247,9 @@
     case WebFeature::kNavigateEventRestoreScroll:
       return DeprecationInfo::WithTranslation(
           feature, DeprecationIssueType::kNavigateEventRestoreScroll);
+    case WebFeature::kExplicitOverflowVisibleOnReplacedElement:
+      return DeprecationInfo::WithTranslation(
+          feature, DeprecationIssueType::kOverflowVisibleOnReplacedElement);
     default:
       return DeprecationInfo::NotDeprecated(feature);
   }
diff --git a/third_party/blink/renderer/core/inspector/inspector_audits_issue.cc b/third_party/blink/renderer/core/inspector/inspector_audits_issue.cc
index 2fc40fd..6a339d6 100644
--- a/third_party/blink/renderer/core/inspector/inspector_audits_issue.cc
+++ b/third_party/blink/renderer/core/inspector/inspector_audits_issue.cc
@@ -536,6 +536,10 @@
       type = protocol::Audits::DeprecationIssueTypeEnum::
           OpenWebDatabaseInsecureContext;
       break;
+    case DeprecationIssueType::kOverflowVisibleOnReplacedElement:
+      type = protocol::Audits::DeprecationIssueTypeEnum::
+          OverflowVisibleOnReplacedElement;
+      break;
     case DeprecationIssueType::kPictureSourceSrc:
       type = protocol::Audits::DeprecationIssueTypeEnum::PictureSourceSrc;
       break;
diff --git a/third_party/blink/renderer/core/inspector/inspector_audits_issue.h b/third_party/blink/renderer/core/inspector/inspector_audits_issue.h
index 6ac5eec..65f11a48 100644
--- a/third_party/blink/renderer/core/inspector/inspector_audits_issue.h
+++ b/third_party/blink/renderer/core/inspector/inspector_audits_issue.h
@@ -70,6 +70,7 @@
   kNotificationPermissionRequestedIframe,
   kObsoleteWebRtcCipherSuite,
   kOpenWebDatabaseInsecureContext,
+  kOverflowVisibleOnReplacedElement,
   kPictureSourceSrc,
   kPrefixedCancelAnimationFrame,
   kPrefixedRequestAnimationFrame,
diff --git a/third_party/blink/renderer/core/layout/layout_object.cc b/third_party/blink/renderer/core/layout/layout_object.cc
index d8cca14..c2fdf0b 100644
--- a/third_party/blink/renderer/core/layout/layout_object.cc
+++ b/third_party/blink/renderer/core/layout/layout_object.cc
@@ -2804,6 +2804,15 @@
   }
 }
 
+bool LayoutObject::BelongsToElementChangingOverflowBehaviour() const {
+  auto* element = DynamicTo<Element>(GetNode());
+  if (!element)
+    return false;
+
+  return IsA<HTMLVideoElement>(element) || IsA<HTMLCanvasElement>(element) ||
+         IsA<HTMLImageElement>(element);
+}
+
 void LayoutObject::StyleDidChange(StyleDifference diff,
                                   const ComputedStyle* old_style) {
   NOT_DESTROYED();
@@ -2855,13 +2864,17 @@
   // changing the behavior regardless of the counts. Likewise, embedded content
   // will remain clipped regardless of the overflow: visible behvaior change.
   // Note for this reason we exclude SVG and embedded content from the counts.
-  if (IsLayoutReplaced() && !IsSVG() && !IsLayoutEmbeddedContent()) {
+  if (BelongsToElementChangingOverflowBehaviour()) {
     if ((StyleRef().HasExplicitOverflowXVisible() &&
          StyleRef().OverflowX() == EOverflow::kVisible) ||
         (StyleRef().HasExplicitOverflowYVisible() &&
          StyleRef().OverflowY() == EOverflow::kVisible)) {
       UseCounter::Count(GetDocument(),
                         WebFeature::kExplicitOverflowVisibleOnReplacedElement);
+
+      Deprecation::CountDeprecation(
+          GetDocument().GetExecutionContext(),
+          WebFeature::kExplicitOverflowVisibleOnReplacedElement);
       if (!StyleRef().ObjectPropertiesPreventReplacedOverflow()) {
         UseCounter::Count(
             GetDocument(),
diff --git a/third_party/blink/renderer/core/layout/layout_object.h b/third_party/blink/renderer/core/layout/layout_object.h
index 7fac6dd..97cf44e1 100644
--- a/third_party/blink/renderer/core/layout/layout_object.h
+++ b/third_party/blink/renderer/core/layout/layout_object.h
@@ -3569,6 +3569,13 @@
     bitfields_.SetShouldAssumePaintOffsetTranslationForLayoutShiftTracking(b);
   }
 
+  // Returns true if this layout object is created for an element which will be
+  // changing behaviour for overflow: visible.
+  // See
+  // https://groups.google.com/a/chromium.org/g/blink-dev/c/MuTeW_AFgxA/m/IlT4QVEfAgAJ
+  // for details.
+  bool BelongsToElementChangingOverflowBehaviour() const;
+
  protected:
   // Identifiers for each of LayoutObject subclasses.
   // The identifier name for blink::LayoutFoo should be kLayoutObjectFoo.
diff --git a/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc b/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
index 8f6e4aa..b4d0269 100644
--- a/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
+++ b/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
@@ -679,6 +679,9 @@
       const auto& box_model = To<LayoutBoxModelObject>(object_);
       TransformPaintPropertyNode::State state{
           gfx::Vector2dF(box_model.StickyPositionOffset())};
+      state.direct_compositing_reasons =
+          full_context_.direct_compositing_reasons &
+          CompositingReason::kStickyPosition;
       // TODO(wangxianzhu): Not using GetCompositorElementId() here because
       // sticky elements don't work properly under multicol for now, to keep
       // consistency with CompositorElementIdFromUniqueObjectId() below.
@@ -690,55 +693,59 @@
       state.flags.flattens_inherited_transform =
           context_.should_flatten_inherited_transform;
 
-      const auto* layout_constraint = box_model.StickyConstraints();
-      DCHECK(layout_constraint);
-      const auto* scroll_container_properties =
-          layout_constraint->containing_scroll_container_layer
-              ->GetLayoutObject()
-              .FirstFragment()
-              .PaintProperties();
-      // A scroll node is only created if an object can be scrolled manually,
-      // while sticky position attaches to anything that clips overflow.
-      // No need to (actually can't) setup composited sticky constraint if
-      // the clipping ancestor we attach to doesn't have a scroll node.
-      bool scroll_container_scrolls =
-          scroll_container_properties &&
-          scroll_container_properties->Scroll() == context_.current.scroll;
-      if (scroll_container_scrolls) {
-        auto constraint = std::make_unique<CompositorStickyConstraint>();
-        constraint->is_anchored_left = layout_constraint->is_anchored_left;
-        constraint->is_anchored_right = layout_constraint->is_anchored_right;
-        constraint->is_anchored_top = layout_constraint->is_anchored_top;
-        constraint->is_anchored_bottom = layout_constraint->is_anchored_bottom;
+      if (state.direct_compositing_reasons) {
+        const auto* layout_constraint = box_model.StickyConstraints();
+        DCHECK(layout_constraint);
+        const auto* scroll_container_properties =
+            layout_constraint->containing_scroll_container_layer
+                ->GetLayoutObject()
+                .FirstFragment()
+                .PaintProperties();
+        // A scroll node is only created if an object can be scrolled manually,
+        // while sticky position attaches to anything that clips overflow.
+        // No need to (actually can't) setup composited sticky constraint if
+        // the clipping ancestor we attach to doesn't have a scroll node.
+        bool scroll_container_scrolls =
+            scroll_container_properties &&
+            scroll_container_properties->Scroll() == context_.current.scroll;
+        if (scroll_container_scrolls) {
+          auto constraint = std::make_unique<CompositorStickyConstraint>();
+          constraint->is_anchored_left = layout_constraint->is_anchored_left;
+          constraint->is_anchored_right = layout_constraint->is_anchored_right;
+          constraint->is_anchored_top = layout_constraint->is_anchored_top;
+          constraint->is_anchored_bottom =
+              layout_constraint->is_anchored_bottom;
 
-        constraint->left_offset = layout_constraint->left_offset.ToFloat();
-        constraint->right_offset = layout_constraint->right_offset.ToFloat();
-        constraint->top_offset = layout_constraint->top_offset.ToFloat();
-        constraint->bottom_offset = layout_constraint->bottom_offset.ToFloat();
-        constraint->constraint_box_rect =
-            gfx::RectF(layout_constraint->constraining_rect);
-        constraint->scroll_container_relative_sticky_box_rect = gfx::RectF(
-            layout_constraint->scroll_container_relative_sticky_box_rect);
-        constraint->scroll_container_relative_containing_block_rect =
-            gfx::RectF(layout_constraint
-                           ->scroll_container_relative_containing_block_rect);
-        if (const PaintLayer* sticky_box_shifting_ancestor =
-                layout_constraint->nearest_sticky_layer_shifting_sticky_box) {
-          constraint->nearest_element_shifting_sticky_box =
-              CompositorElementIdFromUniqueObjectId(
-                  sticky_box_shifting_ancestor->GetLayoutObject().UniqueId(),
-                  CompositorElementIdNamespace::kStickyTranslation);
+          constraint->left_offset = layout_constraint->left_offset.ToFloat();
+          constraint->right_offset = layout_constraint->right_offset.ToFloat();
+          constraint->top_offset = layout_constraint->top_offset.ToFloat();
+          constraint->bottom_offset =
+              layout_constraint->bottom_offset.ToFloat();
+          constraint->constraint_box_rect =
+              gfx::RectF(layout_constraint->constraining_rect);
+          constraint->scroll_container_relative_sticky_box_rect = gfx::RectF(
+              layout_constraint->scroll_container_relative_sticky_box_rect);
+          constraint->scroll_container_relative_containing_block_rect =
+              gfx::RectF(layout_constraint
+                             ->scroll_container_relative_containing_block_rect);
+          if (const PaintLayer* sticky_box_shifting_ancestor =
+                  layout_constraint->nearest_sticky_layer_shifting_sticky_box) {
+            constraint->nearest_element_shifting_sticky_box =
+                CompositorElementIdFromUniqueObjectId(
+                    sticky_box_shifting_ancestor->GetLayoutObject().UniqueId(),
+                    CompositorElementIdNamespace::kStickyTranslation);
+          }
+          if (const PaintLayer* containing_block_shifting_ancestor =
+                  layout_constraint
+                      ->nearest_sticky_layer_shifting_containing_block) {
+            constraint->nearest_element_shifting_containing_block =
+                CompositorElementIdFromUniqueObjectId(
+                    containing_block_shifting_ancestor->GetLayoutObject()
+                        .UniqueId(),
+                    CompositorElementIdNamespace::kStickyTranslation);
+          }
+          state.sticky_constraint = std::move(constraint);
         }
-        if (const PaintLayer* containing_block_shifting_ancestor =
-                layout_constraint
-                    ->nearest_sticky_layer_shifting_containing_block) {
-          constraint->nearest_element_shifting_containing_block =
-              CompositorElementIdFromUniqueObjectId(
-                  containing_block_shifting_ancestor->GetLayoutObject()
-                      .UniqueId(),
-                  CompositorElementIdNamespace::kStickyTranslation);
-        }
-        state.sticky_constraint = std::move(constraint);
       }
 
       // TODO(crbug.com/1117658): Implement direct update of StickyTranslation
@@ -2976,9 +2983,11 @@
   if (properties->TransformIsolationNode())
     return true;
   if (auto* offset_translation = properties->PaintOffsetTranslation()) {
-    if (offset_translation->RequiresCompositingForScrollDependentPosition())
+    if (offset_translation->RequiresCompositingForFixedPosition())
       return true;
   }
+  if (auto* sticky_translation = properties->StickyTranslation())
+    return true;
   if (properties->OverflowClip())
     return true;
   return false;
diff --git a/third_party/blink/renderer/core/paint/paint_property_tree_builder_test.cc b/third_party/blink/renderer/core/paint/paint_property_tree_builder_test.cc
index 3634243..5840de6 100644
--- a/third_party/blink/renderer/core/paint/paint_property_tree_builder_test.cc
+++ b/third_party/blink/renderer/core/paint/paint_property_tree_builder_test.cc
@@ -6415,6 +6415,8 @@
 
   const auto* outer_properties = PaintPropertiesForElement("outer");
   ASSERT_TRUE(outer_properties && outer_properties->StickyTranslation());
+  EXPECT_TRUE(outer_properties->StickyTranslation()
+                  ->RequiresCompositingForStickyPosition());
   EXPECT_EQ(gfx::Vector2dF(0, 60),
             outer_properties->StickyTranslation()->Translation2D());
   ASSERT_NE(nullptr,
@@ -6429,6 +6431,8 @@
 
   const auto* middle_properties = PaintPropertiesForElement("middle");
   ASSERT_TRUE(middle_properties && middle_properties->StickyTranslation());
+  EXPECT_TRUE(middle_properties->StickyTranslation()
+                  ->RequiresCompositingForStickyPosition());
   EXPECT_EQ(gfx::Vector2dF(0, 15),
             middle_properties->StickyTranslation()->Translation2D());
   ASSERT_NE(nullptr,
@@ -6443,6 +6447,8 @@
 
   const auto* inner_properties = PaintPropertiesForElement("inner");
   ASSERT_TRUE(inner_properties && inner_properties->StickyTranslation());
+  EXPECT_TRUE(inner_properties->StickyTranslation()
+                  ->RequiresCompositingForStickyPosition());
   EXPECT_EQ(gfx::Vector2dF(0, 20),
             inner_properties->StickyTranslation()->Translation2D());
   ASSERT_NE(nullptr,
@@ -6457,9 +6463,9 @@
                 ->nearest_element_shifting_containing_block);
 }
 
-TEST_P(PaintPropertyTreeBuilderTest, NonScrollableSticky) {
+TEST_P(PaintPropertyTreeBuilderTest, StickyUnderOverflowHidden) {
   // This test verifies the property tree builder applies sticky offset
-  // correctly when the clipping container cannot be scrolled, and
+  // correctly when the scroll container cannot be manually scrolled, and
   // does not emit sticky constraints.
   SetBodyInnerHTML(R"HTML(
     <div id="scroller" style="overflow:hidden; width:300px; height:200px;">
@@ -6478,6 +6484,10 @@
 
   const auto* outer_properties = PaintPropertiesForElement("outer");
   ASSERT_TRUE(outer_properties && outer_properties->StickyTranslation());
+  // We still composite the element for better performance programmatic scroll
+  // offset animation.
+  EXPECT_TRUE(outer_properties->StickyTranslation()
+                  ->RequiresCompositingForStickyPosition());
   EXPECT_EQ(gfx::Vector2dF(0, 60),
             outer_properties->StickyTranslation()->Translation2D());
   EXPECT_EQ(nullptr,
@@ -6485,6 +6495,8 @@
 
   const auto* middle_properties = PaintPropertiesForElement("middle");
   ASSERT_TRUE(middle_properties && middle_properties->StickyTranslation());
+  EXPECT_TRUE(middle_properties->StickyTranslation()
+                  ->RequiresCompositingForStickyPosition());
   EXPECT_EQ(gfx::Vector2dF(0, 15),
             middle_properties->StickyTranslation()->Translation2D());
   EXPECT_EQ(nullptr,
@@ -6492,6 +6504,52 @@
 
   const auto* inner_properties = PaintPropertiesForElement("inner");
   ASSERT_TRUE(inner_properties && inner_properties->StickyTranslation());
+  EXPECT_TRUE(inner_properties->StickyTranslation()
+                  ->RequiresCompositingForStickyPosition());
+  EXPECT_EQ(gfx::Vector2dF(0, 20),
+            inner_properties->StickyTranslation()->Translation2D());
+  EXPECT_EQ(nullptr,
+            inner_properties->StickyTranslation()->GetStickyConstraint());
+}
+
+TEST_P(PaintPropertyTreeBuilderTest, StickyUnderScrollerWithoutOverflow) {
+  // This test verifies the property tree builder applies sticky offset
+  // correctly when the scroll container doesn't have overflow, and does not
+  // emit compositing reasons or sticky constraints.
+  SetBodyInnerHTML(R"HTML(
+    <div id="scroller" style="overflow:scroll; width:300px; height:400px;">
+      <div id="outer" style="position:sticky; top:10px;">
+        <div style="height:300px;">
+          <span id="middle" style="position:sticky; top:25px;">
+            <span id="inner" style="position:sticky; top:45px;"></span>
+          </span>
+        </div>
+      </div>
+    </div>
+  )HTML");
+
+  const auto* outer_properties = PaintPropertiesForElement("outer");
+  ASSERT_TRUE(outer_properties && outer_properties->StickyTranslation());
+  EXPECT_FALSE(outer_properties->StickyTranslation()
+                   ->RequiresCompositingForStickyPosition());
+  EXPECT_EQ(gfx::Vector2dF(0, 10),
+            outer_properties->StickyTranslation()->Translation2D());
+  EXPECT_EQ(nullptr,
+            outer_properties->StickyTranslation()->GetStickyConstraint());
+
+  const auto* middle_properties = PaintPropertiesForElement("middle");
+  ASSERT_TRUE(middle_properties && middle_properties->StickyTranslation());
+  EXPECT_FALSE(middle_properties->StickyTranslation()
+                   ->RequiresCompositingForStickyPosition());
+  EXPECT_EQ(gfx::Vector2dF(0, 15),
+            middle_properties->StickyTranslation()->Translation2D());
+  EXPECT_EQ(nullptr,
+            middle_properties->StickyTranslation()->GetStickyConstraint());
+
+  const auto* inner_properties = PaintPropertiesForElement("inner");
+  ASSERT_TRUE(inner_properties && inner_properties->StickyTranslation());
+  EXPECT_FALSE(inner_properties->StickyTranslation()
+                   ->RequiresCompositingForStickyPosition());
   EXPECT_EQ(gfx::Vector2dF(0, 20),
             inner_properties->StickyTranslation()->Translation2D());
   EXPECT_EQ(nullptr,
diff --git a/third_party/blink/renderer/core/paint/replaced_painter.cc b/third_party/blink/renderer/core/paint/replaced_painter.cc
index 7a23c537..0f1cc456 100644
--- a/third_party/blink/renderer/core/paint/replaced_painter.cc
+++ b/third_party/blink/renderer/core/paint/replaced_painter.cc
@@ -4,8 +4,10 @@
 
 #include "third_party/blink/renderer/core/paint/replaced_painter.h"
 
+#include "base/metrics/histogram_macros.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/web_feature.h"
 #include "third_party/blink/renderer/core/layout/layout_replaced.h"
 #include "third_party/blink/renderer/core/layout/svg/layout_svg_root.h"
 #include "third_party/blink/renderer/core/paint/box_painter.h"
@@ -21,6 +23,7 @@
 #include "third_party/blink/renderer/platform/graphics/paint/display_item_cache_skipper.h"
 #include "third_party/blink/renderer/platform/graphics/paint/drawing_recorder.h"
 #include "third_party/blink/renderer/platform/graphics/paint/scoped_paint_chunk_properties.h"
+#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
 
 namespace blink {
 
@@ -163,6 +166,24 @@
                                                         layout_replaced_);
     layout_replaced_.PaintReplaced(content_paint_state.GetPaintInfo(),
                                    content_paint_state.PaintOffset());
+
+    if (layout_replaced_.BelongsToElementChangingOverflowBehaviour() &&
+        !layout_replaced_.ClipsToContentBox() &&
+        layout_replaced_.HasVisualOverflow()) {
+      UseCounter::Count(layout_replaced_.GetDocument(),
+                        WebFeature::kReplacedElementPaintedWithOverflow);
+
+      auto overflow_size = layout_replaced_.PhysicalVisualOverflowRect().size;
+      auto overflow_area = overflow_size.width * overflow_size.height;
+
+      auto content_size = layout_replaced_.Size();
+      auto content_area = content_size.Width() * content_size.Height();
+
+      DCHECK_GT(overflow_area, content_area);
+      UMA_HISTOGRAM_COUNTS_100000(
+          "Blink.Overflow.ReplacedElementAreaOutsideContentRect",
+          (overflow_area - content_area).ToInt());
+    }
   }
 
   if (layout_replaced_.StyleRef().Visibility() == EVisibility::kVisible &&
diff --git a/third_party/blink/renderer/platform/graphics/compositing/pending_layer.cc b/third_party/blink/renderer/platform/graphics/compositing/pending_layer.cc
index 5d103e7..b91fbc0 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/pending_layer.cc
+++ b/third_party/blink/renderer/platform/graphics/compositing/pending_layer.cc
@@ -424,14 +424,14 @@
          !node->IsRoot() && !can_be_decomposited.Contains(node);
          node = &node->Parent()->Unalias()) {
       if (!node->IsIdentityOr2DTranslation() || node->ScrollNode() ||
-          node->GetStickyConstraint() ||
-          node->IsAffectedByOuterViewportBoundsDelta() ||
           node->HasDirectCompositingReasonsOtherThan3dTransform() ||
           !node->FlattensInheritedTransformSameAsParent() ||
           !node->BackfaceVisibilitySameAsParent()) {
         mark_not_decompositable(*node);
         break;
       }
+      DCHECK(!node->GetStickyConstraint());
+      DCHECK(!node->IsAffectedByOuterViewportBoundsDelta());
       can_be_decomposited.insert(node, true);
     }
 
diff --git a/third_party/blink/renderer/platform/graphics/compositing_reasons.h b/third_party/blink/renderer/platform/graphics/compositing_reasons.h
index cff06d6..54668f2 100644
--- a/third_party/blink/renderer/platform/graphics/compositing_reasons.h
+++ b/third_party/blink/renderer/platform/graphics/compositing_reasons.h
@@ -118,13 +118,13 @@
 
     // Various combinations of compositing reasons are defined here also, for
     // more intuitive and faster bitwise logic.
-    kComboScrollDependentPosition = kFixedPosition | kStickyPosition,
+
     // Note that translate is not included, because we care about transforms
     // that are not IsIdentityOrTranslation().
     kPreventingSubpixelAccumulationReasons =
         kWillChangeTransform | kWillChangeScale | kWillChangeRotate,
     kDirectReasonsForPaintOffsetTranslationProperty =
-        kComboScrollDependentPosition | kAffectedByOuterViewportBoundsDelta |
+        kFixedPosition | kAffectedByOuterViewportBoundsDelta |
         kFixedToViewport | kVideo | kCanvas | kPlugin | kIFrame,
     // TODO(dbaron): kWillChangeOther probably shouldn't be in this list.
     kDirectReasonsForTransformProperty =
diff --git a/third_party/blink/renderer/platform/graphics/paint/transform_paint_property_node.h b/third_party/blink/renderer/platform/graphics/paint/transform_paint_property_node.h
index fc78cdf..16270ae 100644
--- a/third_party/blink/renderer/platform/graphics/paint/transform_paint_property_node.h
+++ b/third_party/blink/renderer/platform/graphics/paint/transform_paint_property_node.h
@@ -364,9 +364,8 @@
     return DirectCompositingReasons() & CompositingReason::kFixedToViewport;
   }
 
-  bool RequiresCompositingForScrollDependentPosition() const {
-    return DirectCompositingReasons() &
-           CompositingReason::kComboScrollDependentPosition;
+  bool RequiresCompositingForStickyPosition() const {
+    return DirectCompositingReasons() & CompositingReason::kStickyPosition;
   }
 
   CompositingReasons DirectCompositingReasonsForDebugging() const {
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index cf1c788..f160a1e 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -1896,7 +1896,6 @@
     },
     {
       name: "PrivateNetworkAccessPermissionPrompt",
-      status: "test",
     },
     {
       name: "PushMessaging",
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 35d7cb1..0d3aaaae 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -1788,8 +1788,6 @@
 crbug.com/1078927 virtual/layout_ng_table_frag/fragmentation/border-spacing-break-before-unbreakable-row.html [ Failure ]
 crbug.com/1078927 virtual/layout_ng_table_frag/fragmentation/fragmented-rowspan-alignment.html [ Failure ]
 crbug.com/1078927 virtual/layout_ng_table_frag/fragmentation/fragmented-rowspan.html [ Failure ]
-crbug.com/1078927 virtual/layout_ng_table_frag/fragmentation/nested-repeating-thead-2.html [ Failure ]
-crbug.com/1078927 virtual/layout_ng_table_frag/fragmentation/nested-repeating-thead-3.html [ Failure ]
 crbug.com/1078927 virtual/layout_ng_table_frag/fragmentation/no-repeating-table-header-after-sections.html [ Failure ]
 crbug.com/1078927 virtual/layout_ng_table_frag/fragmentation/single-line-cells-multiple-tables-caption-repeating-thead-tfoot-with-border-spacing-at-top-of-row-2.html [ Failure ]
 crbug.com/1078927 virtual/layout_ng_table_frag/fragmentation/single-line-cells-multiple-tables-caption-repeating-thead-tfoot-with-border-spacing-at-top-of-row-3.html [ Failure ]
diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites
index d467078..e557b009 100644
--- a/third_party/blink/web_tests/VirtualTestSuites
+++ b/third_party/blink/web_tests/VirtualTestSuites
@@ -8,6 +8,12 @@
     ]
   },
   {
+    "prefix": "pna-permission",
+    "platforms": ["Linux", "Mac", "Win", "Fuchsia"],
+    "bases": ["external/wpt/fetch/private-network-access"],
+    "args": ["--enable-features=PrivateNetworkAccessPermissionPrompt"]
+  },
+  {
     "prefix": "disable-accept-language-header",
     "platforms": ["Linux", "Mac", "Win"],
     "bases": ["http/tests/navigation/language"],
diff --git a/third_party/blink/web_tests/fragmentation/nested-repeating-thead-2.html b/third_party/blink/web_tests/fragmentation/nested-repeating-thead-2.html
index 41c504b..197efbb 100644
--- a/third_party/blink/web_tests/fragmentation/nested-repeating-thead-2.html
+++ b/third_party/blink/web_tests/fragmentation/nested-repeating-thead-2.html
@@ -3,7 +3,7 @@
 .header {
   line-height: 80px;
 }
-thead, tfoot {
+thead, tfoot, tr {
   break-inside: avoid;
 }
 </style>
diff --git a/third_party/blink/web_tests/fragmentation/nested-repeating-thead-3.html b/third_party/blink/web_tests/fragmentation/nested-repeating-thead-3.html
index 89b836e..9ff97166 100644
--- a/third_party/blink/web_tests/fragmentation/nested-repeating-thead-3.html
+++ b/third_party/blink/web_tests/fragmentation/nested-repeating-thead-3.html
@@ -3,7 +3,7 @@
 .barcode {
   line-height: 40px;
 }
-thead, tfoot {
+thead, tfoot, tr {
   break-inside: avoid;
 }
 </style>
@@ -31,7 +31,7 @@
         </tr>
         <tr>
           <td>
-            <div style="height: 445px;"></div>
+            <div style="height: 400px;"></div>
           </td>
         </tr>
         <tr>
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/network/same-site-issue-warn-cookie-strict-subresource-context-downgrade.js b/third_party/blink/web_tests/http/tests/inspector-protocol/network/same-site-issue-warn-cookie-strict-subresource-context-downgrade.js
index 41937c62d..69e827b 100644
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/network/same-site-issue-warn-cookie-strict-subresource-context-downgrade.js
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/network/same-site-issue-warn-cookie-strict-subresource-context-downgrade.js
@@ -9,7 +9,7 @@
 
   const setCookieUrl = 'https://cookie.test:8443/inspector-protocol/network/resources/set-cookie.php?cookie='
       + encodeURIComponent('name=value; SameSite=Strict');
-  await session.evaluate(`fetch('${setCookieUrl}', {method: 'POST', credentials: 'include'})`);
+  session.evaluate(`fetch('${setCookieUrl}', {method: 'POST', credentials: 'include'})`);
   const issue = await dp.Audits.onceIssueAdded();
   testRunner.log(issue.params, 'Inspector issue:');
 
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/storage/dom-storage-storage-key-clear.js b/third_party/blink/web_tests/http/tests/inspector-protocol/storage/dom-storage-storage-key-clear.js
new file mode 100644
index 0000000..9fde068
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/storage/dom-storage-storage-key-clear.js
@@ -0,0 +1,24 @@
+(async function(testRunner) {
+  const {dp, session} = await testRunner.startBlank(
+      `Tests that clearing data works for DOMStorage with storageKey\n`);
+
+  await dp.DOMStorage.enable();
+  await dp.Page.enable();
+
+  const addedPromise = dp.DOMStorage.onceDomStorageItemAdded();
+  session.evaluate('window.localStorage.setItem("testKey", "testItem")');
+  const event = await addedPromise;
+  const storageId = event.params.storageId;
+  const storageKey = storageId.storageKey;
+
+  testRunner.log(`Set item: ${await session.evaluate('window.localStorage.getItem("testKey")')}`)
+  testRunner.log(`storageId.storageKey: ${typeof storageKey}`)
+  testRunner.log(storageKey ? "not empty\n" : "empty\n");
+
+  await dp.Storage.clearDataForStorageKey({storageKey: storageKey, storageTypes: 'local_storage'});
+
+  testRunner.log("Clear data");
+  testRunner.log(`Get item: ${await session.evaluate('window.localStorage.getItem("testKey")')}`);
+
+  testRunner.completeTest();
+})
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/storage/dom-storage-storage-key-functionality.js b/third_party/blink/web_tests/http/tests/inspector-protocol/storage/dom-storage-storage-key-functionality.js
index 43b12f5..744ec6a 100644
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/storage/dom-storage-storage-key-functionality.js
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/storage/dom-storage-storage-key-functionality.js
@@ -1,12 +1,12 @@
 (async function(testRunner) {
-  const {dp} = await testRunner.startBlank(
+  const {dp, session} = await testRunner.startBlank(
       `Tests DOMStorage functionality with storageKey\n`);
 
   await dp.DOMStorage.enable();
   await dp.Page.enable();
 
   const addedPromise = dp.DOMStorage.onceDomStorageItemAdded();
-  window.localStorage.setItem("testKey", "testItem");
+  session.evaluate('window.localStorage.setItem("testKey", "testItem")');
   const event = await addedPromise;
   const storageId = event.params.storageId;
 
diff --git a/third_party/blink/web_tests/platform/generic/http/tests/inspector-protocol/storage/dom-storage-storage-key-clear-expected.txt b/third_party/blink/web_tests/platform/generic/http/tests/inspector-protocol/storage/dom-storage-storage-key-clear-expected.txt
new file mode 100644
index 0000000..a2233fb8
--- /dev/null
+++ b/third_party/blink/web_tests/platform/generic/http/tests/inspector-protocol/storage/dom-storage-storage-key-clear-expected.txt
@@ -0,0 +1,9 @@
+Tests that clearing data works for DOMStorage with storageKey
+
+Set item: testItem
+storageId.storageKey: string
+not empty
+
+Clear data
+Get item: null
+
diff --git a/third_party/blink/web_tests/platform/generic/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt b/third_party/blink/web_tests/platform/generic/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt
index f11be43..3855262 100644
--- a/third_party/blink/web_tests/platform/generic/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt
+++ b/third_party/blink/web_tests/platform/generic/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt
@@ -1323,7 +1323,6 @@
     getter referrer
     getter referrerPolicy
     getter signal
-    getter targetAddressSpace
     getter url
     method arrayBuffer
     method blob
diff --git a/third_party/blink/web_tests/platform/generic/webexposed/global-interface-listing-dedicated-worker-expected.txt b/third_party/blink/web_tests/platform/generic/webexposed/global-interface-listing-dedicated-worker-expected.txt
index 08c8d1a3..d3f0dde 100644
--- a/third_party/blink/web_tests/platform/generic/webexposed/global-interface-listing-dedicated-worker-expected.txt
+++ b/third_party/blink/web_tests/platform/generic/webexposed/global-interface-listing-dedicated-worker-expected.txt
@@ -1376,7 +1376,6 @@
 [Worker]     getter referrer
 [Worker]     getter referrerPolicy
 [Worker]     getter signal
-[Worker]     getter targetAddressSpace
 [Worker]     getter url
 [Worker]     method arrayBuffer
 [Worker]     method blob
diff --git a/third_party/blink/web_tests/platform/generic/webexposed/global-interface-listing-expected.txt b/third_party/blink/web_tests/platform/generic/webexposed/global-interface-listing-expected.txt
index de7bd25..6df9e2e 100644
--- a/third_party/blink/web_tests/platform/generic/webexposed/global-interface-listing-expected.txt
+++ b/third_party/blink/web_tests/platform/generic/webexposed/global-interface-listing-expected.txt
@@ -7160,7 +7160,6 @@
     getter referrer
     getter referrerPolicy
     getter signal
-    getter targetAddressSpace
     getter url
     method arrayBuffer
     method blob
diff --git a/third_party/blink/web_tests/platform/generic/webexposed/global-interface-listing-shared-worker-expected.txt b/third_party/blink/web_tests/platform/generic/webexposed/global-interface-listing-shared-worker-expected.txt
index 5c557a45..16c7be69 100644
--- a/third_party/blink/web_tests/platform/generic/webexposed/global-interface-listing-shared-worker-expected.txt
+++ b/third_party/blink/web_tests/platform/generic/webexposed/global-interface-listing-shared-worker-expected.txt
@@ -1215,7 +1215,6 @@
 [Worker]     getter referrer
 [Worker]     getter referrerPolicy
 [Worker]     getter signal
-[Worker]     getter targetAddressSpace
 [Worker]     getter url
 [Worker]     method arrayBuffer
 [Worker]     method blob
diff --git a/third_party/blink/web_tests/virtual/pna-permission/README.md b/third_party/blink/web_tests/virtual/pna-permission/README.md
new file mode 100644
index 0000000..39867dc
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/pna-permission/README.md
@@ -0,0 +1 @@
+This directory is for testing PrivateNetworkAccessPermissionPrompt (https://crbug.com/1338438).
diff --git a/third_party/blink/web_tests/wpt_internal/fenced_frame/unfenced-top.https.html b/third_party/blink/web_tests/wpt_internal/fenced_frame/unfenced-top.https.html
index 5c8bf79a..139f334 100644
--- a/third_party/blink/web_tests/wpt_internal/fenced_frame/unfenced-top.https.html
+++ b/third_party/blink/web_tests/wpt_internal/fenced_frame/unfenced-top.https.html
@@ -250,6 +250,30 @@
   await new_window.execute(() => {});
 }, '_unfencedTop :blob URL failure');
 
+// Test that fragment navigations out of a fenced frame using _unfencedTop
+// do not act as same-document navigations.
+promise_test(async() => {
+  // Create a new window.
+  const new_window = await createAndValidateWindow();
+
+  await new_window.execute(async () => {
+    // Save some state in the document.
+    window.foo = "foo";
+
+    // Navigate _unfencedTop using a fragment URL.
+    window.executor.suspend(() => {
+      window.fenced_frame.execute((parent_url) => {
+        const result = window.open(parent_url+'#foo', '_unfencedTop');
+      }, [location.href]);
+    });
+  });
+
+  // Observe that there is a new document (the old state is lost).
+  await new_window.execute(async () => {
+    assert_equals(window.foo, undefined);
+  });
+}, '_unfencedTop fragment navigation');
+
 async function click(frame) {
   var actions = new test_driver.Actions();
   await actions.pointerMove(0, 0, {origin: frame})
diff --git a/third_party/wpt_tools/README.chromium b/third_party/wpt_tools/README.chromium
index c373a3b..2b5c1fe 100644
--- a/third_party/wpt_tools/README.chromium
+++ b/third_party/wpt_tools/README.chromium
@@ -1,7 +1,7 @@
 Name: web-platform-tests - Test Suites for Web Platform specifications
 Short Name: wpt
 URL: https://github.com/web-platform-tests/wpt/
-Version: 8ee81d9c52f7c47292ba8ae731b52de03a078352
+Version: 6cc880d5283965ca9c9b6eb2a8af4ba0c024f872
 License: LICENSES FOR W3C TEST SUITES (https://www.w3.org/Consortium/Legal/2008/03-bsd-license.html)
 License File: NOT_SHIPPED
 Security Critical: no
diff --git a/third_party/wpt_tools/wpt/tools/wpt/browser.py b/third_party/wpt_tools/wpt/tools/wpt/browser.py
index cf8336b7..cc3e793 100644
--- a/third_party/wpt_tools/wpt/tools/wpt/browser.py
+++ b/third_party/wpt_tools/wpt/tools/wpt/browser.py
@@ -6,7 +6,6 @@
 import stat
 import subprocess
 import tempfile
-import xml.etree.ElementTree as etree  # noqa: N813
 from abc import ABCMeta, abstractmethod
 from datetime import datetime, timedelta
 from distutils.spawn import find_executable
@@ -1452,8 +1451,26 @@
     requirements = "requirements_safari.txt"
 
     def _find_downloads(self):
-        def text_content(e):
-            return etree.tostring(e, encoding="unicode", method="text")
+        def text_content(e, __output=None):
+            # this doesn't use etree.tostring so that we can add spaces for p and br
+            if __output is None:
+                __output = []
+
+            if e.tag == "p":
+                __output.append("\n\n")
+
+            if e.tag == "br":
+                __output.append("\n")
+
+            if e.text is not None:
+                __output.append(e.text)
+
+            for child in e:
+                text_content(child, __output)
+                if child.tail is not None:
+                    __output.append(child.tail)
+
+            return "".join(__output)
 
         self.logger.info("Finding STP download URLs")
         resp = get("https://developer.apple.com/safari/download/")
@@ -1472,38 +1489,50 @@
             if {"download", "dmg", "zip"} & class_names:
                 downloads.append(candidate)
 
-        """
-        When converting to string with text_content, it converts <br> to no space.
-        For example:
-        Input: Safari Technology Preview<br>for macOS&nbsp;Ventura
-        Output: Safari Technology Previewfor macOS Ventura
-        """
-        stp_link_text = re.compile(r"^\s*Safari\s+Technology\s+Preview\s*(?:[0-9]+\s+)?for\s+macOS")
+        # Note we use \s throughout for space as we don't care what form the whitespace takes
+        stp_link_text = re.compile(
+            r"^\s*Safari\s+Technology\s+Preview\s+(?:[0-9]+\s+)?for\s+macOS"
+        )
         requirement = re.compile(
-            r"Requires\s+macOS\s+([0-9\.]+)\s+(?:or\s+later|beta)."
+            r"""(?x)  # (extended regexp syntax for comments)
+            ^\s*Requires\s+macOS\s+  # Starting with the magic string
+            ([0-9\.]+)  # A macOS version number of numbers and dots
+            (?:\s+beta(?:\s+[0-9]+)?)?  # Optionally a beta, itself optionally with a number (no dots!)
+            (?:\s+or\s+later)?  # Optionally an 'or later'
+            \.\s*$  # Ending with a literal dot
+            """
         )
 
         stp_downloads = []
         for download in downloads:
             for link in download.iterfind(".//a[@href]"):
-                if stp_link_text.match(text_content(link)):
+                if stp_link_text.search(text_content(link)):
                     break
+                else:
+                    self.logger.debug("non-matching anchor: " + text_content(link))
             else:
                 continue
 
-            m = requirement.search(text_content(download))
-            if m:
-                version = m.group(1)
+            for el in download.iter():
+                # avoid assuming any given element here, just assume it is a single element
+                m = requirement.search(text_content(el))
+                if m:
+                    version = m.group(1)
 
-                # This assumes the current macOS numbering, whereby X.Y is compatible
-                # with X.(Y+1), e.g. 12.4 is compatible with 12.3, but 13.0 isn't
-                # compatible with 12.3. This doesn't handle the former 10.* numbering.
-                if "." in version:
-                    spec = SpecifierSet(f"~={version}")
-                else:
-                    spec = SpecifierSet(f"=={version}.*")
+                    # This assumes the current macOS numbering, whereby X.Y is compatible
+                    # with X.(Y+1), e.g. 12.4 is compatible with 12.3, but 13.0 isn't
+                    # compatible with 12.3.
+                    if version.count(".") >= (2 if version.startswith("10.") else 1):
+                        spec = SpecifierSet(f"~={version}")
+                    else:
+                        spec = SpecifierSet(f"=={version}.*")
 
-                stp_downloads.append((spec, link.attrib["href"]))
+                    stp_downloads.append((spec, link.attrib["href"]))
+                    break
+            else:
+                self.logger.debug(
+                    "Found a link but no requirement: " + text_content(download)
+                )
 
         if stp_downloads:
             self.logger.info(
diff --git a/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/actions.py b/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/actions.py
index 07ba761..596576b0 100644
--- a/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/actions.py
+++ b/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/actions.py
@@ -70,9 +70,13 @@
     def __init__(self, logger, protocol):
         self.logger = logger
         self.protocol = protocol
+        self.requires_state_reset = False
 
     def __call__(self, payload):
         # TODO: some sort of shallow error checking
+        if self.requires_state_reset:
+            self.reset()
+        self.requires_state_reset = True
         actions = payload["actions"]
         for actionSequence in actions:
             if actionSequence["type"] == "pointer":
@@ -85,6 +89,10 @@
     def get_element(self, element_selector):
         return self.protocol.select.element_by_selector(element_selector)
 
+    def reset(self):
+        self.protocol.action_sequence.release()
+        self.requires_state_reset = False
+
 
 class GenerateTestReportAction:
     name = "generate_test_report"
diff --git a/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/executormarionette.py b/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/executormarionette.py
index 051821c0..fedb7e959 100644
--- a/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/executormarionette.py
+++ b/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/executormarionette.py
@@ -481,6 +481,9 @@
         self.logger.info(actions)
         self.marionette._send_message("WebDriver:PerformActions", actions)
 
+    def release(self):
+        self.marionette._send_message("WebDriver:ReleaseActions", {})
+
 
 class MarionetteTestDriverProtocolPart(TestDriverProtocolPart):
     def setup(self):
diff --git a/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/executorselenium.py b/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/executorselenium.py
index 22842e9..5dc5505 100644
--- a/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/executorselenium.py
+++ b/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/executorselenium.py
@@ -222,6 +222,9 @@
     def send_actions(self, actions):
         self.webdriver.execute(Command.W3C_ACTIONS, {"actions": actions})
 
+    def release(self):
+        self.webdriver.execute(Command.W3C_CLEAR_ACTIONS, {})
+
 
 class SeleniumTestDriverProtocolPart(TestDriverProtocolPart):
     def setup(self):
diff --git a/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/executorwebdriver.py
index 4ba78b2..a5e5e57 100644
--- a/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/executorwebdriver.py
+++ b/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/executorwebdriver.py
@@ -237,6 +237,9 @@
     def send_actions(self, actions):
         self.webdriver.actions.perform(actions['actions'])
 
+    def release(self):
+        self.webdriver.actions.release()
+
 
 class WebDriverTestDriverProtocolPart(TestDriverProtocolPart):
     def setup(self):
@@ -627,6 +630,8 @@
         self.protocol.base.execute_script(self.wait_script, True)
 
         screenshot = self.protocol.webdriver.screenshot()
+        if screenshot is None:
+            raise ValueError('screenshot is None')
 
         # strip off the data:img/png, part of the url
         if screenshot.startswith("data:image/png;base64,"):
diff --git a/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/protocol.py b/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/protocol.py
index 86cfce8..23fa797 100644
--- a/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/protocol.py
+++ b/third_party/wpt_tools/wpt/tools/wptrunner/wptrunner/executors/protocol.py
@@ -384,6 +384,9 @@
         :param actions: A protocol-specific handle to an array of actions."""
         pass
 
+    def release(self):
+        pass
+
 
 class TestDriverProtocolPart(ProtocolPart):
     """Protocol part that implements the basic functionality required for
diff --git a/tools/mb/mb_config.pyl b/tools/mb/mb_config.pyl
index b48dbe6..41dd635 100644
--- a/tools/mb/mb_config.pyl
+++ b/tools/mb/mb_config.pyl
@@ -369,6 +369,7 @@
       'Mac Builder Next': 'mac_arm64_gpu_tests_release_bot_minimal_symbols_no_nacl',
       'Mac deterministic': 'release_bot_mac_strip_minimal_symbols',
       'Mac deterministic (dbg)': 'debug_bot',
+      'Mac deterministic (reclient shadow)': 'release_bot_mac_strip_minimal_symbols_reclient',
       'Site Isolation Android': 'android_release_bot_minimal_symbols_arm64_reclient',
       'VR Linux': 'vr_release_bot_reclient',
       'Win 10 Fast Ring': 'release_trybot_minimal_symbols_reclient',
@@ -616,6 +617,7 @@
       'ios-simulator-noncq': 'ios_simulator_debug_static_bot_xctest',
       'mac-arm64-on-arm64-rel': 'mac_arm64_release_bot',
       'mac-arm64-rel': 'mac_arm64_gpu_tests_release_bot_minimal_symbols_no_nacl',
+      'mac-arm64-rel (reclient shadow)': 'mac_arm64_gpu_tests_release_bot_minimal_symbols_no_nacl_reclient',
     },
 
     'chromium.memory': {
@@ -3374,6 +3376,10 @@
       'release_bot', 'mac_strip', 'minimal_symbols', 'arm64',
     ],
 
+    'release_bot_mac_strip_minimal_symbols_reclient': [
+      'release_bot_reclient', 'mac_strip', 'minimal_symbols',
+    ],
+
     'release_bot_minimal_symbols': [
       'release_bot', 'minimal_symbols',
     ],
diff --git a/tools/mb/mb_config_expectations/chromium.fyi.json b/tools/mb/mb_config_expectations/chromium.fyi.json
index f78e174..25cf058 100644
--- a/tools/mb/mb_config_expectations/chromium.fyi.json
+++ b/tools/mb/mb_config_expectations/chromium.fyi.json
@@ -487,6 +487,16 @@
       "use_goma": true
     }
   },
+  "Mac deterministic (reclient shadow)": {
+    "gn_args": {
+      "dcheck_always_on": false,
+      "enable_stripping": true,
+      "is_component_build": false,
+      "is_debug": false,
+      "symbol_level": 1,
+      "use_remoteexec": true
+    }
+  },
   "Site Isolation Android": {
     "gn_args": {
       "dcheck_always_on": false,
diff --git a/tools/mb/mb_config_expectations/chromium.mac.json b/tools/mb/mb_config_expectations/chromium.mac.json
index 2d52b89a..eca6e79 100644
--- a/tools/mb/mb_config_expectations/chromium.mac.json
+++ b/tools/mb/mb_config_expectations/chromium.mac.json
@@ -127,5 +127,18 @@
       "target_cpu": "arm64",
       "use_goma": true
     }
+  },
+  "mac-arm64-rel (reclient shadow)": {
+    "gn_args": {
+      "dcheck_always_on": false,
+      "enable_nacl": false,
+      "ffmpeg_branding": "Chrome",
+      "is_component_build": false,
+      "is_debug": false,
+      "proprietary_codecs": true,
+      "symbol_level": 1,
+      "target_cpu": "arm64",
+      "use_remoteexec": true
+    }
   }
 }
\ No newline at end of file
diff --git a/tools/metrics/actions/actions.xml b/tools/metrics/actions/actions.xml
index ff1170c9..7bfcce7 100644
--- a/tools/metrics/actions/actions.xml
+++ b/tools/metrics/actions/actions.xml
@@ -3715,6 +3715,12 @@
   </description>
 </action>
 
+<action name="Ash_Clipboard_CopiedItem">
+  <owner>ckincaid@chromium.org</owner>
+  <owner>multipaste@google.com</owner>
+  <description>Emitted when a user copies data to their clipboard.</description>
+</action>
+
 <action name="Ash_ClipboardHistory_PastedItem1">
   <owner>ckincaid@chromium.org</owner>
   <owner>multipaste@google.com</owner>
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index f25ffe7b..a541261 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -40441,8 +40441,8 @@
   <int value="3896" label="WebBluetoothManufacturerDataFilter"/>
   <int value="3897" label="SanitizerAPIGetConfig"/>
   <int value="3898" label="SanitizerAPIGetDefaultConfig"/>
-  <int value="3899" label="ComputePressureObserver_Constructor"/>
-  <int value="3900" label="ComputePressureObserver_Observe"/>
+  <int value="3899" label="PressureObserver_Constructor"/>
+  <int value="3900" label="PressureObserver_Observe"/>
   <int value="3901" label="OBSOLETE_ComputePressureObserver_Stop"/>
   <int value="3902" label="WebAppWindowControlsOverlay"/>
   <int value="3903" label="PaymentRequestShowWithoutGestureOrToken"/>
@@ -40739,9 +40739,9 @@
   <int value="4181" label="BluetoothDeviceForget"/>
   <int value="4182" label="TopicsAPI_BrowsingTopics_Method"/>
   <int value="4183" label="BlockingAttributeRenderToken"/>
-  <int value="4184" label="ComputePressureObserver_Unobserve"/>
-  <int value="4185" label="ComputePressureObserver_Disconnect"/>
-  <int value="4186" label="ComputePressureObserver_TakeRecords"/>
+  <int value="4184" label="PressureObserver_Unobserve"/>
+  <int value="4185" label="PressureObserver_Disconnect"/>
+  <int value="4186" label="PressureObserver_TakeRecords"/>
   <int value="4187" label="PrivacySandboxAdsAPIs"/>
   <int value="4188" label="Fledge"/>
   <int value="4189" label="ElementShowPopup"/>
@@ -40871,6 +40871,7 @@
   <int value="4308" label="SendBeaconWithFormData"/>
   <int value="4309" label="SendBeaconWithURLSearchParams"/>
   <int value="4310" label="SendBeaconWithUSVString"/>
+  <int value="4311" label="ReplacedElementPaintedWithOverflow"/>
 </enum>
 
 <enum name="FeaturePolicyAllowlistType">
@@ -60231,6 +60232,7 @@
   <int value="934236781" label="OmniboxSuggestionsRecyclerView:enabled"/>
   <int value="934292666" label="DownloadRename:enabled"/>
   <int value="934805020" label="CornerShortcuts:disabled"/>
+  <int value="935653796" label="CrOSLateBootMediaDynamicCgroup:enabled"/>
   <int value="935655516" label="password-import-export:disabled"/>
   <int value="936341613" label="OfflinePagesCT:disabled"/>
   <int value="936919953" label="bypass-app-banner-engagement-checks"/>
@@ -60805,6 +60807,7 @@
   <int value="1293697921" label="RequestDesktopSiteExceptions:enabled"/>
   <int value="1294131571" label="disable-winrt-midi-api"/>
   <int value="1294306055" label="FeedInteractiveRefresh:disabled"/>
+  <int value="1296661698" label="CrOSLateBootMediaDynamicCgroup:disabled"/>
   <int value="1296878388" label="PermissionPredictions:disabled"/>
   <int value="1296958520" label="hide-active-apps-from-shelf"/>
   <int value="1298734793" label="OsSettingsAppNotificationsPage:disabled"/>
@@ -76365,6 +76368,38 @@
   <int value="25" label="DOM_LOADING"/>
 </enum>
 
+<enum name="PerformanceMonitor.UsageScenario.LongInterval">
+  <int value="1" label="All Tabs Hidden Audio"/>
+  <int value="2" label="All Tabs Hidden No Video Capture or Audio"/>
+  <int value="4" label="All Tabs Hidden Video Capture"/>
+  <int value="5" label="Audio"/>
+  <int value="6" label="Embedded Video No Navigation"/>
+  <int value="7" label="Embedded Video With Navigation"/>
+  <int value="8" label="Fullscreen Video"/>
+  <int value="9" label="Interaction"/>
+  <int value="10" label="Navigation"/>
+  <int value="11" label="Passive"/>
+  <int value="12" label="Video capture"/>
+  <int value="13" label="Zero Window"/>
+</enum>
+
+<enum name="PerformanceMonitor.UsageScenario.ShortInterval">
+  <int value="1" label="All Tabs Hidden Audio"/>
+  <int value="2" label="All Tabs Hidden No Video Capture or Audio"/>
+  <int value="3" label="All Tabs Hidden No Video Capture or Audio (Recent)"/>
+  <int value="4" label="All Tabs Hidden Video Capture"/>
+  <int value="5" label="Audio"/>
+  <int value="6" label="Embedded Video No Navigation"/>
+  <int value="7" label="Embedded Video With Navigation"/>
+  <int value="8" label="Fullscreen Video"/>
+  <int value="9" label="Interaction"/>
+  <int value="10" label="Navigation"/>
+  <int value="11" label="Passive"/>
+  <int value="12" label="Video capture"/>
+  <int value="13" label="Zero Window"/>
+  <int value="14" label="Zero Window (Recent)"/>
+</enum>
+
 <enum name="PeripheralConnectivityResult">
 <!-- This must be kept current with PeripheralConnectivityResults located in
     ash/components/peripheral_notification/peripheral_notification_manager.h -->
diff --git a/tools/metrics/histograms/metadata/autofill/histograms.xml b/tools/metrics/histograms/metadata/autofill/histograms.xml
index 6a993f5..4298cec 100644
--- a/tools/metrics/histograms/metadata/autofill/histograms.xml
+++ b/tools/metrics/histograms/metadata/autofill/histograms.xml
@@ -1730,6 +1730,10 @@
 <histogram
     name="Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue"
     enum="Boolean" expires_after="2022-12-25">
+  <obsolete>
+    Deprecated in July 2022. Subsumed by
+    Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue2.
+  </obsolete>
   <owner>vidhanj@google.com</owner>
   <owner>koerber@google.com</owner>
   <owner>chrome-autofill-alerts@google.com</owner>
@@ -1741,6 +1745,20 @@
   </summary>
 </histogram>
 
+<histogram
+    name="Autofill.IsValueNotAutofilledOverExistingValueSameAsSubmittedValue2"
+    enum="Boolean" expires_after="2022-12-25">
+  <owner>vidhanj@google.com</owner>
+  <owner>koerber@google.com</owner>
+  <owner>chrome-autofill-alerts@google.com</owner>
+  <summary>
+    This metric is recorderd on form submission for each field that had an
+    initial value on page load and was edited by the user later on. The result
+    of the equality comparison between the submitted field value and supposedly
+    autofillable value is emitted by this metric.
+  </summary>
+</histogram>
+
 <histogram name="Autofill.KeyMetrics.FillingAcceptance{AutofillFormType}"
     enum="BooleanAutofillFillingAcceptance" expires_after="2022-12-12">
   <owner>battre@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/blink/histograms.xml b/tools/metrics/histograms/metadata/blink/histograms.xml
index 4fd0d39..af984c0 100644
--- a/tools/metrics/histograms/metadata/blink/histograms.xml
+++ b/tools/metrics/histograms/metadata/blink/histograms.xml
@@ -2126,6 +2126,16 @@
   </summary>
 </histogram>
 
+<histogram name="Blink.Overflow.ReplacedElementAreaOutsideContentRect"
+    units="pixels" expires_after="2022-12-01">
+  <owner>khushalsagar@chromium.org</owner>
+  <owner>vmpstr@chromium.org</owner>
+  <summary>
+    Computes the area of img, video or canvas elements which is painted outside
+    the content rect if 'overflow' property is respected on these elements.
+  </summary>
+</histogram>
+
 <histogram name="Blink.Paint.PaintArtifactCompositorUpdateReason"
     enum="PaintArtifactCompositorUpdateReason" expires_after="2022-12-01">
   <owner>pdr@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/cryptohome/histograms.xml b/tools/metrics/histograms/metadata/cryptohome/histograms.xml
index 4bc5df44..ba0cdbd 100644
--- a/tools/metrics/histograms/metadata/cryptohome/histograms.xml
+++ b/tools/metrics/histograms/metadata/cryptohome/histograms.xml
@@ -786,6 +786,32 @@
   </summary>
 </histogram>
 
+<histogram name="Cryptohome.TimeToUSSLoadPersisted" units="ms"
+    expires_after="2023-04-01">
+  <owner>thomascedeno@google.com</owner>
+  <owner>cros-hwsec+uma@chromium.org</owner>
+  <summary>
+    The amount of time cryptohome spends reading persistent storage to populate
+    an encrypted USS container flatbuffer, or the time
+    UserSecretStash::LoadPersisted() takes to complete. The metric is emitted
+    during user authentication in cryptohome, when an AuthFactor must be
+    authenticated and UserSecretStash must read the corresponding USS file.
+  </summary>
+</histogram>
+
+<histogram name="Cryptohome.TimeToUSSPersist" units="ms"
+    expires_after="2023-04-01">
+  <owner>thomascedeno@google.com</owner>
+  <owner>cros-hwsec+uma@chromium.org</owner>
+  <summary>
+    The amount of time cryptohome spends writing the USS container flatbuffer to
+    persistent storage. This metric records the time UserSecretStash::Persist()
+    takes to complete. The metric is emitted when a user adds, removes or
+    updates a means of authentication or AuthFactor. Manipulating an AuthFactor
+    requires UserSecretStash to write to the corresponding USS file.
+  </summary>
+</histogram>
+
 <histogram name="Cryptohome.TpmResults" enum="CryptohomeTpmResults"
     expires_after="2022-10-30">
   <owner>afakhry@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/power/histograms.xml b/tools/metrics/histograms/metadata/power/histograms.xml
index ad57c57ad..48269e7 100644
--- a/tools/metrics/histograms/metadata/power/histograms.xml
+++ b/tools/metrics/histograms/metadata/power/histograms.xml
@@ -496,6 +496,36 @@
   <token key="UsageScenario" variants="UsageScenario"/>
 </histogram>
 
+<histogram name="PerformanceMonitor.UsageScenario.LongInterval"
+    enum="PerformanceMonitor.UsageScenario.LongInterval"
+    expires_after="2023-06-18">
+  <owner>olivierli@chromium.org</owner>
+  <owner>catan-team@chromium.org</owner>
+  <summary>
+    Record the usage scenario that applies to the last 2 mins of Chrome use.
+    Recorded every two minutes on a timer. For more information on user
+    scenarios see go/chrome_power_use_per_scenario.
+
+    Values in the enum match the variants present in the
+    &quot;UsageScenario&quot; variants.
+  </summary>
+</histogram>
+
+<histogram name="PerformanceMonitor.UsageScenario.ShortInterval"
+    enum="PerformanceMonitor.UsageScenario.ShortInterval"
+    expires_after="2023-06-18">
+  <owner>olivierli@chromium.org</owner>
+  <owner>catan-team@chromium.org</owner>
+  <summary>
+    Record the usage scenario that applies to the last 10 seconds of Chrome use.
+    Recorded every two minutes on a timer. For more information on user
+    scenarios see go/chrome_power_use_per_scenario.
+
+    Values in the enum match the variants present in the
+    &quot;UsageScenario10Sec&quot; variants.
+  </summary>
+</histogram>
+
 <histogram name="Power.AdaptiveChargingBatteryPercentageOnUnplug" units="%"
     expires_after="2023-03-19">
   <owner>dbasehore@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/quick_answers/histograms.xml b/tools/metrics/histograms/metadata/quick_answers/histograms.xml
index 6c447a9..ee5f2bf 100644
--- a/tools/metrics/histograms/metadata/quick_answers/histograms.xml
+++ b/tools/metrics/histograms/metadata/quick_answers/histograms.xml
@@ -116,6 +116,16 @@
   </token>
 </histogram>
 
+<histogram name="QuickAnswers.DictionaryIntent.Language" enum="LanguageName"
+    expires_after="2023-07-01">
+  <owner>updowndota@chromium.org</owner>
+  <owner>croissant-eng@chromium.org</owner>
+  <summary>
+    For Quick answers fetch, records the query text language of dictionary
+    intent generated on device. ChromeOS only.
+  </summary>
+</histogram>
+
 <histogram name="QuickAnswers.DictionaryIntent.Source"
     enum="QuickAnswersDictionaryIntentSource" expires_after="2023-07-01">
   <owner>updowndota@chromium.org</owner>
diff --git a/tools/perf/core/perfetto_binary_roller/binary_deps.json b/tools/perf/core/perfetto_binary_roller/binary_deps.json
index 54ca75b7..0486f75c 100644
--- a/tools/perf/core/perfetto_binary_roller/binary_deps.json
+++ b/tools/perf/core/perfetto_binary_roller/binary_deps.json
@@ -5,8 +5,8 @@
             "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux_arm64/49b4b5dcbc312d8d2c3751cf29238b8efeb4e494/trace_processor_shell"
         },
         "win": {
-            "hash": "224082e63ee0725d55c44bf7b585ebad9dd44997",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/c2235982559f7008e5384d5145e1febe571573cd/trace_processor_shell.exe"
+            "hash": "f438948d0367b33e3ce4a580cb00f9b1540ae548",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/361efbf9aab595e4dfa79ec48f242d9e722393c9/trace_processor_shell.exe"
         },
         "linux_arm": {
             "hash": "58893933be305d3bfe0a72ebebcacde2ac3ca893",
@@ -21,8 +21,8 @@
             "full_remote_path": "perfetto-luci-artifacts/v25.0/mac-arm64/trace_processor_shell"
         },
         "linux": {
-            "hash": "ac75be567c3531560e0b72f4e15081bf587e172d",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/c2235982559f7008e5384d5145e1febe571573cd/trace_processor_shell"
+            "hash": "c21d40b89142cf06456f64df5b23a7d00a274961",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/361efbf9aab595e4dfa79ec48f242d9e722393c9/trace_processor_shell"
         }
     },
     "power_profile.sql": {
diff --git a/ui/aura/env.cc b/ui/aura/env.cc
index 219ffda5..0f7b3b3 100644
--- a/ui/aura/env.cc
+++ b/ui/aura/env.cc
@@ -14,12 +14,14 @@
 #include "base/observer_list_types.h"
 #include "build/build_config.h"
 #include "ui/aura/client/aura_constants.h"
+#include "ui/aura/client/screen_position_client.h"
 #include "ui/aura/env_input_state_controller.h"
 #include "ui/aura/env_observer.h"
 #include "ui/aura/input_state_lookup.h"
 #include "ui/aura/window.h"
 #include "ui/aura/window_event_dispatcher_observer.h"
 #include "ui/aura/window_occlusion_tracker.h"
+#include "ui/display/screen.h"
 #include "ui/events/event_observer.h"
 #include "ui/events/event_target_iterator.h"
 #include "ui/events/gestures/gesture_recognizer_impl.h"
@@ -152,6 +154,33 @@
   gesture_recognizer_ = std::move(gesture_recognizer);
 }
 
+gfx::Point Env::GetLastPointerPoint(ui::mojom::DragEventSource event_source,
+                                    Window* window,
+                                    absl::optional<gfx::Point> fallback) {
+  if (event_source == ui::mojom::DragEventSource::kTouch && is_touch_down()) {
+    DCHECK(window);
+    DCHECK(window->GetRootWindow());
+    gfx::PointF touch_point_f;
+    bool got_touch_point = gesture_recognizer()->GetLastTouchPointForTarget(
+        window, &touch_point_f);
+    if (got_touch_point) {
+      Window* root = window->GetRootWindow();
+      DCHECK(root);
+      DCHECK(root->GetRootWindow());
+      DCHECK(aura::client::GetScreenPositionClient(root->GetRootWindow()));
+      client::GetScreenPositionClient(root->GetRootWindow())
+          ->ConvertPointToScreen(root, &touch_point_f);
+      return gfx::ToFlooredPoint(touch_point_f);
+    }
+    // Fallback when touch state is lost. See http://crbug.com/1162541.
+    if (fallback)
+      return *fallback;
+  }
+
+  // TODO(https://crbug.com/1338746): Use last_mouse_location_.
+  return display::Screen::GetScreen()->GetCursorScreenPoint();
+}
+
 WindowOcclusionTracker* Env::GetWindowOcclusionTracker() {
   if (!window_occlusion_tracker_) {
     // Use base::WrapUnique + new because of the constructor is private.
diff --git a/ui/aura/env.h b/ui/aura/env.h
index fc2c99d..0b947f2 100644
--- a/ui/aura/env.h
+++ b/ui/aura/env.h
@@ -14,6 +14,7 @@
 #include "build/build_config.h"
 #include "mojo/public/cpp/system/buffer.h"
 #include "ui/aura/aura_export.h"
+#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
 #include "ui/events/event_target.h"
 #include "ui/events/types/event_type.h"
 #include "ui/gfx/geometry/point.h"
@@ -113,10 +114,15 @@
   ui::GestureRecognizer* gesture_recognizer() {
     return gesture_recognizer_.get();
   }
-
   void SetGestureRecognizer(
       std::unique_ptr<ui::GestureRecognizer> gesture_recognizer);
 
+  // The `fallback` parameter allows callers of this API to specify a
+  // value to be returned in the case of a missing touch state.
+  gfx::Point GetLastPointerPoint(ui::mojom::DragEventSource event_source,
+                                 aura::Window* window,
+                                 absl::optional<gfx::Point> fallback);
+
   // Get WindowOcclusionTracker instance. Create one if not yet created.
   WindowOcclusionTracker* GetWindowOcclusionTracker();
 
diff --git a/ui/display/BUILD.gn b/ui/display/BUILD.gn
index 18cce0c..c18d939 100644
--- a/ui/display/BUILD.gn
+++ b/ui/display/BUILD.gn
@@ -184,6 +184,8 @@
     sources += [
       "mac/test/test_screen_mac.h",
       "mac/test/test_screen_mac.mm",
+      "mac/test/virtual_display_mac_util.h",
+      "mac/test/virtual_display_mac_util.mm",
     ]
   }
 
@@ -279,3 +281,19 @@
     ]
   }
 }
+
+# This target is added as a dependency of browser interactive_ui_tests. It must
+# be source_set, otherwise the linker will drop the tests as dead code.
+source_set("display_interactive_ui_tests") {
+  testonly = true
+  if (is_mac) {
+    sources = [ "mac/test/virtual_display_mac_util_interactive_uitest.mm" ]
+
+    deps = [
+      ":display",
+      ":test_support",
+      "//base/test:test_support",
+      "//testing/gtest",
+    ]
+  }
+}
diff --git a/ui/display/mac/test/virtual_display_mac_util.h b/ui/display/mac/test/virtual_display_mac_util.h
new file mode 100644
index 0000000..846d8f3
--- /dev/null
+++ b/ui/display/mac/test/virtual_display_mac_util.h
@@ -0,0 +1,113 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef UI_DISPLAY_MAC_TEST_VIRTUAL_DISPLAY_MAC_UTIL_H_
+#define UI_DISPLAY_MAC_TEST_VIRTUAL_DISPLAY_MAC_UTIL_H_
+
+#import <IOKit/pwr_mgt/IOPMLib.h>
+#include <stdint.h>
+
+#include "base/containers/flat_set.h"
+#include "base/run_loop.h"
+#include "ui/display/display_observer.h"
+#include "ui/display/types/display_constants.h"
+
+namespace display {
+class Display;
+
+namespace test {
+struct DisplayParams;
+
+// This interface creates system-level virtual displays to support the automated
+// integration testing of display information and window placement APIs in
+// multi-screen device environments. It updates the displays that the normal mac
+// screen impl sees, but not `TestScreenMac`.
+class VirtualDisplayMacUtil : public display::DisplayObserver {
+ public:
+  VirtualDisplayMacUtil();
+  ~VirtualDisplayMacUtil() override;
+
+  VirtualDisplayMacUtil(const VirtualDisplayMacUtil&) = delete;
+  VirtualDisplayMacUtil& operator=(const VirtualDisplayMacUtil&) = delete;
+
+  void WarmUp();
+
+  // `display_id` is only used to label the virtual display. This function
+  // returns the generated display::Display id, which can be used with the
+  // Screen instance or passed to `RemoveDisplay`.
+  int64_t AddDisplay(int64_t display_id, const DisplayParams& display_params);
+  void RemoveDisplay(int64_t display_id);
+
+  // Check whether the related CoreGraphics APIs are available in the current
+  // system version.
+  static bool IsAPIAvailable();
+
+  // Preset display configuration parameters.
+  static const DisplayParams k6016x3384;
+  static const DisplayParams k5120x2880;
+  static const DisplayParams k4096x2304;
+  static const DisplayParams k3840x2400;
+  static const DisplayParams k3840x2160;
+  static const DisplayParams k3840x1600;
+  static const DisplayParams k3840x1080;
+  static const DisplayParams k3072x1920;
+  static const DisplayParams k2880x1800;
+  static const DisplayParams k2560x1600;
+  static const DisplayParams k2560x1440;
+  static const DisplayParams k2304x1440;
+  static const DisplayParams k2048x1536;
+  static const DisplayParams k2048x1152;
+  static const DisplayParams k1920x1200;
+  static const DisplayParams k1600x1200;
+  static const DisplayParams k1920x1080;
+  static const DisplayParams k1680x1050;
+  static const DisplayParams k1440x900;
+  static const DisplayParams k1400x1050;
+  static const DisplayParams k1366x768;
+  static const DisplayParams k1280x1024;
+  static const DisplayParams k1280x1800;
+
+ private:
+  class DisplaySleepBlocker {
+   public:
+    DisplaySleepBlocker();
+    ~DisplaySleepBlocker();
+
+    DisplaySleepBlocker(const DisplaySleepBlocker&) = delete;
+    DisplaySleepBlocker& operator=(const DisplaySleepBlocker&) = delete;
+
+   private:
+    // Track the AssertionID argument to IOPMAssertionCreateWithProperties and
+    // IOPMAssertionRelease.
+    IOPMAssertionID assertion_id_ = kIOPMNullAssertionID;
+  };
+
+  // display::DisplayObserver:
+  void OnDisplayMetricsChanged(const display::Display& display,
+                               uint32_t changed_metrics) override;
+  void OnDisplayAdded(const display::Display& new_display) override;
+  void OnDisplayRemoved(const display::Display& old_display) override;
+
+  void OnDisplayAddedOrRemoved(int64_t id);
+
+  // Remove all virtual displays before leaving the scope of
+  // VirtualDisplayMacUtil.
+  void RemoveAllDisplays();
+
+  // Wait for the display with the given `id` to be added.
+  // Return immediately if the display is already available.
+  void WaitForDisplay(int64_t id, bool added);
+
+  bool NeedWarmUp();
+
+  base::flat_set<int64_t> waiting_for_ids_;
+  std::unique_ptr<base::RunLoop> run_loop_;
+
+  DisplaySleepBlocker display_sleep_blocker_;
+};
+
+}  // namespace test
+}  // namespace display
+
+#endif  // UI_DISPLAY_MAC_TEST_VIRTUAL_DISPLAY_MAC_UTIL_H_
diff --git a/ui/display/mac/test/virtual_display_mac_util.mm b/ui/display/mac/test/virtual_display_mac_util.mm
new file mode 100644
index 0000000..99147bd4
--- /dev/null
+++ b/ui/display/mac/test/virtual_display_mac_util.mm
@@ -0,0 +1,602 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/display/mac/test/virtual_display_mac_util.h"
+
+#include <CoreGraphics/CoreGraphics.h>
+#import <Foundation/Foundation.h>
+
+#include <map>
+
+#include "base/auto_reset.h"
+#include "base/check.h"
+#include "base/check_op.h"
+#include "base/mac/scoped_nsobject.h"
+#include "build/build_config.h"
+#include "ui/display/display.h"
+#include "ui/display/screen.h"
+#include "ui/gfx/geometry/size.h"
+
+// These interfaces were generated from CoreGraphics binaries.
+API_AVAILABLE(macos(10.14))
+@interface CGVirtualDisplay : NSObject {
+  unsigned int _vendorID;
+  unsigned int _productID;
+  unsigned int _serialNum;
+  NSString* _name;
+  struct CGSize _sizeInMillimeters;
+  unsigned int _maxPixelsWide;
+  unsigned int _maxPixelsHigh;
+  struct CGPoint _redPrimary;
+  struct CGPoint _greenPrimary;
+  struct CGPoint _bluePrimary;
+  struct CGPoint _whitePoint;
+  id _queue;
+  id _terminationHandler;
+  unsigned int _displayID;
+  unsigned int _hiDPI;
+  NSArray* _modes;
+}
+
+@property(readonly, nonatomic)
+    unsigned int vendorID;  // @synthesize vendorID=_vendorID;
+@property(readonly, nonatomic)
+    unsigned int productID;  // @synthesize productID=_productID;
+@property(readonly, nonatomic)
+    unsigned int serialNum;  // @synthesize serialNum=_serialNum;
+@property(readonly, nonatomic) NSString* name;  // @synthesize name=_name;
+@property(readonly, nonatomic) struct CGSize
+    sizeInMillimeters;  // @synthesize sizeInMillimeters=_sizeInMillimeters;
+@property(readonly, nonatomic)
+    unsigned int maxPixelsWide;  // @synthesize maxPixelsWide=_maxPixelsWide;
+@property(readonly, nonatomic)
+    unsigned int maxPixelsHigh;  // @synthesize maxPixelsHigh=_maxPixelsHigh;
+@property(readonly, nonatomic)
+    struct CGPoint redPrimary;  // @synthesize redPrimary=_redPrimary;
+@property(readonly, nonatomic)
+    struct CGPoint greenPrimary;  // @synthesize greenPrimary=_greenPrimary;
+@property(readonly, nonatomic)
+    struct CGPoint bluePrimary;  // @synthesize bluePrimary=_bluePrimary;
+@property(readonly, nonatomic)
+    struct CGPoint whitePoint;            // @synthesize whitePoint=_whitePoint;
+@property(readonly, nonatomic) id queue;  // @synthesize queue=_queue;
+@property(readonly, nonatomic) id
+    terminationHandler;  // @synthesize terminationHandler=_terminationHandler;
+@property(readonly, nonatomic)
+    unsigned int displayID;  // @synthesize displayID=_displayID;
+@property(readonly, nonatomic) unsigned int hiDPI;  // @synthesize hiDPI=_hiDPI;
+@property(readonly, nonatomic) NSArray* modes;      // @synthesize modes=_modes;
+- (BOOL)applySettings:(id)arg1;
+- (void)dealloc;
+- (id)initWithDescriptor:(id)arg1;
+
+@end
+
+// These interfaces were generated from CoreGraphics binaries.
+API_AVAILABLE(macos(10.14))
+@interface CGVirtualDisplayDescriptor : NSObject {
+  unsigned int _vendorID;
+  unsigned int _productID;
+  unsigned int _serialNum;
+  NSString* _name;
+  struct CGSize _sizeInMillimeters;
+  unsigned int _maxPixelsWide;
+  unsigned int _maxPixelsHigh;
+  struct CGPoint _redPrimary;
+  struct CGPoint _greenPrimary;
+  struct CGPoint _bluePrimary;
+  struct CGPoint _whitePoint;
+  id _queue;
+  id _terminationHandler;
+}
+
+@property(nonatomic) unsigned int vendorID;  // @synthesize vendorID=_vendorID;
+@property(nonatomic)
+    unsigned int productID;  // @synthesize productID=_productID;
+@property(nonatomic)
+    unsigned int serialNum;  // @synthesize serialNum=_serialNum;
+@property(strong, nonatomic) NSString* name;  // @synthesize name=_name;
+@property(nonatomic) struct CGSize
+    sizeInMillimeters;  // @synthesize sizeInMillimeters=_sizeInMillimeters;
+@property(nonatomic)
+    unsigned int maxPixelsWide;  // @synthesize maxPixelsWide=_maxPixelsWide;
+@property(nonatomic)
+    unsigned int maxPixelsHigh;  // @synthesize maxPixelsHigh=_maxPixelsHigh;
+@property(nonatomic)
+    struct CGPoint redPrimary;  // @synthesize redPrimary=_redPrimary;
+@property(nonatomic)
+    struct CGPoint greenPrimary;  // @synthesize greenPrimary=_greenPrimary;
+@property(nonatomic)
+    struct CGPoint bluePrimary;  // @synthesize bluePrimary=_bluePrimary;
+@property(nonatomic)
+    struct CGPoint whitePoint;          // @synthesize whitePoint=_whitePoint;
+@property(retain, nonatomic) id queue;  // @synthesize queue=_queue;
+@property(copy, nonatomic) id
+    terminationHandler;  // @synthesize terminationHandler=_terminationHandler;
+- (void)dealloc;
+- (id)init;
+- (id)dispatchQueue;
+- (void)setDispatchQueue:(id)arg1;
+
+@end
+
+// These interfaces were generated from CoreGraphics binaries.
+API_AVAILABLE(macos(10.14))
+@interface CGVirtualDisplayMode : NSObject {
+  unsigned int _width;
+  unsigned int _height;
+  double _refreshRate;
+}
+
+@property(readonly, nonatomic) unsigned int width;  // @synthesize width=_width;
+@property(readonly, nonatomic)
+    unsigned int height;  // @synthesize height=_height;
+@property(readonly, nonatomic)
+    double refreshRate;  // @synthesize refreshRate=_refreshRate;
+- (id)initWithWidth:(unsigned int)arg1
+             height:(unsigned int)arg2
+        refreshRate:(double)arg3;
+
+@end
+
+// These interfaces were generated from CoreGraphics binaries.
+API_AVAILABLE(macos(10.14))
+@interface CGVirtualDisplaySettings : NSObject {
+  NSArray* _modes;
+  unsigned int _hiDPI;
+}
+
+@property(strong, nonatomic) NSArray* modes;  // @synthesize modes=_modes;
+@property(nonatomic) unsigned int hiDPI;      // @synthesize hiDPI=_hiDPI;
+- (void)dealloc;
+- (id)init;
+
+@end
+
+namespace {
+
+static bool g_during_warm_up = false;
+
+base::flat_set<int64_t> g_unexpected_display_ids;
+
+// A global singleton that tracks the current set of mocked displays.
+std::map<int64_t, base::scoped_nsobject<CGVirtualDisplay>> g_display_map
+    API_AVAILABLE(macos(10.14));
+
+// A helper function for creating virtual display and return CGVirtualDisplay
+// object.
+base::scoped_nsobject<CGVirtualDisplay> CreateVirtualDisplay(int width,
+                                                             int height,
+                                                             int ppi,
+                                                             BOOL hiDPI,
+                                                             NSString* name)
+    API_AVAILABLE(macos(10.14)) {
+  base::scoped_nsobject<CGVirtualDisplaySettings> settings(
+      [[CGVirtualDisplaySettings alloc] init]);
+  [settings setHiDPI:hiDPI];
+
+  base::scoped_nsobject<CGVirtualDisplayDescriptor> descriptor(
+      [[CGVirtualDisplayDescriptor alloc] init]);
+  [descriptor
+      setQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
+  [descriptor setName:name];
+
+  // See System Preferences > Displays > Color > Open Profile > Apple display
+  // native information
+  [descriptor setWhitePoint:CGPointMake(0.3125, 0.3291)];
+  [descriptor setBluePrimary:CGPointMake(0.1494, 0.0557)];
+  [descriptor setGreenPrimary:CGPointMake(0.2559, 0.6983)];
+  [descriptor setRedPrimary:CGPointMake(0.6797, 0.3203)];
+  [descriptor setMaxPixelsHigh:height];
+  [descriptor setMaxPixelsWide:width];
+  [descriptor
+      setSizeInMillimeters:CGSizeMake(25.4 * width / ppi, 25.4 * height / ppi)];
+  [descriptor setSerialNum:0];
+  [descriptor setProductID:0];
+  [descriptor setVendorID:0];
+
+  base::scoped_nsobject<CGVirtualDisplay> display(
+      [[CGVirtualDisplay alloc] initWithDescriptor:descriptor]);
+
+  if ([settings hiDPI]) {
+    width /= 2;
+    height /= 2;
+  }
+  base::scoped_nsobject<CGVirtualDisplayMode> mode([[CGVirtualDisplayMode alloc]
+      initWithWidth:width
+             height:height
+        refreshRate:60]);
+  [settings setModes:@[ mode ]];
+
+  if (![display applySettings:settings])
+    return base::scoped_nsobject<CGVirtualDisplay>();
+
+  return display;
+}
+
+void DealWithUnexpectedDisplay(int64_t id) {
+  if (g_unexpected_display_ids.count(id)) {
+    g_unexpected_display_ids.erase(id);
+  } else {
+    g_unexpected_display_ids.insert(id);
+  }
+}
+
+// This method detects whether the local machine is running headless.
+// Typically returns true when the session is curtained or if there are no
+// physical monitors attached.  In those two scenarios, the online display will
+// be marked as virtual.
+bool IsRunningHeadless() {
+  // Most machines will have < 4 displays but a larger upper bound won't hurt.
+  constexpr UInt32 kMaxDisplaysToQuery = 32;
+  // 0x76697274 is a 4CC value for 'virt' which indicates the display is
+  // virtual.
+  constexpr CGDirectDisplayID kVirtualDisplayID = 0x76697274;
+
+  CGDirectDisplayID online_displays[kMaxDisplaysToQuery];
+  UInt32 online_display_count = 0;
+  CGError return_code = CGGetOnlineDisplayList(
+      kMaxDisplaysToQuery, online_displays, &online_display_count);
+  if (return_code != kCGErrorSuccess) {
+    LOG(ERROR) << __func__
+               << " - CGGetOnlineDisplayList() failed: " << return_code << ".";
+    // If this fails, assume machine is headless to err on the side of caution.
+    return true;
+  }
+
+  bool is_running_headless = true;
+  for (UInt32 i = 0; i < online_display_count; i++) {
+    if (CGDisplayModelNumber(online_displays[i]) != kVirtualDisplayID) {
+      // At least one monitor is attached so the machine is not headless.
+      is_running_headless = false;
+      break;
+    }
+  }
+
+  // TODO(crbug.com/1126278): Please remove this log or replace it with
+  // [D]CHECK() ASAP when the TEST is stable.
+  LOG(INFO) << __func__ << " - Is running headless: " << is_running_headless
+            << ". Online display count: " << online_display_count << ".";
+
+  return is_running_headless;
+}
+
+}  // namespace
+
+namespace display::test {
+
+struct DisplayParams {
+  DisplayParams(int width,
+                int height,
+                int ppi,
+                bool hiDPI,
+                std::string description)
+      : width(width),
+        height(height),
+        ppi(ppi),
+        hiDPI(hiDPI),
+        description([NSString
+            stringWithCString:description.c_str()
+                     encoding:[NSString defaultCStringEncoding]]) {}
+
+  bool IsValid() const {
+    return width > 0 && height > 0 && ppi > 0 && [description length] > 0;
+  }
+
+  int width;
+  int height;
+  int ppi;
+  BOOL hiDPI;
+  base::scoped_nsobject<NSString> description;
+};
+
+VirtualDisplayMacUtil::VirtualDisplayMacUtil() {
+  display::Screen::GetScreen()->AddObserver(this);
+}
+
+VirtualDisplayMacUtil::~VirtualDisplayMacUtil() {
+  RemoveAllDisplays();
+  display::Screen::GetScreen()->RemoveObserver(this);
+}
+
+void VirtualDisplayMacUtil::WarmUp() {
+  constexpr int kMaxRetryCount = 4;
+
+  // TODO(crbug.com/1126278): Please remove this log or replace it with
+  // [D]CHECK() ASAP when the TEST is stable.
+  LOG(INFO) << "VirtualDisplayMacUtil::" << __func__ << " - start.";
+
+  base::AutoReset<bool> auto_reset(&g_during_warm_up, true);
+
+  int retry_count = 0;
+  do {
+    for (int64_t display_id : {1, 2}) {
+      AddDisplay(display_id, k1920x1080);
+    }
+    RemoveAllDisplays();
+
+    retry_count++;
+  } while (!g_unexpected_display_ids.empty() && retry_count <= kMaxRetryCount);
+
+  // TODO(crbug.com/1126278): Please remove this log or replace it with
+  // [D]CHECK() ASAP when the TEST is stable.
+  LOG(INFO) << "VirtualDisplayMacUtil::" << __func__ << " - end.";
+}
+
+int64_t VirtualDisplayMacUtil::AddDisplay(int64_t display_id,
+                                          const DisplayParams& display_params) {
+  if (@available(macos 10.14, *)) {
+    bool need_warm_up = NeedWarmUp();
+
+    DCHECK(display_params.IsValid());
+
+    NSString* display_name =
+        [NSString stringWithFormat:@"Virtual Display #%lld", display_id];
+    base::scoped_nsobject<CGVirtualDisplay> display = CreateVirtualDisplay(
+        display_params.width, display_params.height, display_params.ppi,
+        display_params.hiDPI, display_name);
+    DCHECK(display.get());
+
+    // TODO(crbug.com/1126278): Please remove this log or replace it with
+    // [D]CHECK() ASAP when the TEST is stable.
+    LOG(INFO) << "VirtualDisplayMacUtil::" << __func__
+              << " - display id: " << display_id
+              << ". CreateVirtualDisplay success.";
+
+    int64_t id = [display displayID];
+    DCHECK_NE(id, 0u);
+
+    WaitForDisplay(id, /*added=*/true);
+
+    // TODO(crbug.com/1126278): Please remove this log or replace it with
+    // [D]CHECK() ASAP when the TEST is stable.
+    LOG(INFO) << "VirtualDisplayMacUtil::" << __func__
+              << " - display id: " << display_id << "(" << id
+              << "). WaitForDisplay success.";
+
+    DCHECK(!g_display_map[id]);
+    g_display_map[id] = display;
+
+    if (need_warm_up) {
+      int64_t tmp_id = AddDisplay(0, display_params);
+      RemoveDisplay(tmp_id);
+    }
+
+    return id;
+  }
+  return display::kInvalidDisplayId;
+}
+
+void VirtualDisplayMacUtil::RemoveDisplay(int64_t display_id) {
+  if (@available(macos 10.14, *)) {
+    auto it = g_display_map.find(display_id);
+    DCHECK(it != g_display_map.end());
+
+    g_display_map.erase(it);
+
+    // TODO(crbug.com/1126278): Please remove this log or replace it with
+    // [D]CHECK() ASAP when the TEST is stable.
+    LOG(INFO) << "VirtualDisplayMacUtil::" << __func__
+              << " - display id: " << display_id << ". Erase success.";
+
+    WaitForDisplay(display_id, /*added=*/false);
+
+    // TODO(crbug.com/1126278): Please remove this log or replace it with
+    // [D]CHECK() ASAP when the TEST is stable.
+    LOG(INFO) << "VirtualDisplayMacUtil::" << __func__
+              << " - display id: " << display_id << ". WaitForDisplay success.";
+  }
+}
+
+// static
+bool VirtualDisplayMacUtil::IsAPIAvailable() {
+  bool is_arch_cpu_arm_family = false;
+#if defined(ARCH_CPU_ARM_FAMILY)
+  is_arch_cpu_arm_family = true;
+#endif  // defined(ARCH_CPU_ARM_FAMILY)
+  if (is_arch_cpu_arm_family) {
+    return false;
+  }
+
+  bool is_api_available = false;
+  if (@available(macos 12.0, *)) {
+    is_api_available = true;
+  }
+  if (!is_api_available) {
+    return false;
+  }
+
+  // Only support non-headless bots now. Some odd behavior happened when
+  // enable on headless bots. See
+  // https://ci.chromium.org/ui/p/chromium/builders/try/mac-rel/1058024/test-results
+  // for details.
+  // TODO(crbug.com/1126278): Remove this restriction when support headless
+  // environment.
+  if (IsRunningHeadless()) {
+    return false;
+  }
+
+  return true;
+}
+
+// Predefined display configurations from
+// https://en.wikipedia.org/wiki/Graphics_display_resolution and
+// https://www.theverge.com/tldr/2016/3/21/11278192/apple-iphone-ipad-screen-sizes-pixels-density-so-many-choices.
+const DisplayParams VirtualDisplayMacUtil::k6016x3384 =
+    DisplayParams(6016, 3384, 218, true, "Apple Pro Display XDR");
+const DisplayParams VirtualDisplayMacUtil::k5120x2880 =
+    DisplayParams(5120, 2880, 218, true, "27-inch iMac with Retina 5K display");
+const DisplayParams VirtualDisplayMacUtil::k4096x2304 =
+    DisplayParams(4096,
+                  2304,
+                  219,
+                  true,
+                  "21.5-inch iMac with Retina 4K display");
+const DisplayParams VirtualDisplayMacUtil::k3840x2400 =
+    DisplayParams(3840, 2400, 200, true, "WQUXGA");
+const DisplayParams VirtualDisplayMacUtil::k3840x2160 =
+    DisplayParams(3840, 2160, 200, true, "UHD");
+const DisplayParams VirtualDisplayMacUtil::k3840x1600 =
+    DisplayParams(3840, 1600, 200, true, "WQHD+, UW-QHD+");
+const DisplayParams VirtualDisplayMacUtil::k3840x1080 =
+    DisplayParams(3840, 1080, 200, true, "DFHD");
+const DisplayParams VirtualDisplayMacUtil::k3072x1920 =
+    DisplayParams(3072,
+                  1920,
+                  226,
+                  true,
+                  "16-inch MacBook Pro with Retina display");
+const DisplayParams VirtualDisplayMacUtil::k2880x1800 =
+    DisplayParams(2880,
+                  1800,
+                  220,
+                  true,
+                  "15.4-inch MacBook Pro with Retina display");
+const DisplayParams VirtualDisplayMacUtil::k2560x1600 =
+    DisplayParams(2560,
+                  1600,
+                  227,
+                  true,
+                  "WQXGA, 13.3-inch MacBook Pro with Retina display");
+const DisplayParams VirtualDisplayMacUtil::k2560x1440 =
+    DisplayParams(2560, 1440, 109, false, "27-inch Apple Thunderbolt display");
+const DisplayParams VirtualDisplayMacUtil::k2304x1440 =
+    DisplayParams(2304, 1440, 226, true, "12-inch MacBook with Retina display");
+const DisplayParams VirtualDisplayMacUtil::k2048x1536 =
+    DisplayParams(2048, 1536, 150, false, "QXGA");
+const DisplayParams VirtualDisplayMacUtil::k2048x1152 =
+    DisplayParams(2048, 1152, 150, false, "QWXGA");
+const DisplayParams VirtualDisplayMacUtil::k1920x1200 =
+    DisplayParams(1920, 1200, 150, false, "WUXGA");
+const DisplayParams VirtualDisplayMacUtil::k1600x1200 =
+    DisplayParams(1600, 1200, 125, false, "UXGA");
+const DisplayParams VirtualDisplayMacUtil::k1920x1080 =
+    DisplayParams(1920, 1080, 102, false, "HD, 21.5-inch iMac");
+const DisplayParams VirtualDisplayMacUtil::k1680x1050 =
+    DisplayParams(1680,
+                  1050,
+                  99,
+                  false,
+                  "WSXGA+, Apple Cinema Display (20-inch), 20-inch iMac");
+const DisplayParams VirtualDisplayMacUtil::k1440x900 =
+    DisplayParams(1440, 900, 127, false, "WXGA+, 13.3-inch MacBook Air");
+const DisplayParams VirtualDisplayMacUtil::k1400x1050 =
+    DisplayParams(1400, 1050, 125, false, "SXGA+");
+const DisplayParams VirtualDisplayMacUtil::k1366x768 =
+    DisplayParams(1366, 768, 135, false, "11.6-inch MacBook Air");
+const DisplayParams VirtualDisplayMacUtil::k1280x1024 =
+    DisplayParams(1280, 1024, 100, false, "SXGA");
+const DisplayParams VirtualDisplayMacUtil::k1280x1800 =
+    DisplayParams(1280, 800, 113, false, "13.3-inch MacBook Pro");
+
+VirtualDisplayMacUtil::DisplaySleepBlocker::DisplaySleepBlocker() {
+  IOReturn result = IOPMAssertionCreateWithName(
+      kIOPMAssertionTypeNoDisplaySleep, kIOPMAssertionLevelOn,
+      CFSTR("DisplaySleepBlocker"), &assertion_id_);
+  DCHECK_EQ(result, kIOReturnSuccess);
+}
+
+VirtualDisplayMacUtil::DisplaySleepBlocker::~DisplaySleepBlocker() {
+  IOReturn result = IOPMAssertionRelease(assertion_id_);
+  DCHECK_EQ(result, kIOReturnSuccess);
+}
+
+void VirtualDisplayMacUtil::OnDisplayMetricsChanged(
+    const display::Display& display,
+    uint32_t changed_metrics) {
+  LOG(INFO) << "VirtualDisplayMacUtil::" << __func__
+            << " - display id: " << display.id()
+            << ", changed_metrics: " << changed_metrics << ".";
+}
+
+void VirtualDisplayMacUtil::OnDisplayAdded(
+    const display::Display& new_display) {
+  // TODO(crbug.com/1126278): Please remove this log or replace it with
+  // [D]CHECK() ASAP when the TEST is stable.
+  LOG(INFO) << "VirtualDisplayMacUtil::" << __func__
+            << " - display id: " << new_display.id() << ".";
+
+  OnDisplayAddedOrRemoved(new_display.id());
+}
+
+void VirtualDisplayMacUtil::OnDisplayRemoved(
+    const display::Display& old_display) {
+  // TODO(crbug.com/1126278): Please remove this log or replace it with
+  // [D]CHECK() ASAP when the TEST is stable.
+  LOG(INFO) << "VirtualDisplayMacUtil::" << __func__
+            << " - display id: " << old_display.id() << ".";
+
+  OnDisplayAddedOrRemoved(old_display.id());
+}
+
+void VirtualDisplayMacUtil::OnDisplayAddedOrRemoved(int64_t id) {
+  if (!waiting_for_ids_.count(id)) {
+    DealWithUnexpectedDisplay(id);
+    return;
+  }
+
+  waiting_for_ids_.erase(id);
+  if (!waiting_for_ids_.empty()) {
+    return;
+  }
+
+  run_loop_->Quit();
+}
+
+void VirtualDisplayMacUtil::RemoveAllDisplays() {
+  if (@available(macos 10.14, *)) {
+    int display_count = g_display_map.size();
+
+    // TODO(crbug.com/1126278): Please remove this log or replace it with
+    // [D]CHECK() ASAP when the TEST is stable.
+    LOG(INFO) << "VirtualDisplayMacUtil::" << __func__
+              << " - display count: " << display_count << ".";
+
+    if (!display_count) {
+      return;
+    }
+
+    auto iter = g_display_map.begin();
+    while (iter != g_display_map.end()) {
+      waiting_for_ids_.insert(iter->first);
+      iter++;
+    }
+
+    g_display_map.clear();
+
+    run_loop_ = std::make_unique<base::RunLoop>();
+    run_loop_->Run();
+  }
+}
+
+void VirtualDisplayMacUtil::WaitForDisplay(int64_t id, bool added) {
+  display::Display d;
+  if (display::Screen::GetScreen()->GetDisplayWithDisplayId(id, &d) == added) {
+    return;
+  }
+
+  // TODO(crbug.com/1126278): Please remove this log or replace it with
+  // [D]CHECK() ASAP when the TEST is stable.
+  LOG(INFO) << "VirtualDisplayMacUtil::" << __func__ << " - display id: " << id
+            << "(added: " << added << "). Start waiting.";
+
+  waiting_for_ids_.insert(id);
+
+  run_loop_ = std::make_unique<base::RunLoop>();
+  run_loop_->Run();
+}
+
+bool VirtualDisplayMacUtil::NeedWarmUp() {
+  if (g_during_warm_up) {
+    return false;
+  }
+
+  if (@available(macos 10.14, *)) {
+    return g_display_map.empty();
+  }
+
+  return false;
+}
+
+}  // namespace display::test
diff --git a/ui/display/mac/test/virtual_display_mac_util_interactive_uitest.mm b/ui/display/mac/test/virtual_display_mac_util_interactive_uitest.mm
new file mode 100644
index 0000000..04b24fe
--- /dev/null
+++ b/ui/display/mac/test/virtual_display_mac_util_interactive_uitest.mm
@@ -0,0 +1,95 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/test/task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/display/mac/test/virtual_display_mac_util.h"
+#include "ui/display/screen.h"
+#include "ui/display/types/display_constants.h"
+
+class VirtualDisplayMacUtilInteractiveUitest : public testing::Test {
+ protected:
+  VirtualDisplayMacUtilInteractiveUitest() = default;
+
+  VirtualDisplayMacUtilInteractiveUitest(
+      const VirtualDisplayMacUtilInteractiveUitest&) = delete;
+  VirtualDisplayMacUtilInteractiveUitest& operator=(
+      const VirtualDisplayMacUtilInteractiveUitest&) = delete;
+
+  void SetUp() override {
+    if (!display::test::VirtualDisplayMacUtil::IsAPIAvailable()) {
+      GTEST_SKIP() << "Skipping test for MacOS 11.0 and older or Arm Macs.";
+    }
+
+    testing::Test::SetUp();
+  }
+
+ private:
+  display::ScopedNativeScreen screen_;
+  base::test::TaskEnvironment task_environment_{
+      base::test::TaskEnvironment::MainThreadType::UI};
+};
+
+TEST_F(VirtualDisplayMacUtilInteractiveUitest, WarmUp) {
+  int display_count = display::Screen::GetScreen()->GetNumDisplays();
+
+  display::test::VirtualDisplayMacUtil virtual_display_mac_util;
+  virtual_display_mac_util.WarmUp();
+
+  EXPECT_EQ(display::Screen::GetScreen()->GetNumDisplays(), display_count);
+}
+
+TEST_F(VirtualDisplayMacUtilInteractiveUitest, AddDisplay) {
+  display::test::VirtualDisplayMacUtil virtual_display_mac_util;
+  virtual_display_mac_util.WarmUp();
+
+  int64_t id = virtual_display_mac_util.AddDisplay(
+      1, display::test::VirtualDisplayMacUtil::k1920x1080);
+  EXPECT_NE(id, display::kInvalidDisplayId);
+
+  display::Display d;
+  bool found = display::Screen::GetScreen()->GetDisplayWithDisplayId(id, &d);
+  EXPECT_TRUE(found);
+}
+
+TEST_F(VirtualDisplayMacUtilInteractiveUitest, RemoveDisplay) {
+  display::test::VirtualDisplayMacUtil virtual_display_mac_util;
+  virtual_display_mac_util.WarmUp();
+
+  int64_t id = virtual_display_mac_util.AddDisplay(
+      1, display::test::VirtualDisplayMacUtil::k1920x1080);
+  int display_count = display::Screen::GetScreen()->GetNumDisplays();
+  EXPECT_GT(display_count, 1);
+
+  virtual_display_mac_util.RemoveDisplay(id);
+  EXPECT_EQ(display::Screen::GetScreen()->GetNumDisplays(), display_count - 1);
+
+  display::Display d;
+  bool found = display::Screen::GetScreen()->GetDisplayWithDisplayId(id, &d);
+  EXPECT_FALSE(found);
+}
+
+TEST_F(VirtualDisplayMacUtilInteractiveUitest, IsAPIAvailable) {
+  EXPECT_TRUE(display::test::VirtualDisplayMacUtil::IsAPIAvailable());
+}
+
+TEST_F(VirtualDisplayMacUtilInteractiveUitest, HotPlug) {
+  int display_count = display::Screen::GetScreen()->GetNumDisplays();
+
+  std::unique_ptr<display::test::VirtualDisplayMacUtil>
+      virtual_display_mac_util =
+          std::make_unique<display::test::VirtualDisplayMacUtil>();
+  virtual_display_mac_util->WarmUp();
+
+  virtual_display_mac_util->AddDisplay(
+      1, display::test::VirtualDisplayMacUtil::k1920x1080);
+  EXPECT_EQ(display::Screen::GetScreen()->GetNumDisplays(), display_count + 1);
+
+  virtual_display_mac_util->AddDisplay(
+      2, display::test::VirtualDisplayMacUtil::k1920x1080);
+  EXPECT_EQ(display::Screen::GetScreen()->GetNumDisplays(), display_count + 2);
+
+  virtual_display_mac_util.reset();
+  EXPECT_EQ(display::Screen::GetScreen()->GetNumDisplays(), display_count);
+}
diff --git a/ui/display/manager/configure_displays_task.cc b/ui/display/manager/configure_displays_task.cc
index 37a2dbec..98653ce 100644
--- a/ui/display/manager/configure_displays_task.cc
+++ b/ui/display/manager/configure_displays_task.cc
@@ -231,9 +231,11 @@
 ConfigureDisplaysTask::ConfigureDisplaysTask(
     NativeDisplayDelegate* delegate,
     const std::vector<DisplayConfigureRequest>& requests,
-    ResponseCallback callback)
+    ResponseCallback callback,
+    ConfigurationType configuration_type)
     : delegate_(delegate),
       requests_(requests),
+      configuration_type_(configuration_type),
       callback_(std::move(callback)),
       task_status_(SUCCESS) {
   delegate_->AddObserver(this);
@@ -270,10 +272,13 @@
       is_first_attempt ? &ConfigureDisplaysTask::OnFirstAttemptConfigured
                        : &ConfigureDisplaysTask::OnRetryConfigured;
 
+  uint32_t modeset_flags = display::kTestModeset | display::kCommitModeset;
+  if (configuration_type_ == kConfigurationTypeSeamless)
+    modeset_flags |= display::kSeamlessModeset;
   delegate_->Configure(
       config_requests,
       base::BindOnce(on_configured, weak_ptr_factory_.GetWeakPtr()),
-      display::kTestModeset | display::kCommitModeset);
+      modeset_flags);
 }
 
 void ConfigureDisplaysTask::OnConfigurationChanged() {}
diff --git a/ui/display/manager/configure_displays_task.h b/ui/display/manager/configure_displays_task.h
index 50ebdb9..77dd865 100644
--- a/ui/display/manager/configure_displays_task.h
+++ b/ui/display/manager/configure_displays_task.h
@@ -14,6 +14,7 @@
 #include "base/containers/queue.h"
 #include "base/memory/weak_ptr.h"
 #include "ui/display/manager/display_manager_export.h"
+#include "ui/display/types/display_constants.h"
 #include "ui/display/types/native_display_observer.h"
 #include "ui/gfx/geometry/point.h"
 
@@ -69,9 +70,11 @@
 
   using ResponseCallback = base::OnceCallback<void(Status)>;
 
-  ConfigureDisplaysTask(NativeDisplayDelegate* delegate,
-                        const std::vector<DisplayConfigureRequest>& requests,
-                        ResponseCallback callback);
+  ConfigureDisplaysTask(
+      NativeDisplayDelegate* delegate,
+      const std::vector<DisplayConfigureRequest>& requests,
+      ResponseCallback callback,
+      ConfigurationType configuration_type = kConfigurationTypeFull);
 
   ConfigureDisplaysTask(const ConfigureDisplaysTask&) = delete;
   ConfigureDisplaysTask& operator=(const ConfigureDisplaysTask&) = delete;
@@ -130,6 +133,10 @@
   // Holds the next configuration request to attempt modeset.
   std::vector<DisplayConfigureRequest> requests_;
 
+  // Whether this request should be seamless or not (i.e. should a full modeset
+  // be permitted or not).
+  const ConfigurationType configuration_type_;
+
   // A queue of display requests grouped by their
   // |requests_[index]->display->base_connector_id()|. These request groups are
   // used to downgrade displays' modes stored in |requests_| when the original
diff --git a/ui/display/manager/display_configurator.cc b/ui/display/manager/display_configurator.cc
index af9f38e..76e8df0 100644
--- a/ui/display/manager/display_configurator.cc
+++ b/ui/display/manager/display_configurator.cc
@@ -765,7 +765,7 @@
       native_display_delegate_.get(), layout_manager_.get(),
       requested_display_state_, GetRequestedPowerState(),
       kSetDisplayPowerForceProbe, kRefreshRateThrottleDisabled,
-      /*force_configure=*/true,
+      /*force_configure=*/true, kConfigurationTypeFull,
       base::BindOnce(&DisplayConfigurator::OnConfigured,
                      weak_ptr_factory_.GetWeakPtr()));
   configuration_task_->Run();
@@ -1026,13 +1026,18 @@
     CallAndClearQueuedCallbacks(true);
     return;
   }
+  ConfigurationType configuration_type = kConfigurationTypeFull;
+  if (!HasPendingFullConfiguration()) {
+    DCHECK(HasPendingSeamlessConfiguration());
+    configuration_type = kConfigurationTypeSeamless;
+  }
 
   configuration_task_ = std::make_unique<UpdateDisplayConfigurationTask>(
       native_display_delegate_.get(), layout_manager_.get(),
       requested_display_state_, pending_power_state_, pending_power_flags_,
       pending_refresh_rate_throttle_state_.value_or(
           kRefreshRateThrottleDisabled),
-      force_configure_,
+      force_configure_, configuration_type,
       base::BindOnce(&DisplayConfigurator::OnConfigured,
                      weak_ptr_factory_.GetWeakPtr()));
 
@@ -1105,6 +1110,10 @@
 }
 
 bool DisplayConfigurator::ShouldRunConfigurationTask() const {
+  return HasPendingSeamlessConfiguration() || HasPendingFullConfiguration();
+}
+
+bool DisplayConfigurator::HasPendingFullConfiguration() const {
   if (force_configure_)
     return true;
 
@@ -1117,13 +1126,14 @@
   if (has_pending_power_state_)
     return true;
 
-  // Schedule if there is a pending request to change the refresh rate.
-  if (pending_refresh_rate_throttle_state_)
-    return true;
-
   return false;
 }
 
+bool DisplayConfigurator::HasPendingSeamlessConfiguration() const {
+  // Schedule if there is a pending request to change the refresh rate.
+  return pending_refresh_rate_throttle_state_.has_value();
+}
+
 void DisplayConfigurator::CallAndClearInProgressCallbacks(bool success) {
   for (auto& callback : in_progress_configuration_callbacks_)
     std::move(callback).Run(success);
diff --git a/ui/display/manager/display_configurator.h b/ui/display/manager/display_configurator.h
index 57a0e0c..a904aa3 100644
--- a/ui/display/manager/display_configurator.h
+++ b/ui/display/manager/display_configurator.h
@@ -349,6 +349,14 @@
   // otherwise.
   bool ShouldRunConfigurationTask() const;
 
+  // Returns true if there are pending configuration changes that should be done
+  // seamlessly.
+  bool HasPendingSeamlessConfiguration() const;
+
+  // Returns true if there are pending configuration changes that require a full
+  // modeset.
+  bool HasPendingFullConfiguration() const;
+
   // Helper functions which will call the callbacks in
   // |in_progress_configuration_callbacks_| and
   // |queued_configuration_callbacks_| and clear the lists after. |success| is
diff --git a/ui/display/manager/update_display_configuration_task.cc b/ui/display/manager/update_display_configuration_task.cc
index 7182fd1..21cf3d1 100644
--- a/ui/display/manager/update_display_configuration_task.cc
+++ b/ui/display/manager/update_display_configuration_task.cc
@@ -47,6 +47,7 @@
     int power_flags,
     RefreshRateThrottleState refresh_rate_throttle_state,
     bool force_configure,
+    ConfigurationType configuration_type,
     ResponseCallback callback)
     : delegate_(delegate),
       layout_manager_(layout_manager),
@@ -55,6 +56,7 @@
       power_flags_(power_flags),
       refresh_rate_throttle_state_(refresh_rate_throttle_state),
       force_configure_(force_configure),
+      configuration_type_(configuration_type),
       callback_(std::move(callback)),
       requesting_displays_(false) {
   delegate_->AddObserver(this);
@@ -123,10 +125,8 @@
     return;
   }
   if (!requests.empty()) {
-    // TODO(b:238361145) Plumb seamless modeset for refresh rate throttle
-    // changes.
     configure_task_ = std::make_unique<ConfigureDisplaysTask>(
-        delegate_, requests, std::move(callback));
+        delegate_, requests, std::move(callback), configuration_type_);
     configure_task_->Run();
   } else {
     VLOG(2) << "No displays";
diff --git a/ui/display/manager/update_display_configuration_task.h b/ui/display/manager/update_display_configuration_task.h
index c9d1b2ce..deff9c6c 100644
--- a/ui/display/manager/update_display_configuration_task.h
+++ b/ui/display/manager/update_display_configuration_task.h
@@ -40,6 +40,7 @@
       int power_flags,
       RefreshRateThrottleState refresh_rate_throttle_state,
       bool force_configure,
+      ConfigurationType configuration_type,
       ResponseCallback callback);
 
   UpdateDisplayConfigurationTask(const UpdateDisplayConfigurationTask&) =
@@ -104,6 +105,10 @@
 
   bool force_configure_;
 
+  // Whether the configuration task should be done without blanking the
+  // displays.
+  const ConfigurationType configuration_type_;
+
   // Used to signal that the task has finished.
   ResponseCallback callback_;
 
diff --git a/ui/display/manager/update_display_configuration_task_unittest.cc b/ui/display/manager/update_display_configuration_task_unittest.cc
index 474fbf1..d546295 100644
--- a/ui/display/manager/update_display_configuration_task_unittest.cc
+++ b/ui/display/manager/update_display_configuration_task_unittest.cc
@@ -235,6 +235,7 @@
     UpdateDisplayConfigurationTask task(
         &delegate_, &layout_manager_, MULTIPLE_DISPLAY_STATE_HEADLESS,
         chromeos::DISPLAY_POWER_ALL_ON, 0, kRefreshRateThrottleEnabled, false,
+        kConfigurationTypeFull,
         base::BindOnce(&UpdateDisplayConfigurationTaskTest::ResponseCallback,
                        base::Unretained(this)));
     task.Run();
@@ -254,6 +255,7 @@
     UpdateDisplayConfigurationTask task(
         &delegate_, &layout_manager_, MULTIPLE_DISPLAY_STATE_SINGLE,
         chromeos::DISPLAY_POWER_ALL_ON, 0, kRefreshRateThrottleEnabled, false,
+        kConfigurationTypeFull,
         base::BindOnce(&UpdateDisplayConfigurationTaskTest::ResponseCallback,
                        base::Unretained(this)));
     task.Run();
@@ -277,6 +279,7 @@
     UpdateDisplayConfigurationTask task(
         &delegate_, &layout_manager_, MULTIPLE_DISPLAY_STATE_MULTI_EXTENDED,
         chromeos::DISPLAY_POWER_ALL_ON, 0, kRefreshRateThrottleEnabled, false,
+        kConfigurationTypeFull,
         base::BindOnce(&UpdateDisplayConfigurationTaskTest::ResponseCallback,
                        base::Unretained(this)));
     task.Run();
@@ -305,6 +308,7 @@
     UpdateDisplayConfigurationTask task(
         &delegate_, &layout_manager_, MULTIPLE_DISPLAY_STATE_MULTI_MIRROR,
         chromeos::DISPLAY_POWER_ALL_ON, 0, kRefreshRateThrottleEnabled, false,
+        kConfigurationTypeFull,
         base::BindOnce(&UpdateDisplayConfigurationTaskTest::ResponseCallback,
                        base::Unretained(this)));
     task.Run();
@@ -332,6 +336,7 @@
     UpdateDisplayConfigurationTask task(
         &delegate_, &layout_manager_, MULTIPLE_DISPLAY_STATE_MULTI_MIRROR,
         chromeos::DISPLAY_POWER_ALL_ON, 0, kRefreshRateThrottleEnabled, false,
+        kConfigurationTypeFull,
         base::BindOnce(&UpdateDisplayConfigurationTaskTest::ResponseCallback,
                        base::Unretained(this)));
     task.Run();
@@ -350,6 +355,7 @@
     UpdateDisplayConfigurationTask task(
         &delegate_, &layout_manager_, MULTIPLE_DISPLAY_STATE_MULTI_EXTENDED,
         chromeos::DISPLAY_POWER_ALL_ON, 0, kRefreshRateThrottleEnabled, false,
+        kConfigurationTypeFull,
         base::BindOnce(&UpdateDisplayConfigurationTaskTest::ResponseCallback,
                        base::Unretained(this)));
     task.Run();
@@ -404,6 +410,7 @@
     UpdateDisplayConfigurationTask task(
         &delegate_, &layout_manager_, MULTIPLE_DISPLAY_STATE_SINGLE,
         chromeos::DISPLAY_POWER_ALL_ON, 0, kRefreshRateThrottleEnabled, false,
+        kConfigurationTypeFull,
         base::BindOnce(&UpdateDisplayConfigurationTaskTest::ResponseCallback,
                        base::Unretained(this)));
     task.Run();
@@ -424,6 +431,7 @@
     UpdateDisplayConfigurationTask task(
         &delegate_, &layout_manager_, MULTIPLE_DISPLAY_STATE_SINGLE,
         chromeos::DISPLAY_POWER_ALL_OFF, 0, kRefreshRateThrottleEnabled, false,
+        kConfigurationTypeFull,
         base::BindOnce(&UpdateDisplayConfigurationTaskTest::ResponseCallback,
                        base::Unretained(this)));
     task.Run();
@@ -449,6 +457,7 @@
     UpdateDisplayConfigurationTask task(
         &delegate_, &layout_manager_, MULTIPLE_DISPLAY_STATE_MULTI_EXTENDED,
         chromeos::DISPLAY_POWER_ALL_ON, 0, kRefreshRateThrottleEnabled, false,
+        kConfigurationTypeFull,
         base::BindOnce(&UpdateDisplayConfigurationTaskTest::ResponseCallback,
                        base::Unretained(this)));
     task.Run();
@@ -460,6 +469,7 @@
     UpdateDisplayConfigurationTask task(
         &delegate_, &layout_manager_, MULTIPLE_DISPLAY_STATE_MULTI_MIRROR,
         chromeos::DISPLAY_POWER_ALL_ON, 0, kRefreshRateThrottleEnabled, false,
+        kConfigurationTypeFull,
         base::BindOnce(&UpdateDisplayConfigurationTaskTest::ResponseCallback,
                        base::Unretained(this)));
     task.Run();
@@ -483,6 +493,7 @@
     UpdateDisplayConfigurationTask task(
         &delegate_, &layout_manager_, MULTIPLE_DISPLAY_STATE_MULTI_EXTENDED,
         chromeos::DISPLAY_POWER_ALL_ON, 0, kRefreshRateThrottleEnabled, false,
+        kConfigurationTypeFull,
         base::BindOnce(&UpdateDisplayConfigurationTaskTest::ResponseCallback,
                        base::Unretained(this)));
     task.Run();
@@ -494,7 +505,7 @@
     UpdateDisplayConfigurationTask task(
         &delegate_, &layout_manager_, MULTIPLE_DISPLAY_STATE_MULTI_MIRROR,
         chromeos::DISPLAY_POWER_ALL_ON, 0, kRefreshRateThrottleEnabled,
-        true /* force_configure */,
+        true /* force_configure */, kConfigurationTypeFull,
         base::BindOnce(&UpdateDisplayConfigurationTaskTest::ResponseCallback,
                        base::Unretained(this)));
     task.Run();
diff --git a/ui/display/types/display_constants.h b/ui/display/types/display_constants.h
index f13516c..abc5be90 100644
--- a/ui/display/types/display_constants.h
+++ b/ui/display/types/display_constants.h
@@ -123,11 +123,25 @@
   kRefreshRateThrottleDisabled,
 };
 
+// Whether a configuration should be seamless or full. Full configuration may
+// result in visible artifacts such as blanking to achieve the specified
+// configuration. Seamless configuration requests will fail if the system cannot
+// achieve it without visible artifacts.
+enum ConfigurationType {
+  kConfigurationTypeFull,
+  kConfigurationTypeSeamless,
+};
+
 // A flag to allow ui/display and ozone to adjust the behavior of display
 // configurations.
 enum ModesetFlag {
+  // At least one of kTestModeset and kCommitModeset must be set.
   kTestModeset = 1 << 0,
   kCommitModeset = 1 << 1,
+  // When |kSeamlessModeset| is set, the commit (or test) will succeed only if
+  // the submitted configuration can be completed without visual artifacts such
+  // as blanking.
+  kSeamlessModeset = 1 << 2,
 };
 
 // Defines the float values closest to repeating decimal scale factors.
diff --git a/ui/events/event_switches.cc b/ui/events/event_switches.cc
index a28b059..1dbd4511 100644
--- a/ui/events/event_switches.cc
+++ b/ui/events/event_switches.cc
@@ -40,6 +40,12 @@
 // Disable CancelAllTouches() function for the implementation on cancel single
 // touches.
 const char kDisableCancelAllTouches[] = "disable-cancel-all-touches";
+
+// Enables logic to detect microphone mute switch device state, which disables
+// internal audio input when toggled.
+constexpr char kEnableMicrophoneMuteSwitchDeviceSwitch[] =
+    "enable-microphone-mute-switch-device";
+
 #endif
 
 }  // namespace switches
diff --git a/ui/events/event_switches.h b/ui/events/event_switches.h
index 08a45c3..8347fd3 100644
--- a/ui/events/event_switches.h
+++ b/ui/events/event_switches.h
@@ -21,6 +21,9 @@
 #if defined(USE_OZONE)
 EVENTS_BASE_EXPORT extern const char kEdgeTouchFiltering[];
 EVENTS_BASE_EXPORT extern const char kDisableCancelAllTouches[];
+EVENTS_BASE_EXPORT
+extern const char kEnableMicrophoneMuteSwitchDeviceSwitch[];
+
 #endif
 
 }  // namespace switches
diff --git a/ui/events/ozone/evdev/BUILD.gn b/ui/events/ozone/evdev/BUILD.gn
index c5ef119..8b49af5 100644
--- a/ui/events/ozone/evdev/BUILD.gn
+++ b/ui/events/ozone/evdev/BUILD.gn
@@ -82,8 +82,6 @@
     "mouse_button_map_evdev.h",
     "stylus_button_event_converter_evdev.cc",
     "stylus_button_event_converter_evdev.h",
-    "switches.cc",
-    "switches.h",
     "tablet_event_converter_evdev.cc",
     "tablet_event_converter_evdev.h",
     "touch_evdev_debug_buffer.cc",
diff --git a/ui/events/ozone/evdev/input_device_factory_evdev.cc b/ui/events/ozone/evdev/input_device_factory_evdev.cc
index 59ceb06..764b405d 100644
--- a/ui/events/ozone/evdev/input_device_factory_evdev.cc
+++ b/ui/events/ozone/evdev/input_device_factory_evdev.cc
@@ -21,6 +21,7 @@
 #include "ui/events/devices/device_data_manager.h"
 #include "ui/events/devices/device_util_linux.h"
 #include "ui/events/devices/stylus_state.h"
+#include "ui/events/event_switches.h"
 #include "ui/events/ozone/evdev/device_event_dispatcher_evdev.h"
 #include "ui/events/ozone/evdev/event_converter_evdev_impl.h"
 #include "ui/events/ozone/evdev/event_device_info.h"
@@ -28,7 +29,6 @@
 #include "ui/events/ozone/evdev/keyboard_imposter_checker_evdev.h"
 #include "ui/events/ozone/evdev/microphone_mute_switch_event_converter_evdev.h"
 #include "ui/events/ozone/evdev/stylus_button_event_converter_evdev.h"
-#include "ui/events/ozone/evdev/switches.h"
 #include "ui/events/ozone/evdev/tablet_event_converter_evdev.h"
 #include "ui/events/ozone/evdev/touch_evdev_types.h"
 #include "ui/events/ozone/evdev/touch_event_converter_evdev.h"
diff --git a/ui/events/ozone/evdev/input_device_opener_evdev.cc b/ui/events/ozone/evdev/input_device_opener_evdev.cc
index e91a330..b1d477b 100644
--- a/ui/events/ozone/evdev/input_device_opener_evdev.cc
+++ b/ui/events/ozone/evdev/input_device_opener_evdev.cc
@@ -11,11 +11,11 @@
 #include "base/files/scoped_file.h"
 #include "base/memory/ptr_util.h"
 #include "base/trace_event/trace_event.h"
+#include "ui/events/event_switches.h"
 #include "ui/events/ozone/evdev/event_converter_evdev_impl.h"
 #include "ui/events/ozone/evdev/gamepad_event_converter_evdev.h"
 #include "ui/events/ozone/evdev/microphone_mute_switch_event_converter_evdev.h"
 #include "ui/events/ozone/evdev/stylus_button_event_converter_evdev.h"
-#include "ui/events/ozone/evdev/switches.h"
 #include "ui/events/ozone/evdev/tablet_event_converter_evdev.h"
 #include "ui/events/ozone/evdev/touch_event_converter_evdev.h"
 
@@ -89,7 +89,7 @@
   }
 
   if (base::CommandLine::ForCurrentProcess()->HasSwitch(
-          kEnableMicrophoneMuteSwitchDeviceSwitch) &&
+          switches::kEnableMicrophoneMuteSwitchDeviceSwitch) &&
       devinfo.IsMicrophoneMuteSwitchDevice()) {
     return base::WrapUnique<EventConverterEvdev>(
         new MicrophoneMuteSwitchEventConverterEvdev(
diff --git a/ui/events/ozone/evdev/switches.cc b/ui/events/ozone/evdev/switches.cc
deleted file mode 100644
index a83cb0c7..0000000
--- a/ui/events/ozone/evdev/switches.cc
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2021 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "ui/events/ozone/evdev/switches.h"
-
-namespace ui {
-
-// Enables logic to detect microphone mute switch device state, which disables
-// internal audio input when toggled.
-constexpr char kEnableMicrophoneMuteSwitchDeviceSwitch[] =
-    "enable-microphone-mute-switch-device";
-
-}  // namespace ui
diff --git a/ui/events/ozone/evdev/switches.h b/ui/events/ozone/evdev/switches.h
deleted file mode 100644
index 40965055..0000000
--- a/ui/events/ozone/evdev/switches.h
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2021 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef UI_EVENTS_OZONE_EVDEV_SWITCHES_H_
-#define UI_EVENTS_OZONE_EVDEV_SWITCHES_H_
-
-#include "base/component_export.h"
-
-namespace ui {
-
-COMPONENT_EXPORT(EVDEV)
-extern const char kEnableMicrophoneMuteSwitchDeviceSwitch[];
-
-}  // namespace ui
-
-#endif  // UI_EVENTS_OZONE_EVDEV_SWITCHES_H_
diff --git a/ui/gfx/BUILD.gn b/ui/gfx/BUILD.gn
index c492e236..9fd5b696 100644
--- a/ui/gfx/BUILD.gn
+++ b/ui/gfx/BUILD.gn
@@ -306,8 +306,6 @@
     sources += [
       "platform_font_skia.cc",
       "platform_font_skia.h",
-      "skia_font_delegate.cc",
-      "skia_font_delegate.h",
     ]
   }
 
@@ -382,6 +380,9 @@
   }
 
   # Linux.
+  if (is_linux) {
+    deps += [ "//ui/linux:linux_ui" ]
+  }
   if (is_linux || is_chromeos) {
     deps += [ "//third_party/fontconfig" ]
   }
@@ -690,11 +691,11 @@
     "text_elider_unittest.cc",
     "text_utils_unittest.cc",
   ]
+  if (is_linux) {
+    sources += [ "font_render_params_linux_unittest.cc" ]
+  }
   if (is_linux || is_chromeos) {
-    sources += [
-      "font_fallback_linux_unittest.cc",
-      "font_render_params_linux_unittest.cc",
-    ]
+    sources += [ "font_fallback_linux_unittest.cc" ]
   }
   if (is_mac) {
     sources += [
@@ -879,6 +880,10 @@
     ]
   }
 
+  if (is_linux) {
+    deps += [ "//ui/linux:test_support" ]
+  }
+
   if (is_linux || is_chromeos) {
     sources += [
       "linux/fontconfig_util_unittest.cc",
diff --git a/ui/gfx/DEPS b/ui/gfx/DEPS
index bff94453..73262fa7 100644
--- a/ui/gfx/DEPS
+++ b/ui/gfx/DEPS
@@ -8,6 +8,7 @@
   "+third_party/skia",
   "+third_party/test_fonts",
   "+ui/ios",
+  "+ui/linux",
   "+ui/ozone/buildflags.h",
 
   "-testing/gmock",
diff --git a/ui/gfx/font_render_params_linux.cc b/ui/gfx/font_render_params_linux.cc
index 42bb770b..816846787 100644
--- a/ui/gfx/font_render_params_linux.cc
+++ b/ui/gfx/font_render_params_linux.cc
@@ -23,9 +23,12 @@
 #include "ui/gfx/font.h"
 #include "ui/gfx/font_render_params_linux.h"
 #include "ui/gfx/linux/fontconfig_util.h"
-#include "ui/gfx/skia_font_delegate.h"
 #include "ui/gfx/switches.h"
 
+#if BUILDFLAG(IS_LINUX)
+#include "ui/linux/linux_ui.h"
+#endif
+
 namespace gfx {
 
 namespace {
@@ -209,9 +212,10 @@
 
   // Start with the delegate's settings, but let Fontconfig have the final say.
   FontRenderParams params;
-  const SkiaFontDelegate* delegate = SkiaFontDelegate::instance();
-  if (delegate)
-    params = delegate->GetDefaultFontRenderParams();
+#if BUILDFLAG(IS_LINUX)
+  if (const auto* linux_ui = ui::LinuxUi::instance())
+    params = linux_ui->GetDefaultFontRenderParams();
+#endif
   QueryFontconfig(actual_query, &params, family_out);
   if (!params.antialiasing) {
     // Cairo forces full hinting when antialiasing is disabled, since anything
diff --git a/ui/gfx/font_render_params_linux_unittest.cc b/ui/gfx/font_render_params_linux_unittest.cc
index 57314f8..50e80353 100644
--- a/ui/gfx/font_render_params_linux_unittest.cc
+++ b/ui/gfx/font_render_params_linux_unittest.cc
@@ -18,7 +18,7 @@
 #include "third_party/test_fonts/fontconfig/fontconfig_util_linux.h"
 #include "ui/gfx/font.h"
 #include "ui/gfx/linux/fontconfig_util.h"
-#include "ui/gfx/skia_font_delegate.h"
+#include "ui/linux/fake_linux_ui.h"
 
 namespace gfx {
 
@@ -36,16 +36,16 @@
 const char kFontconfigMatchPatternHeader[] = "  <match target=\"pattern\">\n";
 const char kFontconfigMatchFooter[] = "  </match>\n";
 
-// Implementation of SkiaFontDelegate that returns a canned FontRenderParams
+// Implementation of LinuxUi that returns a canned FontRenderParams
 // struct. This is used to isolate tests from the system's local configuration.
-class TestFontDelegate : public SkiaFontDelegate {
+class TestFontDelegate : public ui::FakeLinuxUi {
  public:
-  TestFontDelegate() {}
+  TestFontDelegate() = default;
 
   TestFontDelegate(const TestFontDelegate&) = delete;
   TestFontDelegate& operator=(const TestFontDelegate&) = delete;
 
-  ~TestFontDelegate() override {}
+  ~TestFontDelegate() override = default;
 
   void set_params(const FontRenderParams& params) { params_ = params; }
 
@@ -55,7 +55,7 @@
   void GetDefaultFontDescription(std::string* family_out,
                                  int* size_pixels_out,
                                  int* style_out,
-                                 Font::Weight* weight_out,
+                                 int* weight_out,
                                  FontRenderParams* params_out) const override {
     NOTIMPLEMENTED();
   }
@@ -111,8 +111,9 @@
 class FontRenderParamsTest : public testing::Test {
  public:
   FontRenderParamsTest() {
-    original_font_delegate_ = SkiaFontDelegate::instance();
-    SkiaFontDelegate::SetInstance(&test_font_delegate_);
+    auto test_font_delegate = std::make_unique<TestFontDelegate>();
+    test_font_delegate_ = test_font_delegate.get();
+    ui::LinuxUi::SetInstance(std::move(test_font_delegate));
     ClearFontRenderParamsCacheForTest();
 
     // Create a new fontconfig configuration and load the default fonts
@@ -137,13 +138,12 @@
     OverrideGlobalFontConfigForTesting(original_config_);
     FcConfigDestroy(override_config_);
 
-    SkiaFontDelegate::SetInstance(
-        const_cast<SkiaFontDelegate*>(original_font_delegate_.get()));
+    ui::LinuxUi::SetInstance(nullptr);
+    test_font_delegate_ = nullptr;
   }
 
  protected:
-  raw_ptr<const SkiaFontDelegate> original_font_delegate_;
-  TestFontDelegate test_font_delegate_;
+  raw_ptr<TestFontDelegate> test_font_delegate_;
 
   raw_ptr<FcConfig> override_config_ = nullptr;
   raw_ptr<FcConfig> original_config_ = nullptr;
@@ -152,42 +152,40 @@
 TEST_F(FontRenderParamsTest, Default) {
   ASSERT_TRUE(LoadConfigDataIntoFontconfig(
       std::string(kFontconfigFileHeader) +
-          // Specify the desired defaults via a font match rather than a pattern
-          // match (since this is the style generally used in
-          // /etc/fonts/conf.d).
-          kFontconfigMatchFontHeader +
-          CreateFontconfigEditStanza("antialias", "bool", "true") +
-          CreateFontconfigEditStanza("autohint", "bool", "true") +
-          CreateFontconfigEditStanza("hinting", "bool", "true") +
-          CreateFontconfigEditStanza("hintstyle", "const", "hintslight") +
-          CreateFontconfigEditStanza("rgba", "const", "rgb") +
-          kFontconfigMatchFooter +
-          // Add a font match for Arimo. Since it specifies a family, it
-          // shouldn't take effect when querying default settings.
-          kFontconfigMatchFontHeader +
-          CreateFontconfigTestStanza("family", "eq", "string", "Arimo") +
-          CreateFontconfigEditStanza("antialias", "bool", "true") +
-          CreateFontconfigEditStanza("autohint", "bool", "false") +
-          CreateFontconfigEditStanza("hinting", "bool", "true") +
-          CreateFontconfigEditStanza("hintstyle", "const", "hintfull") +
-          CreateFontconfigEditStanza("rgba", "const", "none") +
-          kFontconfigMatchFooter +
-          // Add font matches for fonts between 10 and 20 points or pixels.
-          // Since they specify sizes, they also should not affect the defaults.
-          kFontconfigMatchFontHeader +
-          CreateFontconfigTestStanza("size", "more_eq", "double", "10.0") +
-          CreateFontconfigTestStanza("size", "less_eq", "double", "20.0") +
-          CreateFontconfigEditStanza("antialias", "bool", "false") +
-          kFontconfigMatchFooter + kFontconfigMatchFontHeader +
-          CreateFontconfigTestStanza("pixel_size", "more_eq", "double",
-                                     "10.0") +
-          CreateFontconfigTestStanza("pixel_size", "less_eq", "double",
-                                     "20.0") +
-          CreateFontconfigEditStanza("antialias", "bool", "false") +
-          kFontconfigMatchFooter + kFontconfigFileFooter));
+      // Specify the desired defaults via a font match rather than a pattern
+      // match (since this is the style generally used in
+      // /etc/fonts/conf.d).
+      kFontconfigMatchFontHeader +
+      CreateFontconfigEditStanza("antialias", "bool", "true") +
+      CreateFontconfigEditStanza("autohint", "bool", "true") +
+      CreateFontconfigEditStanza("hinting", "bool", "true") +
+      CreateFontconfigEditStanza("hintstyle", "const", "hintslight") +
+      CreateFontconfigEditStanza("rgba", "const", "rgb") +
+      kFontconfigMatchFooter +
+      // Add a font match for Arimo. Since it specifies a family, it
+      // shouldn't take effect when querying default settings.
+      kFontconfigMatchFontHeader +
+      CreateFontconfigTestStanza("family", "eq", "string", "Arimo") +
+      CreateFontconfigEditStanza("antialias", "bool", "true") +
+      CreateFontconfigEditStanza("autohint", "bool", "false") +
+      CreateFontconfigEditStanza("hinting", "bool", "true") +
+      CreateFontconfigEditStanza("hintstyle", "const", "hintfull") +
+      CreateFontconfigEditStanza("rgba", "const", "none") +
+      kFontconfigMatchFooter +
+      // Add font matches for fonts between 10 and 20 points or pixels.
+      // Since they specify sizes, they also should not affect the defaults.
+      kFontconfigMatchFontHeader +
+      CreateFontconfigTestStanza("size", "more_eq", "double", "10.0") +
+      CreateFontconfigTestStanza("size", "less_eq", "double", "20.0") +
+      CreateFontconfigEditStanza("antialias", "bool", "false") +
+      kFontconfigMatchFooter + kFontconfigMatchFontHeader +
+      CreateFontconfigTestStanza("pixel_size", "more_eq", "double", "10.0") +
+      CreateFontconfigTestStanza("pixel_size", "less_eq", "double", "20.0") +
+      CreateFontconfigEditStanza("antialias", "bool", "false") +
+      kFontconfigMatchFooter + kFontconfigFileFooter));
 
-  FontRenderParams params = GetFontRenderParams(
-      FontRenderParamsQuery(), NULL);
+  FontRenderParams params =
+      GetFontRenderParams(FontRenderParamsQuery(), nullptr);
   EXPECT_TRUE(params.antialiasing);
   EXPECT_TRUE(params.autohinter);
   EXPECT_TRUE(params.use_bitmaps);
@@ -200,31 +198,31 @@
 TEST_F(FontRenderParamsTest, Size) {
   ASSERT_TRUE(LoadConfigDataIntoFontconfig(
       std::string(kFontconfigFileHeader) + kFontconfigMatchPatternHeader +
-          CreateFontconfigEditStanza("antialias", "bool", "true") +
-          CreateFontconfigEditStanza("hinting", "bool", "true") +
-          CreateFontconfigEditStanza("hintstyle", "const", "hintfull") +
-          CreateFontconfigEditStanza("rgba", "const", "none") +
-          kFontconfigMatchFooter + kFontconfigMatchPatternHeader +
-          CreateFontconfigTestStanza("pixelsize", "less_eq", "double", "10") +
-          CreateFontconfigEditStanza("antialias", "bool", "false") +
-          kFontconfigMatchFooter + kFontconfigMatchPatternHeader +
-          CreateFontconfigTestStanza("size", "more_eq", "double", "20") +
-          CreateFontconfigEditStanza("hintstyle", "const", "hintslight") +
-          CreateFontconfigEditStanza("rgba", "const", "rgb") +
-          kFontconfigMatchFooter + kFontconfigFileFooter));
+      CreateFontconfigEditStanza("antialias", "bool", "true") +
+      CreateFontconfigEditStanza("hinting", "bool", "true") +
+      CreateFontconfigEditStanza("hintstyle", "const", "hintfull") +
+      CreateFontconfigEditStanza("rgba", "const", "none") +
+      kFontconfigMatchFooter + kFontconfigMatchPatternHeader +
+      CreateFontconfigTestStanza("pixelsize", "less_eq", "double", "10") +
+      CreateFontconfigEditStanza("antialias", "bool", "false") +
+      kFontconfigMatchFooter + kFontconfigMatchPatternHeader +
+      CreateFontconfigTestStanza("size", "more_eq", "double", "20") +
+      CreateFontconfigEditStanza("hintstyle", "const", "hintslight") +
+      CreateFontconfigEditStanza("rgba", "const", "rgb") +
+      kFontconfigMatchFooter + kFontconfigFileFooter));
 
   // The defaults should be used when the supplied size isn't matched by the
   // second or third blocks.
   FontRenderParamsQuery query;
   query.pixel_size = 12;
-  FontRenderParams params = GetFontRenderParams(query, NULL);
+  FontRenderParams params = GetFontRenderParams(query, nullptr);
   EXPECT_TRUE(params.antialiasing);
   EXPECT_EQ(FontRenderParams::HINTING_FULL, params.hinting);
   EXPECT_EQ(FontRenderParams::SUBPIXEL_RENDERING_NONE,
             params.subpixel_rendering);
 
   query.pixel_size = 10;
-  params = GetFontRenderParams(query, NULL);
+  params = GetFontRenderParams(query, nullptr);
   EXPECT_FALSE(params.antialiasing);
   EXPECT_EQ(FontRenderParams::HINTING_FULL, params.hinting);
   EXPECT_EQ(FontRenderParams::SUBPIXEL_RENDERING_NONE,
@@ -232,7 +230,7 @@
 
   query.pixel_size = 0;
   query.point_size = 20;
-  params = GetFontRenderParams(query, NULL);
+  params = GetFontRenderParams(query, nullptr);
   EXPECT_TRUE(params.antialiasing);
   EXPECT_EQ(FontRenderParams::HINTING_SLIGHT, params.hinting);
   EXPECT_EQ(FontRenderParams::SUBPIXEL_RENDERING_RGB,
@@ -244,41 +242,41 @@
   // hinting for italic text.
   ASSERT_TRUE(LoadConfigDataIntoFontconfig(
       std::string(kFontconfigFileHeader) + kFontconfigMatchPatternHeader +
-          CreateFontconfigEditStanza("antialias", "bool", "true") +
-          CreateFontconfigEditStanza("hinting", "bool", "true") +
-          CreateFontconfigEditStanza("hintstyle", "const", "hintslight") +
-          CreateFontconfigEditStanza("rgba", "const", "rgb") +
-          kFontconfigMatchFooter + kFontconfigMatchPatternHeader +
-          CreateFontconfigTestStanza("weight", "eq", "const", "bold") +
-          CreateFontconfigEditStanza("rgba", "const", "none") +
-          kFontconfigMatchFooter + kFontconfigMatchPatternHeader +
-          CreateFontconfigTestStanza("slant", "eq", "const", "italic") +
-          CreateFontconfigEditStanza("hinting", "bool", "false") +
-          kFontconfigMatchFooter + kFontconfigFileFooter));
+      CreateFontconfigEditStanza("antialias", "bool", "true") +
+      CreateFontconfigEditStanza("hinting", "bool", "true") +
+      CreateFontconfigEditStanza("hintstyle", "const", "hintslight") +
+      CreateFontconfigEditStanza("rgba", "const", "rgb") +
+      kFontconfigMatchFooter + kFontconfigMatchPatternHeader +
+      CreateFontconfigTestStanza("weight", "eq", "const", "bold") +
+      CreateFontconfigEditStanza("rgba", "const", "none") +
+      kFontconfigMatchFooter + kFontconfigMatchPatternHeader +
+      CreateFontconfigTestStanza("slant", "eq", "const", "italic") +
+      CreateFontconfigEditStanza("hinting", "bool", "false") +
+      kFontconfigMatchFooter + kFontconfigFileFooter));
 
   FontRenderParamsQuery query;
   query.style = Font::NORMAL;
-  FontRenderParams params = GetFontRenderParams(query, NULL);
+  FontRenderParams params = GetFontRenderParams(query, nullptr);
   EXPECT_EQ(FontRenderParams::HINTING_SLIGHT, params.hinting);
   EXPECT_EQ(FontRenderParams::SUBPIXEL_RENDERING_RGB,
             params.subpixel_rendering);
 
   query.weight = Font::Weight::BOLD;
-  params = GetFontRenderParams(query, NULL);
+  params = GetFontRenderParams(query, nullptr);
   EXPECT_EQ(FontRenderParams::HINTING_SLIGHT, params.hinting);
   EXPECT_EQ(FontRenderParams::SUBPIXEL_RENDERING_NONE,
             params.subpixel_rendering);
 
   query.weight = Font::Weight::NORMAL;
   query.style = Font::ITALIC;
-  params = GetFontRenderParams(query, NULL);
+  params = GetFontRenderParams(query, nullptr);
   EXPECT_EQ(FontRenderParams::HINTING_NONE, params.hinting);
   EXPECT_EQ(FontRenderParams::SUBPIXEL_RENDERING_RGB,
             params.subpixel_rendering);
 
   query.weight = Font::Weight::BOLD;
   query.style = Font::ITALIC;
-  params = GetFontRenderParams(query, NULL);
+  params = GetFontRenderParams(query, nullptr);
   EXPECT_EQ(FontRenderParams::HINTING_NONE, params.hinting);
   EXPECT_EQ(FontRenderParams::SUBPIXEL_RENDERING_NONE,
             params.subpixel_rendering);
@@ -288,15 +286,15 @@
   // Load a config that only enables antialiasing for scalable fonts.
   ASSERT_TRUE(LoadConfigDataIntoFontconfig(
       std::string(kFontconfigFileHeader) + kFontconfigMatchPatternHeader +
-          CreateFontconfigEditStanza("antialias", "bool", "false") +
-          kFontconfigMatchFooter + kFontconfigMatchPatternHeader +
-          CreateFontconfigTestStanza("scalable", "eq", "bool", "true") +
-          CreateFontconfigEditStanza("antialias", "bool", "true") +
-          kFontconfigMatchFooter + kFontconfigFileFooter));
+      CreateFontconfigEditStanza("antialias", "bool", "false") +
+      kFontconfigMatchFooter + kFontconfigMatchPatternHeader +
+      CreateFontconfigTestStanza("scalable", "eq", "bool", "true") +
+      CreateFontconfigEditStanza("antialias", "bool", "true") +
+      kFontconfigMatchFooter + kFontconfigFileFooter));
 
   // Check that we specifically ask how scalable fonts should be rendered.
-  FontRenderParams params = GetFontRenderParams(
-      FontRenderParamsQuery(), NULL);
+  FontRenderParams params =
+      GetFontRenderParams(FontRenderParamsQuery(), nullptr);
   EXPECT_TRUE(params.antialiasing);
 }
 
@@ -304,18 +302,18 @@
   // Load a config that enables embedded bitmaps for fonts <= 10 pixels.
   ASSERT_TRUE(LoadConfigDataIntoFontconfig(
       std::string(kFontconfigFileHeader) + kFontconfigMatchPatternHeader +
-          CreateFontconfigEditStanza("embeddedbitmap", "bool", "false") +
-          kFontconfigMatchFooter + kFontconfigMatchPatternHeader +
-          CreateFontconfigTestStanza("pixelsize", "less_eq", "double", "10") +
-          CreateFontconfigEditStanza("embeddedbitmap", "bool", "true") +
-          kFontconfigMatchFooter + kFontconfigFileFooter));
+      CreateFontconfigEditStanza("embeddedbitmap", "bool", "false") +
+      kFontconfigMatchFooter + kFontconfigMatchPatternHeader +
+      CreateFontconfigTestStanza("pixelsize", "less_eq", "double", "10") +
+      CreateFontconfigEditStanza("embeddedbitmap", "bool", "true") +
+      kFontconfigMatchFooter + kFontconfigFileFooter));
 
   FontRenderParamsQuery query;
-  FontRenderParams params = GetFontRenderParams(query, NULL);
+  FontRenderParams params = GetFontRenderParams(query, nullptr);
   EXPECT_FALSE(params.use_bitmaps);
 
   query.pixel_size = 5;
-  params = GetFontRenderParams(query, NULL);
+  params = GetFontRenderParams(query, nullptr);
   EXPECT_TRUE(params.use_bitmaps);
 }
 
@@ -324,16 +322,16 @@
   // subpixel rendering.
   ASSERT_TRUE(LoadConfigDataIntoFontconfig(
       std::string(kFontconfigFileHeader) + kFontconfigMatchPatternHeader +
-          CreateFontconfigEditStanza("antialias", "bool", "false") +
-          CreateFontconfigEditStanza("hinting", "bool", "false") +
-          CreateFontconfigEditStanza("hintstyle", "const", "hintnone") +
-          CreateFontconfigEditStanza("rgba", "const", "rgb") +
-          kFontconfigMatchFooter + kFontconfigFileFooter));
+      CreateFontconfigEditStanza("antialias", "bool", "false") +
+      CreateFontconfigEditStanza("hinting", "bool", "false") +
+      CreateFontconfigEditStanza("hintstyle", "const", "hintnone") +
+      CreateFontconfigEditStanza("rgba", "const", "rgb") +
+      kFontconfigMatchFooter + kFontconfigFileFooter));
 
   // Full hinting should be forced. See the comment in GetFontRenderParams() for
   // more information.
-  FontRenderParams params = GetFontRenderParams(
-      FontRenderParamsQuery(), NULL);
+  FontRenderParams params =
+      GetFontRenderParams(FontRenderParamsQuery(), nullptr);
   EXPECT_FALSE(params.antialiasing);
   EXPECT_EQ(FontRenderParams::HINTING_FULL, params.hinting);
   EXPECT_EQ(FontRenderParams::SUBPIXEL_RENDERING_NONE,
@@ -344,7 +342,7 @@
 TEST_F(FontRenderParamsTest, ForceSubpixelPositioning) {
   {
     FontRenderParams params =
-        GetFontRenderParams(FontRenderParamsQuery(), NULL);
+        GetFontRenderParams(FontRenderParamsQuery(), nullptr);
     EXPECT_TRUE(params.antialiasing);
     EXPECT_FALSE(params.subpixel_positioning);
     SetFontRenderParamsDeviceScaleFactor(1.0f);
@@ -354,7 +352,7 @@
   // Subpixel positioning should be forced.
   {
     FontRenderParams params =
-        GetFontRenderParams(FontRenderParamsQuery(), NULL);
+        GetFontRenderParams(FontRenderParamsQuery(), nullptr);
     EXPECT_TRUE(params.antialiasing);
     EXPECT_TRUE(params.subpixel_positioning);
     SetFontRenderParamsDeviceScaleFactor(1.0f);
@@ -377,22 +375,21 @@
 }
 
 TEST_F(FontRenderParamsTest, OnlySetConfiguredValues) {
-  // Configure the SkiaFontDelegate (which queries GtkSettings on desktop
-  // Linux) to request subpixel rendering.
+  // Configure the LinuxUi to request subpixel rendering.
   FontRenderParams system_params;
   system_params.subpixel_rendering = FontRenderParams::SUBPIXEL_RENDERING_RGB;
-  test_font_delegate_.set_params(system_params);
+  test_font_delegate_->set_params(system_params);
 
   // Load a Fontconfig config that enables antialiasing but doesn't say anything
   // about subpixel rendering.
   ASSERT_TRUE(LoadConfigDataIntoFontconfig(
       std::string(kFontconfigFileHeader) + kFontconfigMatchPatternHeader +
-          CreateFontconfigEditStanza("antialias", "bool", "true") +
-          kFontconfigMatchFooter + kFontconfigFileFooter));
+      CreateFontconfigEditStanza("antialias", "bool", "true") +
+      kFontconfigMatchFooter + kFontconfigFileFooter));
 
   // The subpixel rendering setting from the delegate should make it through.
-  FontRenderParams params = GetFontRenderParams(
-      FontRenderParamsQuery(), NULL);
+  FontRenderParams params =
+      GetFontRenderParams(FontRenderParamsQuery(), nullptr);
   EXPECT_EQ(system_params.subpixel_rendering, params.subpixel_rendering);
 }
 
@@ -405,7 +402,7 @@
   system_params.antialiasing = true;
   system_params.hinting = FontRenderParams::HINTING_MEDIUM;
   system_params.subpixel_rendering = FontRenderParams::SUBPIXEL_RENDERING_RGB;
-  test_font_delegate_.set_params(system_params);
+  test_font_delegate_->set_params(system_params);
 
   FontRenderParamsQuery query;
   query.families.push_back("Arimo");
@@ -440,11 +437,11 @@
   // Configure Fontconfig to use Tinos for both Helvetica and Arimo.
   ASSERT_TRUE(LoadConfigDataIntoFontconfig(
       std::string(kFontconfigFileHeader) +
-          CreateFontconfigAliasStanza("Helvetica", "Tinos") +
-          kFontconfigMatchPatternHeader +
-          CreateFontconfigTestStanza("family", "eq", "string", "Arimo") +
-          CreateFontconfigEditStanza("family", "string", "Tinos") +
-          kFontconfigMatchFooter + kFontconfigFileFooter));
+      CreateFontconfigAliasStanza("Helvetica", "Tinos") +
+      kFontconfigMatchPatternHeader +
+      CreateFontconfigTestStanza("family", "eq", "string", "Arimo") +
+      CreateFontconfigEditStanza("family", "string", "Tinos") +
+      kFontconfigMatchFooter + kFontconfigFileFooter));
 
   FontRenderParamsQuery query;
   query.families.push_back("Helvetica");
diff --git a/ui/gfx/platform_font_skia.cc b/ui/gfx/platform_font_skia.cc
index 4a829602b..cefc3fe 100644
--- a/ui/gfx/platform_font_skia.cc
+++ b/ui/gfx/platform_font_skia.cc
@@ -22,13 +22,16 @@
 #include "ui/gfx/font.h"
 #include "ui/gfx/font_list.h"
 #include "ui/gfx/font_render_params.h"
-#include "ui/gfx/skia_font_delegate.h"
 #include "ui/gfx/text_utils.h"
 
 #if BUILDFLAG(IS_WIN)
 #include "ui/gfx/system_fonts_win.h"
 #endif
 
+#if BUILDFLAG(IS_LINUX)
+#include "ui/linux/linux_ui.h"
+#endif
+
 namespace gfx {
 namespace {
 
@@ -163,13 +166,17 @@
   weight = system_font.GetWeight();
 #endif  // BUILDFLAG(IS_WIN)
 
-  // On Linux, SkiaFontDelegate is used to query the native toolkit (e.g.
-  // GTK+) for the default UI font.
-  const SkiaFontDelegate* delegate = SkiaFontDelegate::instance();
-  if (delegate) {
-    delegate->GetDefaultFontDescription(&family, &size_pixels, &style, &weight,
-                                        &params);
-  } else if (default_font_description_) {
+#if BUILDFLAG(IS_LINUX)
+  // On Linux, LinuxUi is used to query the native toolkit (e.g.
+  // GTK) for the default UI font.
+  if (const auto* linux_ui = ui::LinuxUi::instance()) {
+    int weight_int;
+    linux_ui->GetDefaultFontDescription(
+        &family, &size_pixels, &style, static_cast<int*>(&weight_int), &params);
+    weight = static_cast<Font::Weight>(weight_int);
+  } else
+#endif
+      if (default_font_description_) {
 #if BUILDFLAG(IS_CHROMEOS)
     // On ChromeOS, a FontList font description string is stored as a
     // translatable resource and passed in via SetDefaultFontDescription().
diff --git a/ui/gfx/platform_font_skia_unittest.cc b/ui/gfx/platform_font_skia_unittest.cc
index d2ad3c7..6060900a 100644
--- a/ui/gfx/platform_font_skia_unittest.cc
+++ b/ui/gfx/platform_font_skia_unittest.cc
@@ -15,17 +15,20 @@
 #include "ui/gfx/font.h"
 #include "ui/gfx/font_names_testing.h"
 #include "ui/gfx/font_render_params.h"
-#include "ui/gfx/skia_font_delegate.h"
 
 #if BUILDFLAG(IS_WIN)
 #include "ui/gfx/system_fonts_win.h"
 #endif
 
+#if BUILDFLAG(IS_LINUX)
+#include "ui/linux/fake_linux_ui.h"
+#endif
+
 namespace gfx {
 
-// Implementation of SkiaFontDelegate used to control the default font
-// description.
-class TestFontDelegate : public SkiaFontDelegate {
+#if BUILDFLAG(IS_LINUX)
+// Implementation of LinuxUi used to control the default font description.
+class TestFontDelegate : public ui::FakeLinuxUi {
  public:
   TestFontDelegate() = default;
 
@@ -48,12 +51,12 @@
   void GetDefaultFontDescription(std::string* family_out,
                                  int* size_pixels_out,
                                  int* style_out,
-                                 Font::Weight* weight_out,
+                                 int* weight_out,
                                  FontRenderParams* params_out) const override {
     *family_out = family_;
     *size_pixels_out = size_pixels_;
     *style_out = style_;
-    *weight_out = weight_;
+    *weight_out = static_cast<int>(weight_);
     *params_out = params_;
   }
 
@@ -76,36 +79,33 @@
   ~PlatformFontSkiaTest() override = default;
 
   void SetUp() override {
-    original_font_delegate_ = SkiaFontDelegate::instance();
-    SkiaFontDelegate::SetInstance(&test_font_delegate_);
+    DCHECK_EQ(ui::LinuxUi::instance(), nullptr);
+    auto test_font_delegate = std::make_unique<TestFontDelegate>();
+    test_font_delegate_ = test_font_delegate.get();
+    ui::LinuxUi::SetInstance(std::move(test_font_delegate));
     PlatformFontSkia::ReloadDefaultFont();
   }
 
   void TearDown() override {
-    DCHECK_EQ(&test_font_delegate_, SkiaFontDelegate::instance());
-    SkiaFontDelegate::SetInstance(
-        const_cast<SkiaFontDelegate*>(original_font_delegate_.get()));
+    DCHECK_EQ(test_font_delegate_, ui::LinuxUi::instance());
+    ui::LinuxUi::SetInstance(nullptr);
     PlatformFontSkia::ReloadDefaultFont();
   }
 
  protected:
-  TestFontDelegate test_font_delegate_;
-
- private:
-  // Originally-registered delegate.
-  raw_ptr<const SkiaFontDelegate> original_font_delegate_;
+  TestFontDelegate* test_font_delegate_ = nullptr;
 };
 
 // Test that PlatformFontSkia's default constructor initializes the instance
 // with the correct parameters.
 TEST_F(PlatformFontSkiaTest, DefaultFont) {
-  test_font_delegate_.set_family(kTestFontName);
-  test_font_delegate_.set_size_pixels(13);
-  test_font_delegate_.set_style(Font::NORMAL);
+  test_font_delegate_->set_family(kTestFontName);
+  test_font_delegate_->set_size_pixels(13);
+  test_font_delegate_->set_style(Font::NORMAL);
   FontRenderParams params;
   params.antialiasing = false;
   params.hinting = FontRenderParams::HINTING_FULL;
-  test_font_delegate_.set_params(params);
+  test_font_delegate_->set_params(params);
   scoped_refptr<gfx::PlatformFontSkia> font(new gfx::PlatformFontSkia());
   EXPECT_EQ(kTestFontName, font->GetFontName());
   EXPECT_EQ(13, font->GetFontSize());
@@ -115,10 +115,10 @@
   EXPECT_EQ(params.hinting, font->GetFontRenderParams().hinting);
 
   // Drop the old default font and check that new settings are loaded.
-  test_font_delegate_.set_family(kSymbolFontName);
-  test_font_delegate_.set_size_pixels(15);
-  test_font_delegate_.set_style(gfx::Font::ITALIC);
-  test_font_delegate_.set_weight(gfx::Font::Weight::BOLD);
+  test_font_delegate_->set_family(kSymbolFontName);
+  test_font_delegate_->set_size_pixels(15);
+  test_font_delegate_->set_style(gfx::Font::ITALIC);
+  test_font_delegate_->set_weight(gfx::Font::Weight::BOLD);
   PlatformFontSkia::ReloadDefaultFont();
   scoped_refptr<gfx::PlatformFontSkia> font2(new gfx::PlatformFontSkia());
   EXPECT_EQ(kSymbolFontName, font2->GetFontName());
@@ -126,6 +126,7 @@
   EXPECT_NE(font2->GetStyle() & Font::ITALIC, 0);
   EXPECT_EQ(gfx::Font::Weight::BOLD, font2->GetWeight());
 }
+#endif  // BUILDFLAG(IS_LINUX)
 
 TEST(PlatformFontSkiaRenderParamsTest, DefaultFontRenderParams) {
   scoped_refptr<PlatformFontSkia> default_font(new PlatformFontSkia());
diff --git a/ui/gfx/skia_font_delegate.cc b/ui/gfx/skia_font_delegate.cc
deleted file mode 100644
index f7c24c5f..0000000
--- a/ui/gfx/skia_font_delegate.cc
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright 2013 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "ui/gfx/skia_font_delegate.h"
-
-namespace {
-
-gfx::SkiaFontDelegate* g_skia_font_delegate = 0;
-
-}  // namespace
-
-namespace gfx {
-
-void SkiaFontDelegate::SetInstance(SkiaFontDelegate* instance) {
-  g_skia_font_delegate = instance;
-}
-
-const SkiaFontDelegate* SkiaFontDelegate::instance() {
-  return g_skia_font_delegate;
-}
-
-}  // namespace gfx
diff --git a/ui/gfx/skia_font_delegate.h b/ui/gfx/skia_font_delegate.h
deleted file mode 100644
index 05636e9..0000000
--- a/ui/gfx/skia_font_delegate.h
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2013 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef UI_GFX_SKIA_FONT_DELEGATE_H_
-#define UI_GFX_SKIA_FONT_DELEGATE_H_
-
-#include <memory>
-#include <string>
-
-#include "ui/gfx/font_render_params.h"
-#include "ui/gfx/gfx_export.h"
-
-namespace gfx {
-
-// Allows a Linux platform-specific overriding of font preferences.
-class GFX_EXPORT SkiaFontDelegate {
- public:
-  virtual ~SkiaFontDelegate() {}
-
-  // Sets the dynamically loaded singleton that provides font preferences.
-  // This pointer is not owned, and if this method is called a second time,
-  // the first instance is not deleted.
-  static void SetInstance(SkiaFontDelegate* instance);
-
-  // Returns a SkiaFontDelegate instance for the toolkit used in
-  // the user's desktop environment.
-  //
-  // Can return NULL, in case no toolkit has been set. (For example, if we're
-  // running with the "--ash" flag.)
-  static const SkiaFontDelegate* instance();
-
-  // Returns the default font rendering settings.
-  virtual FontRenderParams GetDefaultFontRenderParams() const = 0;
-
-  // Returns details about the default UI font. |style_out| holds a bitfield of
-  // gfx::Font::Style values.
-  virtual void GetDefaultFontDescription(
-      std::string* family_out,
-      int* size_pixels_out,
-      int* style_out,
-      Font::Weight* weight_out,
-      FontRenderParams* params_out) const = 0;
-};
-
-}  // namespace gfx
-
-#endif  // UI_GFX_SKIA_FONT_DELEGATE_H_
diff --git a/ui/gtk/gtk_ui.cc b/ui/gtk/gtk_ui.cc
index b410534..1fbb5815 100644
--- a/ui/gtk/gtk_ui.cc
+++ b/ui/gtk/gtk_ui.cc
@@ -458,12 +458,12 @@
 void GtkUi::GetDefaultFontDescription(std::string* family_out,
                                       int* size_pixels_out,
                                       int* style_out,
-                                      gfx::Font::Weight* weight_out,
+                                      int* weight_out,
                                       gfx::FontRenderParams* params_out) const {
   *family_out = default_font_family_;
   *size_pixels_out = default_font_size_pixels_;
   *style_out = default_font_style_;
-  *weight_out = default_font_weight_;
+  *weight_out = static_cast<int>(default_font_weight_);
   *params_out = default_font_render_params_;
 }
 
diff --git a/ui/gtk/gtk_ui.h b/ui/gtk/gtk_ui.h
index 1137f17..e73bbdf 100644
--- a/ui/gtk/gtk_ui.h
+++ b/ui/gtk/gtk_ui.h
@@ -14,6 +14,7 @@
 #include "printing/buildflags/buildflags.h"
 #include "ui/base/glib/glib_signal.h"
 #include "ui/gfx/color_utils.h"
+#include "ui/gfx/font_render_params.h"
 #include "ui/gtk/gtk_ui_platform.h"
 #include "ui/linux/linux_ui_base.h"
 #include "ui/linux/window_frame_provider.h"
@@ -66,7 +67,7 @@
       std::string* family_out,
       int* size_pixels_out,
       int* style_out,
-      gfx::Font::Weight* weight_out,
+      int* weight_out,
       gfx::FontRenderParams* params_out) const override;
 
   // ui::ShellDialogLinux:
diff --git a/ui/linux/BUILD.gn b/ui/linux/BUILD.gn
index fa3e8d33..48b78fe 100644
--- a/ui/linux/BUILD.gn
+++ b/ui/linux/BUILD.gn
@@ -29,7 +29,6 @@
   deps = [
     "//base",
     "//build:chromecast_buildflags",
-    "//ui/gfx",
     "//ui/gfx/animation",
   ]
   public_deps = [
@@ -46,6 +45,7 @@
     "//base",
     "//ui/gfx",
     "//ui/native_theme",
+    "//ui/shell_dialogs",
   ]
 }
 
diff --git a/ui/linux/fake_linux_ui.cc b/ui/linux/fake_linux_ui.cc
index 5dfb564..211b9093 100644
--- a/ui/linux/fake_linux_ui.cc
+++ b/ui/linux/fake_linux_ui.cc
@@ -6,6 +6,7 @@
 
 #include "base/time/time.h"
 #include "ui/gfx/color_palette.h"
+#include "ui/gfx/font_render_params.h"
 #include "ui/gfx/geometry/size.h"
 #include "ui/gfx/image/image.h"
 #include "ui/shell_dialogs/select_file_policy.h"
@@ -30,7 +31,7 @@
     std::string* family_out,
     int* size_pixels_out,
     int* style_out,
-    gfx::Font::Weight* weight_out,
+    int* weight_out,
     gfx::FontRenderParams* params_out) const {}
 
 ui::SelectFileDialog* FakeLinuxUi::CreateSelectFileDialog(
diff --git a/ui/linux/fake_linux_ui.h b/ui/linux/fake_linux_ui.h
index 7e3228c..17ab87be 100644
--- a/ui/linux/fake_linux_ui.h
+++ b/ui/linux/fake_linux_ui.h
@@ -24,7 +24,7 @@
       std::string* family_out,
       int* size_pixels_out,
       int* style_out,
-      gfx::Font::Weight* weight_out,
+      int* weight_out,
       gfx::FontRenderParams* params_out) const override;
   ui::SelectFileDialog* CreateSelectFileDialog(
       void* listener,
diff --git a/ui/linux/linux_ui.cc b/ui/linux/linux_ui.cc
index 5e9ec12..bae51ee 100644
--- a/ui/linux/linux_ui.cc
+++ b/ui/linux/linux_ui.cc
@@ -28,7 +28,6 @@
 // static
 std::unique_ptr<LinuxUi> LinuxUi::SetInstance(
     std::unique_ptr<LinuxUi> instance) {
-  SkiaFontDelegate::SetInstance(instance.get());
   gfx::AnimationSettingsProviderLinux::SetInstance(instance.get());
 
   return std::exchange(GetLinuxUiInstance(), std::move(instance));
diff --git a/ui/linux/linux_ui.h b/ui/linux/linux_ui.h
index 41b2bb0..e8be3f2e 100644
--- a/ui/linux/linux_ui.h
+++ b/ui/linux/linux_ui.h
@@ -19,8 +19,6 @@
 #include "printing/buildflags/buildflags.h"
 #include "third_party/skia/include/core/SkColor.h"
 #include "ui/gfx/animation/animation_settings_provider_linux.h"
-#include "ui/gfx/geometry/size.h"
-#include "ui/gfx/skia_font_delegate.h"
 
 // The main entrypoint into Linux toolkit specific code. GTK/QT code should only
 // be executed behind this interface.
@@ -34,6 +32,7 @@
 }
 
 namespace gfx {
+struct FontRenderParams;
 class Image;
 class Size;
 }  // namespace gfx
@@ -61,8 +60,7 @@
 // Adapter class with targets to render like different toolkits. Set by any
 // project that wants to do linux desktop native rendering.
 class COMPONENT_EXPORT(LINUX_UI) LinuxUi
-    : public gfx::SkiaFontDelegate,
-      public gfx::AnimationSettingsProviderLinux {
+    : public gfx::AnimationSettingsProviderLinux {
  public:
   using UseSystemThemeCallback =
       base::RepeatingCallback<bool(aura::Window* window)>;
@@ -218,6 +216,18 @@
       const ui::Event& event,
       std::vector<TextEditCommandAuraLinux>* commands) = 0;
 
+  // Returns the default font rendering settings.
+  virtual gfx::FontRenderParams GetDefaultFontRenderParams() const = 0;
+
+  // Returns details about the default UI font. |style_out| holds a bitfield of
+  // gfx::Font::Style values.
+  virtual void GetDefaultFontDescription(
+      std::string* family_out,
+      int* size_pixels_out,
+      int* style_out,
+      int* weight_out,
+      gfx::FontRenderParams* params_out) const = 0;
+
  protected:
   struct CmdLineArgs {
     CmdLineArgs();
diff --git a/ui/ozone/platform/drm/gpu/mock_drm_device.cc b/ui/ozone/platform/drm/gpu/mock_drm_device.cc
index c23fe32..dc1a784e 100644
--- a/ui/ozone/platform/drm/gpu/mock_drm_device.cc
+++ b/ui/ozone/platform/drm/gpu/mock_drm_device.cc
@@ -37,7 +37,6 @@
 };
 
 namespace ui {
-
 namespace {
 
 constexpr uint32_t kTestModesetFlags =
@@ -45,6 +44,11 @@
 
 constexpr uint32_t kCommitModesetFlags = DRM_MODE_ATOMIC_ALLOW_MODESET;
 
+// Seamless modeset is defined by the lack of DRM_MODE_ATOMIC_ALLOW_MODESET.
+// This also happens to be the same set of flags as would be used for a
+// pageflip, or other atomic property changes that do not require modesetting.
+constexpr uint32_t kSeamlessModesetFlags = 0;
+
 template <class Object>
 Object* DrmAllocator() {
   return static_cast<Object*>(drmMalloc(sizeof(Object)));
@@ -478,12 +482,20 @@
     uint32_t crtc_count,
     scoped_refptr<PageFlipRequest> page_flip_request) {
   commit_count_++;
-  if (flags == kTestModesetFlags)
-    ++test_modeset_count_;
-  else if (flags == kCommitModesetFlags)
-    ++commit_modeset_count_;
+  const bool test_only = flags & DRM_MODE_ATOMIC_TEST_ONLY;
+  switch (flags) {
+    case kTestModesetFlags:
+      ++test_modeset_count_;
+      break;
+    case kCommitModesetFlags:
+      ++commit_modeset_count_;
+      break;
+    case kSeamlessModesetFlags:
+      ++seamless_modeset_count_;
+      break;
+  }
 
-  if ((flags & kCommitModesetFlags && !set_crtc_expectation_) ||
+  if ((!test_only && !set_crtc_expectation_) ||
       (flags & DRM_MODE_ATOMIC_NONBLOCK && !commit_expectation_)) {
     return false;
   }
@@ -517,7 +529,7 @@
   if (page_flip_request)
     callbacks_.push(page_flip_request->AddPageFlip());
 
-  if (flags & DRM_MODE_ATOMIC_TEST_ONLY)
+  if (test_only)
     return true;
 
   // Only update values if not testing.
diff --git a/ui/ozone/platform/drm/gpu/mock_drm_device.h b/ui/ozone/platform/drm/gpu/mock_drm_device.h
index 05269d403..2c5456c 100644
--- a/ui/ozone/platform/drm/gpu/mock_drm_device.h
+++ b/ui/ozone/platform/drm/gpu/mock_drm_device.h
@@ -77,6 +77,7 @@
   int get_overlay_clear_call_count() const { return overlay_clear_call_count_; }
   int get_test_modeset_count() const { return test_modeset_count_; }
   int get_commit_modeset_count() const { return commit_modeset_count_; }
+  int get_seamless_modeset_count() const { return seamless_modeset_count_; }
   int get_commit_count() const { return commit_count_; }
   int get_set_object_property_count() const {
     return set_object_property_count_;
@@ -232,6 +233,7 @@
   int allocate_buffer_count_;
   int test_modeset_count_ = 0;
   int commit_modeset_count_ = 0;
+  int seamless_modeset_count_ = 0;
   int commit_count_ = 0;
   int set_object_property_count_ = 0;
   int set_gamma_ramp_count_ = 0;
diff --git a/ui/ozone/platform/drm/gpu/screen_manager.cc b/ui/ozone/platform/drm/gpu/screen_manager.cc
index 2b3cd7d..f6c4d2cd 100644
--- a/ui/ozone/platform/drm/gpu/screen_manager.cc
+++ b/ui/ozone/platform/drm/gpu/screen_manager.cc
@@ -323,6 +323,9 @@
                      ParamsToTracedValue(controllers_params, modeset_flag),
                      "before", base::trace_event::ToTracedValue(this));
 
+  // At least one of these flags must be set.
+  DCHECK(modeset_flag & (display::kCommitModeset | display::kTestModeset));
+
   // Split them to different lists unique to each DRM Device.
   base::flat_map<scoped_refptr<DrmDevice>, ControllerConfigsList>
       displays_for_drm_devices;
@@ -337,6 +340,7 @@
   }
 
   const bool commit_modeset = modeset_flag & display::kCommitModeset;
+  const bool is_seamless_modeset = modeset_flag & display::kSeamlessModeset;
   bool config_success = true;
   // Perform display configurations together for the same DRM only.
   for (const auto& configs_on_drm : displays_for_drm_devices) {
@@ -346,8 +350,9 @@
 
     if (modeset_flag & display::kTestModeset) {
       bool test_modeset =
-          TestAndSetPreferredModifiers(drm_controllers_params) ||
-          TestAndSetLinearModifier(drm_controllers_params);
+          TestAndSetPreferredModifiers(drm_controllers_params,
+                                       is_seamless_modeset) ||
+          TestAndSetLinearModifier(drm_controllers_params, is_seamless_modeset);
       config_success &= test_modeset;
       VLOG(1) << "Test-modeset " << (test_modeset ? "succeeded." : "failed.");
       if (!test_modeset)
@@ -356,9 +361,10 @@
 
     if (commit_modeset) {
       bool can_modeset_with_overlays =
-          TestModesetWithOverlays(drm_controllers_params);
+          TestModesetWithOverlays(drm_controllers_params, is_seamless_modeset);
       bool modeset_commit_result =
-          Modeset(drm_controllers_params, can_modeset_with_overlays);
+          Modeset(drm_controllers_params, can_modeset_with_overlays,
+                  is_seamless_modeset);
       config_success &= modeset_commit_result;
       if (modeset_commit_result) {
         VLOG(1) << "Modeset succeeded.";
@@ -378,7 +384,8 @@
 }
 
 bool ScreenManager::TestAndSetPreferredModifiers(
-    const ControllerConfigsList& controllers_params) {
+    const ControllerConfigsList& controllers_params,
+    bool is_seamless_modeset) {
   TRACE_EVENT1("drm", "ScreenManager::TestAndSetPreferredModifiers",
                "display_count", controllers_params.size());
 
@@ -416,9 +423,10 @@
     }
   }
 
-  if (!drm->plane_manager()->Commit(
-          std::move(commit_request),
-          DRM_MODE_ATOMIC_TEST_ONLY | DRM_MODE_ATOMIC_ALLOW_MODESET)) {
+  uint32_t flags = DRM_MODE_ATOMIC_TEST_ONLY;
+  if (!is_seamless_modeset)
+    flags |= DRM_MODE_ATOMIC_ALLOW_MODESET;
+  if (!drm->plane_manager()->Commit(std::move(commit_request), flags)) {
     return false;
   }
 
@@ -427,7 +435,8 @@
 }
 
 bool ScreenManager::TestAndSetLinearModifier(
-    const ControllerConfigsList& controllers_params) {
+    const ControllerConfigsList& controllers_params,
+    bool is_seamless_modeset) {
   TRACE_EVENT1("drm", "ScreenManager::TestAndSetLinearModifier",
                "display_count", controllers_params.size());
 
@@ -468,9 +477,10 @@
     }
   }
 
-  if (!drm->plane_manager()->Commit(
-          std::move(commit_request),
-          DRM_MODE_ATOMIC_TEST_ONLY | DRM_MODE_ATOMIC_ALLOW_MODESET)) {
+  uint32_t flags = DRM_MODE_ATOMIC_TEST_ONLY;
+  if (!is_seamless_modeset)
+    flags |= DRM_MODE_ATOMIC_ALLOW_MODESET;
+  if (!drm->plane_manager()->Commit(std::move(commit_request), flags)) {
     return false;
   }
 
@@ -501,7 +511,8 @@
 }
 
 bool ScreenManager::TestModesetWithOverlays(
-    const ControllerConfigsList& controllers_params) {
+    const ControllerConfigsList& controllers_params,
+    bool is_seamless_modeset) {
   TRACE_EVENT1("drm", "ScreenManager::TestModesetWithOverlays", "display_count",
                controllers_params.size());
 
@@ -537,13 +548,15 @@
   if (!does_an_overlay_exist)
     return false;
 
-  return drm->plane_manager()->Commit(
-      std::move(commit_request),
-      DRM_MODE_ATOMIC_TEST_ONLY | DRM_MODE_ATOMIC_ALLOW_MODESET);
+  uint32_t flags = DRM_MODE_ATOMIC_TEST_ONLY;
+  if (!is_seamless_modeset)
+    flags |= DRM_MODE_ATOMIC_ALLOW_MODESET;
+  return drm->plane_manager()->Commit(std::move(commit_request), flags);
 }
 
 bool ScreenManager::Modeset(const ControllerConfigsList& controllers_params,
-                            bool can_modeset_with_overlays) {
+                            bool can_modeset_with_overlays,
+                            bool is_seamless_modeset) {
   TRACE_EVENT2("drm", "ScreenManager::Modeset", "display_count",
                controllers_params.size(), "modeset_with_overlays",
                can_modeset_with_overlays);
@@ -579,8 +592,8 @@
     }
   }
 
-  bool commit_status = drm->plane_manager()->Commit(
-      commit_request, DRM_MODE_ATOMIC_ALLOW_MODESET);
+  uint32_t flags = is_seamless_modeset ? 0 : DRM_MODE_ATOMIC_ALLOW_MODESET;
+  bool commit_status = drm->plane_manager()->Commit(commit_request, flags);
 
   UpdateControllerStateAfterModeset(drm, commit_request, commit_status);
 
diff --git a/ui/ozone/platform/drm/gpu/screen_manager.h b/ui/ozone/platform/drm/gpu/screen_manager.h
index ada15d9..97ae879 100644
--- a/ui/ozone/platform/drm/gpu/screen_manager.h
+++ b/ui/ozone/platform/drm/gpu/screen_manager.h
@@ -119,9 +119,10 @@
       uint32_t crtc);
 
   bool TestAndSetPreferredModifiers(
-      const ControllerConfigsList& controllers_params);
-  bool TestAndSetLinearModifier(
-      const ControllerConfigsList& controllers_params);
+      const ControllerConfigsList& controllers_params,
+      bool is_seamless_modeset);
+  bool TestAndSetLinearModifier(const ControllerConfigsList& controllers_params,
+                                bool is_seamless_modeset);
   // Setting the Preferred modifiers that passed from one of the Modeset Test
   // functions. The preferred modifiers are used in Modeset.
   void SetPreferredModifiers(
@@ -130,9 +131,11 @@
   // The planes used for modesetting can have overlays beside the primary, test
   // if we can modeset with them. If not, return false to indicate that we must
   // only use the primary plane.
-  bool TestModesetWithOverlays(const ControllerConfigsList& controllers_params);
+  bool TestModesetWithOverlays(const ControllerConfigsList& controllers_params,
+                               bool is_seamless_modeset);
   bool Modeset(const ControllerConfigsList& controllers_params,
-               bool can_modeset_with_overlays);
+               bool can_modeset_with_overlays,
+               bool is_seamless_modeset);
 
   // Configures a display controller to be enabled. The display controller is
   // identified by (|crtc|, |connector|) and the controller is to be modeset
diff --git a/ui/ozone/platform/drm/gpu/screen_manager_unittest.cc b/ui/ozone/platform/drm/gpu/screen_manager_unittest.cc
index 3d0258b8..e91a722 100644
--- a/ui/ozone/platform/drm/gpu/screen_manager_unittest.cc
+++ b/ui/ozone/platform/drm/gpu/screen_manager_unittest.cc
@@ -265,6 +265,24 @@
   EXPECT_TRUE(controller->HasCrtc(drm_, kPrimaryCrtc));
 }
 
+TEST_F(ScreenManagerTest, CheckWithSeamlessModeset) {
+  InitializeDrmStateWithDefault(drm_.get(), /*is_atomic=*/true);
+
+  screen_manager_->AddDisplayController(drm_, kPrimaryCrtc, kPrimaryConnector);
+
+  ScreenManager::ControllerConfigsList controllers_to_enable;
+  controllers_to_enable.emplace_back(
+      kPrimaryDisplayId, drm_, kPrimaryCrtc, kPrimaryConnector,
+      GetPrimaryBounds().origin(),
+      std::make_unique<drmModeModeInfo>(kDefaultMode));
+  screen_manager_->ConfigureDisplayControllers(
+      controllers_to_enable,
+      display::kCommitModeset | display::kSeamlessModeset);
+
+  EXPECT_EQ(drm_->get_commit_modeset_count(), 0);
+  EXPECT_EQ(drm_->get_seamless_modeset_count(), 1);
+}
+
 TEST_F(ScreenManagerTest, CheckWithInvalidBounds) {
   InitializeDrmStateWithDefault(drm_.get(), /*is_atomic=*/true);
 
diff --git a/ui/ozone/platform/wayland/host/wayland_popup.cc b/ui/ozone/platform/wayland/host/wayland_popup.cc
index 68d8044..ef51aa8 100644
--- a/ui/ozone/platform/wayland/host/wayland_popup.cc
+++ b/ui/ozone/platform/wayland/host/wayland_popup.cc
@@ -50,11 +50,12 @@
   params.bounds = bounds_dip;
   params.menu_type =
       delegate()->GetMenuType().value_or(MenuType::kRootContextMenu);
-  params.anchor = delegate()->GetOwnedWindowAnchorAndRectInDIP();
+  params.anchor = delegate()->GetOwnedWindowAnchorAndRectInPx();
   if (params.anchor.has_value()) {
+    // TODO(crbug.com/1306688): Change anchor_rect to DIP.
     params.anchor->anchor_rect =
         delegate()->ConvertRectToDIP(wl::TranslateBoundsToParentCoordinates(
-            params.anchor->anchor_rect, parent_window()->GetBoundsInDIP()));
+            params.anchor->anchor_rect, parent_window()->GetBoundsInPixels()));
     // If size is empty, set 1x1.
     if (params.anchor->anchor_rect.size().IsEmpty())
       params.anchor->anchor_rect.set_size({1, 1});
diff --git a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
index 6dfa4580..89a37a2a 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
@@ -2302,7 +2302,7 @@
   MockWaylandPlatformWindowDelegate menu_window_delegate;
   EXPECT_CALL(menu_window_delegate, GetMenuType())
       .WillOnce(Return(MenuType::kRootMenu));
-  EXPECT_CALL(menu_window_delegate, GetOwnedWindowAnchorAndRectInDIP())
+  EXPECT_CALL(menu_window_delegate, GetOwnedWindowAnchorAndRectInPx())
       .WillOnce(Return(absl::nullopt));
   gfx::Rect menu_window_bounds(gfx::Point(439, 46),
                                menu_window_positioner.size);
@@ -2382,7 +2382,7 @@
       OwnedWindowAnchorPosition::kBottomRight,
       OwnedWindowAnchorGravity::kBottomLeft,
       OwnedWindowConstraintAdjustment::kAdjustmentFlipY};
-  EXPECT_CALL(menu_window_delegate, GetOwnedWindowAnchorAndRectInDIP())
+  EXPECT_CALL(menu_window_delegate, GetOwnedWindowAnchorAndRectInPx())
       .WillOnce(Return(anchor));
   gfx::Rect menu_window_bounds(gfx::Point(176, 74),
                                menu_window_positioner.size);
@@ -2405,7 +2405,7 @@
             OwnedWindowAnchorGravity::kBottomRight,
             OwnedWindowConstraintAdjustment::kAdjustmentFlipY |
                 OwnedWindowConstraintAdjustment::kAdjustmentFlipX};
-  EXPECT_CALL(nested_menu_window_delegate, GetOwnedWindowAnchorAndRectInDIP())
+  EXPECT_CALL(nested_menu_window_delegate, GetOwnedWindowAnchorAndRectInPx())
       .WillOnce(Return(anchor));
   gfx::Rect nested_menu_window_bounds(gfx::Point(492, 157),
                                       nested_menu_window_positioner.size);
diff --git a/ui/ozone/platform/x11/gl_ozone_glx.cc b/ui/ozone/platform/x11/gl_ozone_glx.cc
index 0e19b76..06dc614 100644
--- a/ui/ozone/platform/x11/gl_ozone_glx.cc
+++ b/ui/ozone/platform/x11/gl_ozone_glx.cc
@@ -90,7 +90,7 @@
 }
 
 bool GLOzoneGLX::CanImportNativePixmap() {
-  return gl::GLImageGLXNativePixmap::CanImportNativePixmap();
+  return false;
 }
 
 std::unique_ptr<NativePixmapGLBinding> GLOzoneGLX::ImportNativePixmap(
diff --git a/ui/ozone/platform/x11/x11_surface_factory.cc b/ui/ozone/platform/x11/x11_surface_factory.cc
index 9a122d1..e7affbd 100644
--- a/ui/ozone/platform/x11/x11_surface_factory.cc
+++ b/ui/ozone/platform/x11/x11_surface_factory.cc
@@ -45,10 +45,7 @@
     return GLOzoneEGL::InitializeStaticGLBindings(implementation);
   }
 
-  bool CanImportNativePixmap() override {
-    return gl::GLSurfaceEGL::GetGLDisplayEGL()
-        ->ext->b_EGL_NOK_texture_from_pixmap;
-  }
+  bool CanImportNativePixmap() override { return false; }
 
   // This implementation is used when ANGLE supports pixmaps through
   // eglCreatePixmapSurface and exposes it through EGL extension
diff --git a/ui/ozone/test/mock_platform_window_delegate.h b/ui/ozone/test/mock_platform_window_delegate.h
index c63540a..9da8666 100644
--- a/ui/ozone/test/mock_platform_window_delegate.h
+++ b/ui/ozone/test/mock_platform_window_delegate.h
@@ -40,7 +40,7 @@
   MOCK_METHOD0(GetMinimumSizeForWindow, absl::optional<gfx::Size>());
   MOCK_METHOD0(GetMaximumSizeForWindow, absl::optional<gfx::Size>());
   MOCK_METHOD0(GetMenuType, absl::optional<MenuType>());
-  MOCK_METHOD0(GetOwnedWindowAnchorAndRectInDIP,
+  MOCK_METHOD0(GetOwnedWindowAnchorAndRectInPx,
                absl::optional<OwnedWindowAnchor>());
   MOCK_METHOD0(OnMouseEnter, void());
 };
diff --git a/ui/platform_window/platform_window_delegate.cc b/ui/platform_window/platform_window_delegate.cc
index 5b88c11..ea10ab5 100644
--- a/ui/platform_window/platform_window_delegate.cc
+++ b/ui/platform_window/platform_window_delegate.cc
@@ -44,7 +44,7 @@
     PlatformWindowOcclusionState occlusion_state) {}
 
 absl::optional<OwnedWindowAnchor>
-PlatformWindowDelegate::GetOwnedWindowAnchorAndRectInDIP() {
+PlatformWindowDelegate::GetOwnedWindowAnchorAndRectInPx() {
   return absl::nullopt;
 }
 
diff --git a/ui/platform_window/platform_window_delegate.h b/ui/platform_window/platform_window_delegate.h
index ea72df25..1bae07b 100644
--- a/ui/platform_window/platform_window_delegate.h
+++ b/ui/platform_window/platform_window_delegate.h
@@ -135,7 +135,7 @@
   // positioning. Useful for such backends as Wayland as it provides flexibility
   // in positioning child windows, which must be repositioned if the originally
   // intended position caused the surface to be constrained.
-  virtual absl::optional<OwnedWindowAnchor> GetOwnedWindowAnchorAndRectInDIP();
+  virtual absl::optional<OwnedWindowAnchor> GetOwnedWindowAnchorAndRectInPx();
 
   // Enables or disables frame rate throttling.
   virtual void SetFrameRateThrottleEnabled(bool enabled);
diff --git a/ui/qt/qt_ui.cc b/ui/qt/qt_ui.cc
index a44ed93b..5cb5798 100644
--- a/ui/qt/qt_ui.cc
+++ b/ui/qt/qt_ui.cc
@@ -137,7 +137,7 @@
 void QtUi::GetDefaultFontDescription(std::string* family_out,
                                      int* size_pixels_out,
                                      int* style_out,
-                                     gfx::Font::Weight* weight_out,
+                                     int* weight_out,
                                      gfx::FontRenderParams* params_out) const {
   if (family_out)
     *family_out = font_family_;
@@ -335,15 +335,14 @@
     font_size_pixels_ = font_size_points_ * GetDeviceScaleFactor();
   }
   font_style_ = desc.is_italic ? gfx::Font::ITALIC : gfx::Font::NORMAL;
-  font_weight_ =
-      static_cast<gfx::Font::Weight>(QtWeightToCssWeight(desc.weight));
+  font_weight_ = QtWeightToCssWeight(desc.weight);
 
   gfx::FontRenderParamsQuery query;
   query.families = {font_family_};
   query.pixel_size = font_size_pixels_;
   query.point_size = font_size_points_;
   query.style = font_style_;
-  query.weight = font_weight_;
+  query.weight = static_cast<gfx::Font::Weight>(font_weight_);
 
   gfx::FontRenderParams fc_params;
   gfx::QueryFontconfig(query, &fc_params, nullptr);
diff --git a/ui/qt/qt_ui.h b/ui/qt/qt_ui.h
index 179b0b3..2735e26 100644
--- a/ui/qt/qt_ui.h
+++ b/ui/qt/qt_ui.h
@@ -12,6 +12,7 @@
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "ui/color/color_provider.h"
 #include "ui/color/color_provider_manager.h"
+#include "ui/gfx/font_render_params.h"
 #include "ui/linux/linux_ui_base.h"
 #include "ui/qt/qt_interface.h"
 
@@ -43,7 +44,7 @@
       std::string* family_out,
       int* size_pixels_out,
       int* style_out,
-      gfx::Font::Weight* weight_out,
+      int* weight_out,
       gfx::FontRenderParams* params_out) const override;
 
   // ui::ShellDialogLinux:
@@ -111,7 +112,7 @@
   int font_size_pixels_ = 0;
   int font_size_points_ = 0;
   gfx::Font::FontStyle font_style_ = gfx::Font::NORMAL;
-  gfx::Font::Weight font_weight_;
+  int font_weight_;
   gfx::FontRenderParams font_params_;
 
   std::unique_ptr<QtInterface> shim_;
diff --git a/ui/views/cocoa/native_widget_mac_ns_window_host.mm b/ui/views/cocoa/native_widget_mac_ns_window_host.mm
index 0ca869c..84aa682 100644
--- a/ui/views/cocoa/native_widget_mac_ns_window_host.mm
+++ b/ui/views/cocoa/native_widget_mac_ns_window_host.mm
@@ -373,11 +373,10 @@
           in_process_ns_window_bridge_.get(), GetNSWindowMojo());
 
   Widget* widget = native_widget_mac_->GetWidget();
-  // Tooltip Widgets shouldn't have their own tooltip manager, but tooltips are
-  // native on Mac, so nothing should ever want one in Widget form.
-  DCHECK_NE(params.type, Widget::InitParams::TYPE_TOOLTIP);
   widget_type_ = params.type;
-  tooltip_manager_ = std::make_unique<TooltipManagerMac>(GetNSWindowMojo());
+  bool is_tooltip = params.type == Widget::InitParams::TYPE_TOOLTIP;
+  if (!is_tooltip)
+    tooltip_manager_ = std::make_unique<TooltipManagerMac>(GetNSWindowMojo());
 
   if (params.workspace.length()) {
     std::string restoration_data;
@@ -396,6 +395,7 @@
     window_params->is_translucent =
         params.opacity == Widget::InitParams::WindowOpacity::kTranslucent;
     window_params->is_headless_mode_window = params.headless_mode;
+    window_params->is_tooltip = is_tooltip;
     is_headless_mode_window_ = params.headless_mode;
 
     // OSX likes to put shadows on most things. However, frameless windows (with
diff --git a/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc b/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc
index a24ea472..fdead2a 100644
--- a/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc
+++ b/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc
@@ -904,12 +904,17 @@
 }
 
 absl::optional<ui::OwnedWindowAnchor>
-DesktopWindowTreeHostPlatform::GetOwnedWindowAnchorAndRectInDIP() {
-  const auto* anchor =
+DesktopWindowTreeHostPlatform::GetOwnedWindowAnchorAndRectInPx() {
+  auto* anchor =
       GetContentWindow()->GetProperty(aura::client::kOwnedWindowAnchor);
   if (!anchor)
     return absl::nullopt;
-  return *anchor;
+  // Make a copy of the structure. Otherwise, conversion will result in
+  // overriding the stored property's value.
+  ui::OwnedWindowAnchor window_anchor = *anchor;
+  // Anchor rect must be translated from DIP to px.
+  window_anchor.anchor_rect = ToPixelRect(window_anchor.anchor_rect);
+  return window_anchor;
 }
 
 gfx::Rect DesktopWindowTreeHostPlatform::ConvertRectToPixels(
diff --git a/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.h b/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.h
index cbc3e81..cbf0abe 100644
--- a/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.h
+++ b/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.h
@@ -151,7 +151,7 @@
   absl::optional<gfx::Size> GetMaximumSizeForWindow() override;
   SkPath GetWindowMaskForWindowShapeInPixels() override;
   absl::optional<ui::MenuType> GetMenuType() override;
-  absl::optional<ui::OwnedWindowAnchor> GetOwnedWindowAnchorAndRectInDIP()
+  absl::optional<ui::OwnedWindowAnchor> GetOwnedWindowAnchorAndRectInPx()
       override;
   gfx::Rect ConvertRectToPixels(const gfx::Rect& rect_in_dip) const override;
   gfx::Rect ConvertRectToDIP(const gfx::Rect& rect_in_pixels) const override;
diff --git a/ui/views/widget/native_widget_mac_unittest.mm b/ui/views/widget/native_widget_mac_unittest.mm
index 7eab8980..32d9648 100644
--- a/ui/views/widget/native_widget_mac_unittest.mm
+++ b/ui/views/widget/native_widget_mac_unittest.mm
@@ -2082,6 +2082,31 @@
   parent->CloseNow();
 }
 
+// Tests that tooltip widgets get the correct accessibilty role so that they're
+// not announced as windows by VoiceOver.
+TEST_F(NativeWidgetMacTest, AccessibilityRole) {
+  {
+    NativeWidgetMacTestWindow* window;
+
+    Widget::InitParams init_params =
+        CreateParams(Widget::InitParams::TYPE_WINDOW);
+    Widget* widget =
+        CreateWidgetWithTestWindow(std::move(init_params), &window);
+    ASSERT_EQ([window accessibilityRole], NSAccessibilityWindowRole);
+    widget->CloseNow();
+  }
+  {
+    NativeWidgetMacTestWindow* window;
+
+    Widget::InitParams init_params =
+        CreateParams(Widget::InitParams::TYPE_TOOLTIP);
+    Widget* widget =
+        CreateWidgetWithTestWindow(std::move(init_params), &window);
+    ASSERT_EQ([window accessibilityRole], NSAccessibilityHelpTagRole);
+    widget->CloseNow();
+  }
+}
+
 // Test that updateFullKeyboardAccess method on BridgedContentView correctly
 // sets the keyboard accessibility mode on the associated focus manager.
 TEST_F(NativeWidgetMacFullKeyboardAccessTest, FullKeyboardToggle) {
diff --git a/ui/views/widget/widget_interactive_uitest.cc b/ui/views/widget/widget_interactive_uitest.cc
index 04791dca..f20021d 100644
--- a/ui/views/widget/widget_interactive_uitest.cc
+++ b/ui/views/widget/widget_interactive_uitest.cc
@@ -42,6 +42,7 @@
 #include "ui/views/test/widget_test.h"
 #include "ui/views/touchui/touch_selection_controller_impl.h"
 #include "ui/views/widget/root_view.h"
+#include "ui/views/widget/unique_widget_ptr.h"
 #include "ui/views/widget/widget.h"
 #include "ui/views/widget/widget_utils.h"
 #include "ui/views/window/dialog_delegate.h"
@@ -62,6 +63,25 @@
 
 namespace {
 
+template <class T>
+class UniqueWidgetPtrT : public views::UniqueWidgetPtr {
+ public:
+  UniqueWidgetPtrT() = default;
+  UniqueWidgetPtrT(std::unique_ptr<T> widget)  // NOLINT
+      : views::UniqueWidgetPtr(std::move(widget)) {}
+  UniqueWidgetPtrT(UniqueWidgetPtrT&&) = default;
+  UniqueWidgetPtrT& operator=(UniqueWidgetPtrT&&) = default;
+  ~UniqueWidgetPtrT() = default;
+
+  T& operator*() const {
+    return static_cast<T&>(views::UniqueWidgetPtr::operator*());
+  }
+  T* operator->() const {
+    return static_cast<T*>(views::UniqueWidgetPtr::operator->());
+  }
+  T* get() const { return static_cast<T*>(views::UniqueWidgetPtr::get()); }
+};
+
 // A View that closes the Widget and exits the current message-loop when it
 // receives a mouse-release event.
 class ExitLoopOnRelease : public View {
@@ -517,9 +537,9 @@
 // Test view focus restoration when a widget is deactivated and re-activated.
 TEST_F(WidgetTestInteractive, ViewFocusOnWidgetActivationChanges) {
   WidgetAutoclosePtr widget1(CreateTopLevelPlatformWidget());
-  View* view1 = new View;
+  View* view1 =
+      widget1->GetContentsView()->AddChildView(std::make_unique<View>());
   view1->SetFocusBehavior(View::FocusBehavior::ALWAYS);
-  widget1->GetContentsView()->AddChildView(view1);
 
   WidgetAutoclosePtr widget2(CreateTopLevelPlatformWidget());
   View* view2a = new View;
@@ -765,33 +785,33 @@
 // Tests whether the widget only becomes active when the underlying window
 // is really active.
 TEST_F(WidgetTestInteractive, WidgetNotActivatedOnFakeActivationMessages) {
-  WidgetActivationTest widget1;
+  UniqueWidgetPtrT widget1 = std::make_unique<WidgetActivationTest>();
   Widget::InitParams init_params =
       CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
-  init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
-  init_params.native_widget = new DesktopNativeWidgetAura(&widget1);
+  init_params.native_widget = new DesktopNativeWidgetAura(widget1.get());
   init_params.bounds = gfx::Rect(0, 0, 200, 200);
-  widget1.Init(std::move(init_params));
-  widget1.Show();
-  EXPECT_EQ(true, widget1.active());
+  widget1->Init(std::move(init_params));
+  widget1->Show();
+  EXPECT_EQ(true, widget1->active());
 
-  WidgetActivationTest widget2;
-  init_params.native_widget = new DesktopNativeWidgetAura(&widget2);
-  widget2.Init(std::move(init_params));
-  widget2.Show();
-  EXPECT_EQ(true, widget2.active());
-  EXPECT_EQ(false, widget1.active());
+  UniqueWidgetPtrT widget2 = std::make_unique<WidgetActivationTest>();
+  init_params = CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
+  init_params.native_widget = new DesktopNativeWidgetAura(widget2.get());
+  widget2->Init(std::move(init_params));
+  widget2->Show();
+  EXPECT_EQ(true, widget2->active());
+  EXPECT_EQ(false, widget1->active());
 
-  HWND win32_native_window1 = HWNDForWidget(&widget1);
+  HWND win32_native_window1 = HWNDForWidget(widget1.get());
   EXPECT_TRUE(::IsWindow(win32_native_window1));
 
   ::SendMessage(win32_native_window1, WM_NCACTIVATE, 1, 0);
-  EXPECT_EQ(false, widget1.active());
-  EXPECT_EQ(true, widget2.active());
+  EXPECT_EQ(false, widget1->active());
+  EXPECT_EQ(true, widget2->active());
 
   ::SetActiveWindow(win32_native_window1);
-  EXPECT_EQ(true, widget1.active());
-  EXPECT_EQ(false, widget2.active());
+  EXPECT_EQ(true, widget1->active());
+  EXPECT_EQ(false, widget2->active());
 }
 
 // On Windows if we create a fullscreen window on a thread, then it affects the
@@ -799,42 +819,41 @@
 // this we reduce the bounds of a fullscreen window by 1px when it loses
 // activation. This test verifies the same.
 TEST_F(WidgetTestInteractive, FullscreenBoundsReducedOnActivationLoss) {
-  Widget widget1;
+  UniqueWidgetPtr widget1 = std::make_unique<Widget>();
   Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
-  params.native_widget = new DesktopNativeWidgetAura(&widget1);
-  params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
-  widget1.Init(std::move(params));
-  widget1.SetBounds(gfx::Rect(0, 0, 200, 200));
-  widget1.Show();
+  params.native_widget = new DesktopNativeWidgetAura(widget1.get());
+  widget1->Init(std::move(params));
+  widget1->SetBounds(gfx::Rect(0, 0, 200, 200));
+  widget1->Show();
 
-  widget1.Activate();
+  widget1->Activate();
   RunPendingMessages();
   EXPECT_EQ(::GetActiveWindow(),
-            widget1.GetNativeWindow()->GetHost()->GetAcceleratedWidget());
+            widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget());
 
-  widget1.SetFullscreen(true);
-  EXPECT_TRUE(widget1.IsFullscreen());
+  widget1->SetFullscreen(true);
+  EXPECT_TRUE(widget1->IsFullscreen());
   // Ensure that the StopIgnoringPosChanges task in HWNDMessageHandler runs.
   // This task is queued when a widget becomes fullscreen.
   RunPendingMessages();
   EXPECT_EQ(::GetActiveWindow(),
-            widget1.GetNativeWindow()->GetHost()->GetAcceleratedWidget());
-  gfx::Rect fullscreen_bounds = widget1.GetWindowBoundsInScreen();
+            widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget());
+  gfx::Rect fullscreen_bounds = widget1->GetWindowBoundsInScreen();
 
-  Widget widget2;
-  params.native_widget = new DesktopNativeWidgetAura(&widget2);
-  params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
-  widget2.Init(std::move(params));
-  widget2.SetBounds(gfx::Rect(0, 0, 200, 200));
-  widget2.Show();
+  UniqueWidgetPtr widget2 = std::make_unique<Widget>();
+  params = CreateParams(Widget::InitParams::TYPE_WINDOW);
+  params.native_widget = new DesktopNativeWidgetAura(widget2.get());
+  widget2->Init(std::move(params));
+  widget2->SetBounds(gfx::Rect(0, 0, 200, 200));
+  widget2->Show();
 
-  widget2.Activate();
+  widget2->Activate();
   RunPendingMessages();
   EXPECT_EQ(::GetActiveWindow(),
-            widget2.GetNativeWindow()->GetHost()->GetAcceleratedWidget());
+            widget2->GetNativeWindow()->GetHost()->GetAcceleratedWidget());
 
   gfx::Rect fullscreen_bounds_after_activation_loss =
-      widget1.GetWindowBoundsInScreen();
+      widget1->GetWindowBoundsInScreen();
 
   // After deactivation loss the bounds of the fullscreen widget should be
   // reduced by 1px.
@@ -842,44 +861,40 @@
                 fullscreen_bounds_after_activation_loss.height(),
             1);
 
-  widget1.Activate();
+  widget1->Activate();
   RunPendingMessages();
   EXPECT_EQ(::GetActiveWindow(),
-            widget1.GetNativeWindow()->GetHost()->GetAcceleratedWidget());
+            widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget());
 
   gfx::Rect fullscreen_bounds_after_activate =
-      widget1.GetWindowBoundsInScreen();
+      widget1->GetWindowBoundsInScreen();
 
   // After activation the bounds of the fullscreen widget should be restored.
   EXPECT_EQ(fullscreen_bounds, fullscreen_bounds_after_activate);
-
-  widget1.CloseNow();
-  widget2.CloseNow();
 }
 
 // Ensure the window rect and client rects are correct with a window that was
 // maximized.
 TEST_F(WidgetTestInteractive, FullscreenMaximizedWindowBounds) {
-  Widget widget;
+  UniqueWidgetPtr widget = std::make_unique<Widget>();
   Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
-  params.native_widget = new DesktopNativeWidgetAura(&widget);
-  params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
-  widget.set_frame_type(Widget::FrameType::kForceCustom);
-  widget.Init(std::move(params));
-  widget.SetBounds(gfx::Rect(0, 0, 200, 200));
-  widget.Show();
+  params.native_widget = new DesktopNativeWidgetAura(widget.get());
+  widget->set_frame_type(Widget::FrameType::kForceCustom);
+  widget->Init(std::move(params));
+  widget->SetBounds(gfx::Rect(0, 0, 200, 200));
+  widget->Show();
 
-  widget.Maximize();
-  EXPECT_TRUE(widget.IsMaximized());
+  widget->Maximize();
+  EXPECT_TRUE(widget->IsMaximized());
 
-  widget.SetFullscreen(true);
-  EXPECT_TRUE(widget.IsFullscreen());
-  EXPECT_FALSE(widget.IsMaximized());
+  widget->SetFullscreen(true);
+  EXPECT_TRUE(widget->IsFullscreen());
+  EXPECT_FALSE(widget->IsMaximized());
   // Ensure that the StopIgnoringPosChanges task in HWNDMessageHandler runs.
   // This task is queued when a widget becomes fullscreen.
   RunPendingMessages();
 
-  aura::WindowTreeHost* host = widget.GetNativeWindow()->GetHost();
+  aura::WindowTreeHost* host = widget->GetNativeWindow()->GetHost();
   HWND hwnd = host->GetAcceleratedWidget();
 
   HMONITOR monitor = ::MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
@@ -889,22 +904,20 @@
   ASSERT_TRUE(::GetMonitorInfo(monitor, &monitor_info));
 
   gfx::Rect monitor_bounds(monitor_info.rcMonitor);
-  gfx::Rect window_bounds = widget.GetWindowBoundsInScreen();
+  gfx::Rect window_bounds = widget->GetWindowBoundsInScreen();
   gfx::Rect client_area_bounds = host->GetBoundsInPixels();
 
   EXPECT_EQ(window_bounds, monitor_bounds);
   EXPECT_EQ(monitor_bounds, client_area_bounds);
 
   // Setting not fullscreen should return it to maximized.
-  widget.SetFullscreen(false);
-  EXPECT_FALSE(widget.IsFullscreen());
-  EXPECT_TRUE(widget.IsMaximized());
+  widget->SetFullscreen(false);
+  EXPECT_FALSE(widget->IsFullscreen());
+  EXPECT_TRUE(widget->IsMaximized());
 
   client_area_bounds = host->GetBoundsInPixels();
   EXPECT_TRUE(monitor_bounds.Contains(client_area_bounds));
   EXPECT_NE(monitor_bounds, client_area_bounds);
-
-  widget.CloseNow();
 }
 #endif  // BUILDFLAG(IS_WIN)
 
@@ -918,17 +931,16 @@
       focus_listener.focus_changes();
 
   // Create a top level widget.
-  Widget top_level_widget;
+  UniqueWidgetPtr top_level_widget = std::make_unique<Widget>();
   Widget::InitParams init_params =
       CreateParams(Widget::InitParams::TYPE_WINDOW);
   init_params.show_state = ui::SHOW_STATE_NORMAL;
   gfx::Rect initial_bounds(0, 0, 500, 500);
   init_params.bounds = initial_bounds;
-  init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
-  top_level_widget.Init(std::move(init_params));
-  ShowSync(&top_level_widget);
+  top_level_widget->Init(std::move(init_params));
+  ShowSync(top_level_widget.get());
 
-  gfx::NativeView top_level_native_view = top_level_widget.GetNativeView();
+  gfx::NativeView top_level_native_view = top_level_widget->GetNativeView();
   ASSERT_FALSE(focus_listener.focus_changes().empty());
   EXPECT_EQ(1u, focus_changes.size());
   EXPECT_EQ(top_level_native_view, focus_changes[0]);
@@ -938,7 +950,7 @@
   dialog_delegate->SetModalType(ui::MODAL_TYPE_WINDOW);
 
   Widget* modal_dialog_widget = views::DialogDelegate::CreateDialogWidget(
-      dialog_delegate.release(), nullptr, top_level_widget.GetNativeView());
+      dialog_delegate.release(), nullptr, top_level_widget->GetNativeView());
   modal_dialog_widget->SetBounds(gfx::Rect(100, 100, 200, 200));
 
   // Note the dialog widget doesn't need a ShowSync. Since it is modal, it gains
@@ -953,33 +965,34 @@
 #if BUILDFLAG(IS_MAC)
   // Window modal dialogs on Mac are "sheets", which animate to close before
   // activating their parent widget.
-  views::test::WidgetActivationWaiter waiter(&top_level_widget, true);
+  views::test::WidgetActivationWaiter waiter(top_level_widget.get(), true);
   modal_dialog_widget->Close();
   waiter.Wait();
 #else
-  modal_dialog_widget->CloseNow();
+  views::test::WidgetDestroyedWaiter waiter(modal_dialog_widget);
+  modal_dialog_widget->Close();
+  waiter.Wait();
 #endif
 
   ASSERT_EQ(5u, focus_changes.size());
   EXPECT_EQ(gfx::kNullNativeView, focus_changes[3]);
   EXPECT_EQ(top_level_native_view, focus_changes[4]);
 
-  top_level_widget.CloseNow();
+  top_level_widget->Close();
   WidgetFocusManager::GetInstance()->RemoveFocusChangeListener(&focus_listener);
 }
 #endif
 
 TEST_F(DesktopWidgetTestInteractive, CanActivateFlagIsHonored) {
-  Widget widget;
+  UniqueWidgetPtr widget = std::make_unique<Widget>();
   Widget::InitParams init_params =
       CreateParams(Widget::InitParams::TYPE_WINDOW);
   init_params.bounds = gfx::Rect(0, 0, 200, 200);
-  init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
   init_params.activatable = Widget::InitParams::Activatable::kNo;
-  widget.Init(std::move(init_params));
+  widget->Init(std::move(init_params));
 
-  widget.Show();
-  EXPECT_FALSE(widget.IsActive());
+  widget->Show();
+  EXPECT_FALSE(widget->IsActive());
 }
 
 #if defined(USE_AURA)
@@ -1022,41 +1035,37 @@
 #endif  // !BUILDFLAG(IS_WIN)
 
   // Create first widget and view, activate the widget, and focus the view.
-  Widget widget1;
+  UniqueWidgetPtr widget1 = std::make_unique<Widget>();
   Widget::InitParams params1 = CreateParams(Widget::InitParams::TYPE_POPUP);
-  params1.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
   params1.activatable = Widget::InitParams::Activatable::kYes;
-  widget1.Init(std::move(params1));
+  widget1->Init(std::move(params1));
 
-  View* view1 = new View();
+  View* view1 = widget1->GetRootView()->AddChildView(std::make_unique<View>());
   view1->SetFocusBehavior(View::FocusBehavior::ALWAYS);
-  widget1.GetRootView()->AddChildView(view1);
 
-  widget1.Show();
-  ActivateSync(&widget1);
+  widget1->Show();
+  ActivateSync(widget1.get());
 
-  FocusManager* focus_manager1 = widget1.GetFocusManager();
+  FocusManager* focus_manager1 = widget1->GetFocusManager();
   ASSERT_TRUE(focus_manager1);
   focus_manager1->SetFocusedView(view1);
   EXPECT_EQ(view1, focus_manager1->GetFocusedView());
 
   // Create second widget and view, activate the widget, and focus the view.
-  Widget widget2;
+  UniqueWidgetPtr widget2 = std::make_unique<Widget>();
   Widget::InitParams params2 = CreateParams(Widget::InitParams::TYPE_POPUP);
-  params2.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
   params2.activatable = Widget::InitParams::Activatable::kYes;
-  widget2.Init(std::move(params2));
+  widget2->Init(std::move(params2));
 
-  View* view2 = new View();
+  View* view2 = widget2->GetRootView()->AddChildView(std::make_unique<View>());
   view2->SetFocusBehavior(View::FocusBehavior::ALWAYS);
-  widget2.GetRootView()->AddChildView(view2);
 
-  widget2.Show();
-  ActivateSync(&widget2);
-  EXPECT_TRUE(widget2.IsActive());
-  EXPECT_FALSE(widget1.IsActive());
+  widget2->Show();
+  ActivateSync(widget2.get());
+  EXPECT_TRUE(widget2->IsActive());
+  EXPECT_FALSE(widget1->IsActive());
 
-  FocusManager* focus_manager2 = widget2.GetFocusManager();
+  FocusManager* focus_manager2 = widget2->GetFocusManager();
   ASSERT_TRUE(focus_manager2);
   focus_manager2->SetFocusedView(view2);
   EXPECT_EQ(view2, focus_manager2->GetFocusedView());
@@ -1065,8 +1074,8 @@
   // activated.
   view1->SetEnabled(false);
   EXPECT_NE(view1, focus_manager1->GetFocusedView());
-  EXPECT_FALSE(widget1.IsActive());
-  EXPECT_TRUE(widget2.IsActive());
+  EXPECT_FALSE(widget1->IsActive());
+  EXPECT_TRUE(widget2->IsActive());
 }
 
 TEST_F(WidgetTestInteractive, ShowCreatesActiveWindow) {
@@ -1140,13 +1149,12 @@
 
 #if BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_MAC)
 TEST_F(WidgetTestInteractive, InactiveWidgetDoesNotGrabActivation) {
-  WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget());
+  UniqueWidgetPtr widget = base::WrapUnique(CreateTopLevelPlatformWidget());
   ShowSync(widget.get());
   EXPECT_EQ(GetWidgetShowState(widget.get()), ui::SHOW_STATE_NORMAL);
 
-  WidgetAutoclosePtr widget2(new Widget());
+  UniqueWidgetPtr widget2 = std::make_unique<Widget>();
   Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_POPUP);
-  params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
   widget2->Init(std::move(params));
   widget2->Show();
   RunPendingMessagesForActiveStatusChange();
@@ -1459,36 +1467,36 @@
   // Verifies Widget::SetCapture() results in updating native capture along with
   // invoking the right Widget function.
   void TestCapture(bool use_desktop_native_widget) {
-    CaptureLostState capture_state1;
-    CaptureLostTrackingWidget widget1(&capture_state1);
-    InitPlatformWidget(&widget1, use_desktop_native_widget);
-    widget1.Show();
+    UniqueWidgetPtrT widget1 =
+        std::make_unique<CaptureLostTrackingWidget>(capture_state1_.get());
+    InitPlatformWidget(widget1.get(), use_desktop_native_widget);
+    widget1->Show();
 
-    CaptureLostState capture_state2;
-    CaptureLostTrackingWidget widget2(&capture_state2);
-    InitPlatformWidget(&widget2, use_desktop_native_widget);
-    widget2.Show();
+    UniqueWidgetPtrT widget2 =
+        std::make_unique<CaptureLostTrackingWidget>(capture_state2_.get());
+    InitPlatformWidget(widget2.get(), use_desktop_native_widget);
+    widget2->Show();
 
     // Set capture to widget2 and verity it gets it.
-    widget2.SetCapture(widget2.GetRootView());
-    EXPECT_FALSE(widget1.HasCapture());
-    EXPECT_TRUE(widget2.HasCapture());
-    EXPECT_FALSE(capture_state1.GetAndClearGotCaptureLost());
-    EXPECT_FALSE(capture_state2.GetAndClearGotCaptureLost());
+    widget2->SetCapture(widget2->GetRootView());
+    EXPECT_FALSE(widget1->HasCapture());
+    EXPECT_TRUE(widget2->HasCapture());
+    EXPECT_FALSE(capture_state1_->GetAndClearGotCaptureLost());
+    EXPECT_FALSE(capture_state2_->GetAndClearGotCaptureLost());
 
     // Set capture to widget1 and verify it gets it.
-    widget1.SetCapture(widget1.GetRootView());
-    EXPECT_TRUE(widget1.HasCapture());
-    EXPECT_FALSE(widget2.HasCapture());
-    EXPECT_FALSE(capture_state1.GetAndClearGotCaptureLost());
-    EXPECT_TRUE(capture_state2.GetAndClearGotCaptureLost());
+    widget1->SetCapture(widget1->GetRootView());
+    EXPECT_TRUE(widget1->HasCapture());
+    EXPECT_FALSE(widget2->HasCapture());
+    EXPECT_FALSE(capture_state1_->GetAndClearGotCaptureLost());
+    EXPECT_TRUE(capture_state2_->GetAndClearGotCaptureLost());
 
     // Release and verify no one has it.
-    widget1.ReleaseCapture();
-    EXPECT_FALSE(widget1.HasCapture());
-    EXPECT_FALSE(widget2.HasCapture());
-    EXPECT_TRUE(capture_state1.GetAndClearGotCaptureLost());
-    EXPECT_FALSE(capture_state2.GetAndClearGotCaptureLost());
+    widget1->ReleaseCapture();
+    EXPECT_FALSE(widget1->HasCapture());
+    EXPECT_FALSE(widget2->HasCapture());
+    EXPECT_TRUE(capture_state1_->GetAndClearGotCaptureLost());
+    EXPECT_FALSE(capture_state2_->GetAndClearGotCaptureLost());
   }
 
   void InitPlatformWidget(Widget* widget, bool use_desktop_native_widget) {
@@ -1499,9 +1507,25 @@
         use_desktop_native_widget
             ? nullptr
             : CreatePlatformNativeWidgetImpl(widget, kDefault, nullptr);
-    params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
     widget->Init(std::move(params));
   }
+
+ protected:
+  void SetUp() override {
+    DesktopWidgetTestInteractive::SetUp();
+    capture_state1_ = std::make_unique<CaptureLostState>();
+    capture_state2_ = std::make_unique<CaptureLostState>();
+  }
+
+  void TearDown() override {
+    capture_state1_.reset();
+    capture_state2_.reset();
+    DesktopWidgetTestInteractive::TearDown();
+  }
+
+ private:
+  std::unique_ptr<CaptureLostState> capture_state1_;
+  std::unique_ptr<CaptureLostState> capture_state2_;
 };
 
 // See description in TestCapture().
@@ -1549,6 +1573,7 @@
   EXPECT_TRUE(capture_state.GetAndClearGotCaptureLost());
 }
 
+// TODO(kylixrd): Remove this test once Widget ownership is normalized.
 TEST_F(WidgetCaptureTest, DestroyWithCapture_WidgetOwnsNativeWidget) {
   Widget widget;
   Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
@@ -1562,32 +1587,31 @@
 
 // Test that no state is set if capture fails.
 TEST_F(WidgetCaptureTest, FailedCaptureRequestIsNoop) {
-  Widget widget;
+  UniqueWidgetPtr widget = std::make_unique<Widget>();
   Widget::InitParams params =
       CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
-  params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
   params.bounds = gfx::Rect(400, 400);
-  widget.Init(std::move(params));
+  widget->Init(std::move(params));
 
-  MouseView* mouse_view1 = new MouseView;
-  MouseView* mouse_view2 = new MouseView;
   auto contents_view = std::make_unique<View>();
-  contents_view->AddChildView(mouse_view1);
-  contents_view->AddChildView(mouse_view2);
-  widget.SetContentsView(std::move(contents_view));
+  MouseView* mouse_view1 =
+      contents_view->AddChildView(std::make_unique<MouseView>());
+  MouseView* mouse_view2 =
+      contents_view->AddChildView(std::make_unique<MouseView>());
+  widget->SetContentsView(std::move(contents_view));
 
   mouse_view1->SetBounds(0, 0, 200, 400);
   mouse_view2->SetBounds(200, 0, 200, 400);
 
   // Setting capture should fail because |widget| is not visible.
-  widget.SetCapture(mouse_view1);
-  EXPECT_FALSE(widget.HasCapture());
+  widget->SetCapture(mouse_view1);
+  EXPECT_FALSE(widget->HasCapture());
 
-  widget.Show();
-  ui::test::EventGenerator generator(GetRootWindow(&widget),
-                                     widget.GetNativeWindow());
+  widget->Show();
+  ui::test::EventGenerator generator(GetRootWindow(widget.get()),
+                                     widget->GetNativeWindow());
   generator.set_current_screen_location(
-      widget.GetClientAreaBoundsInScreen().CenterPoint());
+      widget->GetClientAreaBoundsInScreen().CenterPoint());
   generator.PressLeftButton();
 
   EXPECT_FALSE(mouse_view1->pressed());
@@ -1799,37 +1823,33 @@
   WidgetFocusManager::GetInstance()->AddFocusChangeListener(&focus_listener);
 
   // Create a top level widget.
-  Widget top_level_widget;
+  UniqueWidgetPtr top_level_widget = std::make_unique<Widget>();
   Widget::InitParams init_params =
       CreateParams(Widget::InitParams::TYPE_WINDOW);
   init_params.show_state = ui::SHOW_STATE_NORMAL;
   gfx::Rect initial_bounds(0, 0, 500, 500);
   init_params.bounds = initial_bounds;
-  init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
-  top_level_widget.Init(std::move(init_params));
-  ShowSync(&top_level_widget);
+  top_level_widget->Init(std::move(init_params));
+  ShowSync(top_level_widget.get());
 
   ASSERT_FALSE(focus_listener.focus_changes().empty());
-  EXPECT_EQ(top_level_widget.GetNativeView(),
+  EXPECT_EQ(top_level_widget->GetNativeView(),
             focus_listener.focus_changes().back());
 
-  EXPECT_FALSE(top_level_widget.HasCapture());
-  top_level_widget.SetCapture(nullptr);
-  EXPECT_TRUE(top_level_widget.HasCapture());
+  EXPECT_FALSE(top_level_widget->HasCapture());
+  top_level_widget->SetCapture(nullptr);
+  EXPECT_TRUE(top_level_widget->HasCapture());
 
   // Create a modal dialog.
   auto dialog_delegate = std::make_unique<DialogDelegateView>();
   dialog_delegate->SetModalType(ui::MODAL_TYPE_SYSTEM);
 
   Widget* modal_dialog_widget = views::DialogDelegate::CreateDialogWidget(
-      dialog_delegate.release(), nullptr, top_level_widget.GetNativeView());
+      dialog_delegate.release(), nullptr, top_level_widget->GetNativeView());
   modal_dialog_widget->SetBounds(gfx::Rect(100, 100, 200, 200));
   ShowSync(modal_dialog_widget);
 
-  EXPECT_FALSE(top_level_widget.HasCapture());
-
-  modal_dialog_widget->CloseNow();
-  top_level_widget.CloseNow();
+  EXPECT_FALSE(top_level_widget->HasCapture());
   WidgetFocusManager::GetInstance()->RemoveFocusChangeListener(&focus_listener);
 }
 
@@ -1846,32 +1866,30 @@
 // mouse events when a different widget grabs capture. Except for Windows,
 // which does not send a synthetic mouse exit.
 TEST_F(WidgetCaptureTest, MAYBE_MouseExitOnCaptureGrab) {
-  Widget widget1;
+  UniqueWidgetPtr widget1 = std::make_unique<Widget>();
   Widget::InitParams params1 =
       CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
-  params1.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
-  widget1.Init(std::move(params1));
+  widget1->Init(std::move(params1));
   MouseView* mouse_view1 =
-      widget1.SetContentsView(std::make_unique<MouseView>());
-  widget1.Show();
-  widget1.SetBounds(gfx::Rect(300, 300));
+      widget1->SetContentsView(std::make_unique<MouseView>());
+  widget1->Show();
+  widget1->SetBounds(gfx::Rect(300, 300));
 
-  Widget widget2;
+  UniqueWidgetPtr widget2 = std::make_unique<Widget>();
   Widget::InitParams params2 =
       CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
-  params2.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
-  widget2.Init(std::move(params2));
-  widget2.Show();
-  widget2.SetBounds(gfx::Rect(400, 0, 300, 300));
+  widget2->Init(std::move(params2));
+  widget2->Show();
+  widget2->SetBounds(gfx::Rect(400, 0, 300, 300));
 
-  ui::test::EventGenerator generator(GetRootWindow(&widget1));
+  ui::test::EventGenerator generator(GetRootWindow(widget1.get()));
   generator.set_current_screen_location(gfx::Point(100, 100));
   generator.MoveMouseBy(0, 0);
 
   EXPECT_EQ(1, mouse_view1->EnteredCalls());
   EXPECT_EQ(0, mouse_view1->ExitedCalls());
 
-  widget2.SetCapture(nullptr);
+  widget2->SetCapture(nullptr);
   EXPECT_EQ(0, mouse_view1->EnteredCalls());
   // On Windows, Chrome doesn't synthesize a separate mouse exited event.
   // Instead, it uses ::TrackMouseEvent to get notified of the mouse leaving.
@@ -1916,18 +1934,17 @@
 // Test that setting capture on widget activation of a non-toplevel widget
 // (e.g. a bubble on Linux) succeeds.
 TEST_F(WidgetCaptureTest, SetCaptureToNonToplevel) {
-  Widget toplevel;
+  UniqueWidgetPtr toplevel = std::make_unique<Widget>();
   Widget::InitParams toplevel_params =
       CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
-  toplevel_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
-  toplevel.Init(std::move(toplevel_params));
-  toplevel.Show();
+  toplevel->Init(std::move(toplevel_params));
+  toplevel->Show();
 
-  Widget* child = new Widget;
+  UniqueWidgetPtr child = std::make_unique<Widget>();
   Widget::InitParams child_params =
       CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
-  child_params.parent = toplevel.GetNativeView();
-  child_params.context = toplevel.GetNativeWindow();
+  child_params.parent = toplevel->GetNativeView();
+  child_params.context = toplevel->GetNativeWindow();
   child->Init(std::move(child_params));
 
   CaptureOnActivationObserver observer;
@@ -1983,39 +2000,37 @@
 // on Windows that it is correctly processed by the widget that doesn't have
 // capture. This behavior is not desired on OSes other than Windows.
 TEST_F(WidgetCaptureTest, MouseEventDispatchedToRightWindow) {
-  MouseEventTrackingWidget widget1;
+  UniqueWidgetPtrT widget1 = std::make_unique<MouseEventTrackingWidget>();
   Widget::InitParams params1 =
       CreateParams(views::Widget::InitParams::TYPE_WINDOW);
-  params1.native_widget = new DesktopNativeWidgetAura(&widget1);
-  params1.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
-  widget1.Init(std::move(params1));
-  widget1.Show();
+  params1.native_widget = new DesktopNativeWidgetAura(widget1.get());
+  widget1->Init(std::move(params1));
+  widget1->Show();
 
-  MouseEventTrackingWidget widget2;
+  UniqueWidgetPtrT widget2 = std::make_unique<MouseEventTrackingWidget>();
   Widget::InitParams params2 =
       CreateParams(views::Widget::InitParams::TYPE_WINDOW);
-  params2.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
-  params2.native_widget = new DesktopNativeWidgetAura(&widget2);
-  widget2.Init(std::move(params2));
-  widget2.Show();
+  params2.native_widget = new DesktopNativeWidgetAura(widget2.get());
+  widget2->Init(std::move(params2));
+  widget2->Show();
 
   // Set capture to widget2 and verity it gets it.
-  widget2.SetCapture(widget2.GetRootView());
-  EXPECT_FALSE(widget1.HasCapture());
-  EXPECT_TRUE(widget2.HasCapture());
+  widget2->SetCapture(widget2->GetRootView());
+  EXPECT_FALSE(widget1->HasCapture());
+  EXPECT_TRUE(widget2->HasCapture());
 
-  widget1.GetAndClearGotMouseEvent();
-  widget2.GetAndClearGotMouseEvent();
+  widget1->GetAndClearGotMouseEvent();
+  widget2->GetAndClearGotMouseEvent();
   // Send a mouse event to the RootWindow associated with |widget1|. Even though
   // |widget2| has capture, |widget1| should still get the event.
   ui::MouseEvent mouse_event(ui::ET_MOUSE_EXITED, gfx::Point(), gfx::Point(),
                              ui::EventTimeForNow(), ui::EF_NONE, ui::EF_NONE);
   ui::EventDispatchDetails details =
-      widget1.GetNativeWindow()->GetHost()->GetEventSink()->OnEventFromSource(
+      widget1->GetNativeWindow()->GetHost()->GetEventSink()->OnEventFromSource(
           &mouse_event);
   ASSERT_FALSE(details.dispatcher_destroyed);
-  EXPECT_TRUE(widget1.GetAndClearGotMouseEvent());
-  EXPECT_FALSE(widget2.GetAndClearGotMouseEvent());
+  EXPECT_TRUE(widget1->GetAndClearGotMouseEvent());
+  EXPECT_FALSE(widget2->GetAndClearGotMouseEvent());
 }
 #endif  // BUILDFLAG(IS_WIN)
 
diff --git a/ui/webui/resources/BUILD.gn b/ui/webui/resources/BUILD.gn
index 2b70d13..483defef 100644
--- a/ui/webui/resources/BUILD.gn
+++ b/ui/webui/resources/BUILD.gn
@@ -232,8 +232,6 @@
   "js/cr/event_target.m.js",
   "js/cr.m.js",
   "js/cr/ui.m.js",
-  "js/cr/ui/drag_wrapper.js",
-  "js/cr/ui/focus_grid.js",
   "js/cr/ui/focus_outline_manager.m.js",
   "js/cr/ui/focus_row.m.js",
   "js/cr/ui/keyboard_shortcut_list.m.js",
@@ -334,6 +332,8 @@
     "cr_elements/cr_tree/cr_tree_item.html.ts",
     "js/assert_ts.ts",
     "js/custom_element.ts",
+    "js/cr/ui/focus_grid.ts",
+    "js/cr/ui/drag_wrapper.ts",
   ]
 
   if (include_polymer) {
diff --git a/ui/webui/resources/js/cr/ui/BUILD.gn b/ui/webui/resources/js/cr/ui/BUILD.gn
index fb79712..21226194 100644
--- a/ui/webui/resources/js/cr/ui/BUILD.gn
+++ b/ui/webui/resources/js/cr/ui/BUILD.gn
@@ -28,6 +28,18 @@
   public_deps = [
     ":preprocess_generated",
     ":preprocess_src",
+    ":preprocess_src_ts",
+  ]
+}
+
+# TS files are passed to a separate target so that they are not listed in the
+# |out_manifest|.
+preprocess_if_expr("preprocess_src_ts") {
+  in_folder = "."
+  out_folder = preprocess_folder
+  in_files = [
+    "drag_wrapper.ts",
+    "focus_grid.ts",
   ]
 }
 
@@ -35,11 +47,7 @@
   in_folder = "./"
   out_folder = "$preprocess_folder"
   out_manifest = "$target_gen_dir/$preprocess_src_manifest"
-  in_files = [
-    "drag_wrapper.js",
-    "focus_grid.js",
-    "store.js",
-  ]
+  in_files = [ "store.js" ]
 
   if (is_chromeos_ash) {
     in_files += [
@@ -304,8 +312,6 @@
 js_type_check("ui_resources_modules") {
   is_polymer3 = true
   deps = [
-    ":drag_wrapper",
-    ":focus_grid",
     ":focus_outline_manager.m",
     ":focus_row.m",
     ":focus_row_behavior.m",
@@ -377,16 +383,6 @@
   }
 }
 
-js_library("drag_wrapper") {
-}
-
-js_library("focus_grid") {
-  deps = [
-    ":focus_row.m",
-    "../..:assert.m",
-  ]
-}
-
 js_library("focus_outline_manager.m") {
   sources =
       [ "$root_gen_dir/ui/webui/resources/js/cr/ui/focus_outline_manager.m.js" ]
diff --git a/ui/webui/resources/js/cr/ui/drag_wrapper.js b/ui/webui/resources/js/cr/ui/drag_wrapper.js
deleted file mode 100644
index 3a586e37..0000000
--- a/ui/webui/resources/js/cr/ui/drag_wrapper.js
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-/**
- * @fileoverview DragWrapper
- * A class for simplifying HTML5 drag and drop. Classes should use this to
- * handle the details of nested drag enters and leaves.
- */
-/** @interface */
-export class DragWrapperDelegate {
-  // TODO(devlin): The only method this "delegate" actually needs is
-  // shouldAcceptDrag(); the rest can be events emitted by the DragWrapper.
-  /**
-   * @param {MouseEvent} e The event for the drag.
-   * @return {boolean} Whether the drag should be accepted. If false,
-   *     subsequent methods (doDrag*) will not be called.
-   */
-  shouldAcceptDrag(e) {}
-
-  /** @param {MouseEvent} e */
-  doDragEnter(e) {}
-
-  /** @param {MouseEvent} e */
-  doDragLeave(e) {}
-
-  /** @param {MouseEvent} e */
-  doDragOver(e) {}
-
-  /** @param {MouseEvent} e */
-  doDrop(e) {}
-}
-
-  /**
-   * Creates a DragWrapper which listens for drag target events on |target| and
-   * delegates event handling to |delegate|.
-   */
-export class DragWrapper {
-  /**
-   * @param {!Element} target
-   * @param {!DragWrapperDelegate} delegate
-   */
-  constructor(target, delegate) {
-    /**
-     * The number of un-paired dragenter events that have fired on |this|.
-     * This is incremented by |onDragEnter_| and decremented by
-     * |onDragLeave_|. This is necessary because dragging over child widgets
-     * will fire additional enter and leave events on |this|. A non-zero value
-     * does not necessarily indicate that |isCurrentDragTarget()| is true.
-     * @private {number}
-     */
-    this.dragEnters_ = 0;
-
-    /** @private {!Element} */
-    this.target_ = target;
-
-    /** @private {!DragWrapperDelegate} */
-    this.delegate_ = delegate;
-
-    target.addEventListener(
-        'dragenter', e => this.onDragEnter_(/** @type {!MouseEvent} */ (e)));
-    target.addEventListener(
-        'dragover', e => this.onDragOver_(/** @type {!MouseEvent} */ (e)));
-    target.addEventListener(
-        'drop', e => this.onDrop_(/** @type {!MouseEvent} */ (e)));
-    target.addEventListener(
-        'dragleave', e => this.onDragLeave_(/** @type {!MouseEvent} */ (e)));
-  }
-
-  /**
-   * Whether the tile page is currently being dragged over with data it can
-   * accept.
-   * @return {boolean}
-   */
-  get isCurrentDragTarget() {
-    return this.target_.classList.contains('drag-target');
-  }
-
-  /**
-   * Delegate for dragenter events fired on |target_|.
-   * @param {!MouseEvent} e A MouseEvent for the drag.
-   * @private
-   */
-  onDragEnter_(e) {
-    if (++this.dragEnters_ === 1) {
-      if (this.delegate_.shouldAcceptDrag(e)) {
-        this.target_.classList.add('drag-target');
-        this.delegate_.doDragEnter(e);
-      }
-    } else {
-      // Sometimes we'll get an enter event over a child element without an
-      // over event following it. In this case we have to still call the
-      // drag over delegate so that we make the necessary updates (one visible
-      // symptom of not doing this is that the cursor's drag state will
-      // flicker during drags).
-      this.onDragOver_(e);
-    }
-  }
-
-  /**
-   * Thunk for dragover events fired on |target_|.
-   * @param {!MouseEvent} e A MouseEvent for the drag.
-   * @private
-   */
-  onDragOver_(e) {
-    if (!this.target_.classList.contains('drag-target')) {
-      return;
-    }
-    this.delegate_.doDragOver(e);
-  }
-
-  /**
-   * Thunk for drop events fired on |target_|.
-   * @param {!MouseEvent} e A MouseEvent for the drag.
-   * @private
-   */
-  onDrop_(e) {
-    this.dragEnters_ = 0;
-    if (!this.target_.classList.contains('drag-target')) {
-      return;
-    }
-    this.target_.classList.remove('drag-target');
-    this.delegate_.doDrop(e);
-  }
-
-  /**
-   * Thunk for dragleave events fired on |target_|.
-   * @param {!MouseEvent} e A MouseEvent for the drag.
-   * @private
-   */
-  onDragLeave_(e) {
-    if (--this.dragEnters_ > 0) {
-      return;
-    }
-
-    this.target_.classList.remove('drag-target');
-    this.delegate_.doDragLeave(e);
-  }
-}
diff --git a/ui/webui/resources/js/cr/ui/drag_wrapper.ts b/ui/webui/resources/js/cr/ui/drag_wrapper.ts
new file mode 100644
index 0000000..b6fd31d
--- /dev/null
+++ b/ui/webui/resources/js/cr/ui/drag_wrapper.ts
@@ -0,0 +1,114 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview DragWrapper
+ * A class for simplifying HTML5 drag and drop. Classes should use this to
+ * handle the details of nested drag enters and leaves.
+ */
+export interface DragWrapperDelegate {
+  // TODO(devlin): The only method this "delegate" actually needs is
+  // shouldAcceptDrag(); the rest can be events emitted by the DragWrapper.
+  /**
+   * @return Whether the drag should be accepted. If false,
+   *     subsequent methods (doDrag*) will not be called.
+   */
+  shouldAcceptDrag(e: MouseEvent): boolean;
+
+  doDragEnter(e: MouseEvent): void;
+
+  doDragLeave(e: MouseEvent): void;
+
+  doDragOver(e: MouseEvent): void;
+
+  doDrop(e: MouseEvent): void;
+}
+
+/**
+ * Creates a DragWrapper which listens for drag target events on |target| and
+ * delegates event handling to |delegate|.
+ */
+export class DragWrapper {
+  /**
+   * The number of un-paired dragenter events that have fired on |this|.
+   * This is incremented by |onDragEnter_| and decremented by
+   * |onDragLeave_|. This is necessary because dragging over child widgets
+   * will fire additional enter and leave events on |this|. A non-zero value
+   * does not necessarily indicate that |isCurrentDragTarget()| is true.
+   */
+  private dragEnters_: number = 0;
+  private target_: HTMLElement;
+  private delegate_: DragWrapperDelegate;
+
+  constructor(target: HTMLElement, delegate: DragWrapperDelegate) {
+    this.target_ = target;
+    this.delegate_ = delegate;
+
+    target.addEventListener('dragenter', e => this.onDragEnter_(e));
+    target.addEventListener('dragover', e => this.onDragOver_(e));
+    target.addEventListener('drop', e => this.onDrop_(e));
+    target.addEventListener('dragleave', e => this.onDragLeave_(e));
+  }
+
+  /**
+   * Whether the tile page is currently being dragged over with data it can
+   * accept.
+   */
+  get isCurrentDragTarget(): boolean {
+    return this.target_.classList.contains('drag-target');
+  }
+
+  /**
+   * Delegate for dragenter events fired on |target_|.
+   */
+  private onDragEnter_(e: MouseEvent) {
+    if (++this.dragEnters_ === 1) {
+      if (this.delegate_.shouldAcceptDrag(e)) {
+        this.target_.classList.add('drag-target');
+        this.delegate_.doDragEnter(e);
+      }
+    } else {
+      // Sometimes we'll get an enter event over a child element without an
+      // over event following it. In this case we have to still call the
+      // drag over delegate so that we make the necessary updates (one visible
+      // symptom of not doing this is that the cursor's drag state will
+      // flicker during drags).
+      this.onDragOver_(e);
+    }
+  }
+
+  /**
+   * Thunk for dragover events fired on |target_|.
+   */
+  private onDragOver_(e: MouseEvent) {
+    if (!this.target_.classList.contains('drag-target')) {
+      return;
+    }
+    this.delegate_.doDragOver(e);
+  }
+
+  /**
+   * Thunk for drop events fired on |target_|.
+   */
+  private onDrop_(e: MouseEvent) {
+    this.dragEnters_ = 0;
+    if (!this.target_.classList.contains('drag-target')) {
+      return;
+    }
+    this.target_.classList.remove('drag-target');
+    this.delegate_.doDrop(e);
+  }
+
+  /**
+   * Thunk for dragleave events fired on |target_|.
+   */
+  private onDragLeave_(e: MouseEvent) {
+    if (--this.dragEnters_ > 0) {
+      return;
+    }
+
+    this.target_.classList.remove('drag-target');
+    this.delegate_.doDragLeave(e);
+  }
+}
diff --git a/ui/webui/resources/js/cr/ui/focus_grid.js b/ui/webui/resources/js/cr/ui/focus_grid.ts
similarity index 67%
rename from ui/webui/resources/js/cr/ui/focus_grid.js
rename to ui/webui/resources/js/cr/ui/focus_grid.ts
index 770121d..a765f72 100644
--- a/ui/webui/resources/js/cr/ui/focus_grid.js
+++ b/ui/webui/resources/js/cr/ui/focus_grid.ts
@@ -4,6 +4,7 @@
 
 // clang-format off
 import {assert} from '../../assert.m.js';
+
 import {FocusRow, FocusRowDelegate} from './focus_row.m.js';
 // clang-format on
 
@@ -27,24 +28,13 @@
  *   focusable  focusable  focusable
  *   focusable  focusable  [focused]  (row: 1, col: 2)
  *   focusable  focusable  focusable
- *
- * @implements {FocusRowDelegate}
  */
-export class FocusGrid {
-  constructor() {
-    /** @type {!Array<!FocusRow>} */
-    this.rows = [];
+export class FocusGrid implements FocusRowDelegate {
+  rows: FocusRow[] = [];
+  private ignoreFocusChange_: boolean = false;
+  private lastFocused_: EventTarget|null = null;
 
-    /** @private {boolean} */
-    this.ignoreFocusChange_ = false;
-
-    /** @private {?EventTarget} */
-    this.lastFocused_ = null;
-  }
-
-  // override
-  // Note: Not using @override because it breaks TypeScript.
-  onFocus(row, e) {
+  onFocus(row: FocusRow, e: Event) {
     if (this.ignoreFocusChange_) {
       this.ignoreFocusChange_ = false;
     } else {
@@ -56,8 +46,7 @@
     });
   }
 
-  // override
-  onKeydown(row, e) {
+  onKeydown(row: FocusRow, e: KeyboardEvent) {
     const rowIndex = this.rows.indexOf(row);
     assert(rowIndex >= 0);
 
@@ -76,10 +65,7 @@
     const rowToFocus = this.rows[newRow];
     if (rowToFocus) {
       this.ignoreFocusChange_ = true;
-      rowToFocus
-          .getEquivalentElement(
-              /** @type {!Element} */ (this.lastFocused_))
-          .focus();
+      rowToFocus.getEquivalentElement(this.lastFocused_ as HTMLElement).focus();
       e.preventDefault();
       return true;
     }
@@ -87,8 +73,7 @@
     return false;
   }
 
-  // override
-  getCustomEquivalent(sampleElement) {
+  getCustomEquivalent(_sampleElement: HTMLElement) {
     return null;
   }
 
@@ -103,12 +88,12 @@
   }
 
   /**
-   * @param {!Element} target A target item to find in this grid.
-   * @return {number} The row index. -1 if not found.
+   * @param target A target item to find in this grid.
+   * @return The row index. -1 if not found.
    */
-  getRowIndexForTarget(target) {
+  getRowIndexForTarget(target: HTMLElement): number {
     for (let i = 0; i < this.rows.length; ++i) {
-      if (this.rows[i].getElements().indexOf(target) >= 0) {
+      if (this.rows[i]!.getElements().indexOf(target) >= 0) {
         return i;
       }
     }
@@ -116,13 +101,13 @@
   }
 
   /**
-   * @param {Element} root An element to search for.
-   * @return {?FocusRow} The row with root of |root| or null.
+   * @param root An element to search for.
+   * @return The row with root of |root| or null.
    */
-  getRowForRoot(root) {
+  getRowForRoot(root: HTMLElement): FocusRow|null {
     for (let i = 0; i < this.rows.length; ++i) {
-      if (this.rows[i].root === root) {
-        return this.rows[i];
+      if (this.rows[i]!.root === root) {
+        return this.rows[i]!;
       }
     }
     return null;
@@ -130,19 +115,19 @@
 
   /**
    * Adds |row| to the end of this list.
-   * @param {!FocusRow} row The row that needs to be added to this grid.
+   * @param row The row that needs to be added to this grid.
    */
-  addRow(row) {
+  addRow(row: FocusRow) {
     this.addRowBefore(row, null);
   }
 
   /**
    * Adds |row| before |nextRow|. If |nextRow| is not in the list or it's
    * null, |row| is added to the end.
-   * @param {!FocusRow} row The row that needs to be added to this grid.
-   * @param {FocusRow} nextRow The row that should follow |row|.
+   * @param row The row that needs to be added to this grid.
+   * @param nextRow The row that should follow |row|.
    */
-  addRowBefore(row, nextRow) {
+  addRowBefore(row: FocusRow, nextRow: FocusRow|null) {
     row.delegate = row.delegate || this;
 
     const nextRowIndex = nextRow ? this.rows.indexOf(nextRow) : -1;
@@ -155,9 +140,9 @@
 
   /**
    * Removes a row from the focus row. No-op if row is not in the grid.
-   * @param {FocusRow} row The row that needs to be removed.
+   * @param row The row that needs to be removed.
    */
-  removeRow(row) {
+  removeRow(row: FocusRow|null) {
     const nextRowIndex = row ? this.rows.indexOf(row) : -1;
     if (nextRowIndex > -1) {
       this.rows.splice(nextRowIndex, 1);
@@ -167,21 +152,21 @@
   /**
    * Makes sure that at least one row is active. Should be called once, after
    * adding all rows to FocusGrid.
-   * @param {number=} preferredRow The row to select if no other row is
+   * @param preferredRow The row to select if no other row is
    *     active. Selects the first item if this is beyond the range of the
    *     grid.
    */
-  ensureRowActive(preferredRow) {
+  ensureRowActive(preferredRow?: number) {
     if (this.rows.length === 0) {
       return;
     }
 
     for (let i = 0; i < this.rows.length; ++i) {
-      if (this.rows[i].isActive()) {
+      if (this.rows[i]!.isActive()) {
         return;
       }
     }
 
-    (this.rows[preferredRow || 0] || this.rows[0]).makeActive(true);
+    (this.rows[preferredRow || 0] || this.rows[0]!).makeActive(true);
   }
 }
diff --git a/ui/webui/resources/js/cr/ui/focus_row.js b/ui/webui/resources/js/cr/ui/focus_row.js
index 213ceda..99abe3f8 100644
--- a/ui/webui/resources/js/cr/ui/focus_row.js
+++ b/ui/webui/resources/js/cr/ui/focus_row.js
@@ -85,8 +85,8 @@
      * that can gain focus is in a shadow DOM. Allowing an override via a
      * function leaves the details of how the element is retrieved to the
      * component.
-     * @param {!Element} element
-     * @return {!Element}
+     * @param {!HTMLElement} element
+     * @return {!HTMLElement}
      */
     static getFocusableElement(element) {
       if (element.getFocusableElement) {
@@ -141,16 +141,18 @@
     }
 
     /**
-     * @param {!Element} sampleElement An element for to find an equivalent for.
-     * @return {!Element} An equivalent element to focus for |sampleElement|.
+     * @param {!HTMLElement} sampleElement An element for to find an equivalent
+     *     for.
+     * @return {!HTMLElement} An equivalent element to focus for
+     *     |sampleElement|.
      * @protected
      */
     getCustomEquivalent(sampleElement) {
-      return /** @type {!Element} */ (assert(this.getFirstFocusable()));
+      return /** @type {!HTMLElement} */ (assert(this.getFirstFocusable()));
     }
 
     /**
-     * @return {!Array<!Element>} All registered elements (regardless of
+     * @return {!Array<!HTMLElement>} All registered elements (regardless of
      *     focusability).
      */
     getElements() {
@@ -160,9 +162,9 @@
 
     /**
      * Find the element that best matches |sampleElement|.
-     * @param {!Element} sampleElement An element from a row of the same type
-     *     which previously held focus.
-     * @return {!Element} The element that best matches sampleElement.
+     * @param {!HTMLElement} sampleElement An element from a row of the same
+     *     type which previously held focus.
+     * @return {!HTMLElement} The element that best matches sampleElement.
      */
     getEquivalentElement(sampleElement) {
       if (this.getFocusableElements().indexOf(sampleElement) >= 0) {
@@ -182,7 +184,7 @@
 
     /**
      * @param {string=} opt_type An optional type to search for.
-     * @return {?Element} The first focusable element with |type|.
+     * @return {?HTMLElement} The first focusable element with |type|.
      */
     getFirstFocusable(opt_type) {
       const element = this.getFocusableElements().find(
@@ -190,7 +192,7 @@
       return element || null;
     }
 
-    /** @return {!Array<!Element>} Registered, focusable elements. */
+    /** @return {!Array<!HTMLElement>} Registered, focusable elements. */
     getFocusableElements() {
       return this.getElements().filter(cr.ui.FocusRow.isFocusable);
     }
@@ -234,7 +236,7 @@
         return;
       }
 
-      const currentTarget = /** @type {!Element} */ (e.currentTarget);
+      const currentTarget = /** @type {!HTMLElement} */ (e.currentTarget);
       if (this.getFocusableElements().indexOf(currentTarget) >= 0) {
         this.makeActive(false);
       }
@@ -273,7 +275,7 @@
     onKeydown_(e) {
       const elements = this.getFocusableElements();
       const currentElement = cr.ui.FocusRow.getFocusableElement(
-          /** @type {!Element} */ (e.currentTarget));
+          /** @type {!HTMLElement} */ (e.currentTarget));
       const elementIndex = elements.indexOf(currentElement);
       assert(elementIndex >= 0);
 
@@ -343,8 +345,8 @@
     onFocus(row, e) {}
 
     /**
-     * @param {!Element} sampleElement An element to find an equivalent for.
-     * @return {?Element} An equivalent element to focus, or null to use the
+     * @param {!HTMLElement} sampleElement An element to find an equivalent for.
+     * @return {?HTMLElement} An equivalent element to focus, or null to use the
      *     default FocusRow element.
      */
     getCustomEquivalent(sampleElement) {}
diff --git a/ui/webui/resources/js/cr/ui/focus_row_behavior.js b/ui/webui/resources/js/cr/ui/focus_row_behavior.js
index 0ee9a91..be59eb9 100644
--- a/ui/webui/resources/js/cr/ui/focus_row_behavior.js
+++ b/ui/webui/resources/js/cr/ui/focus_row_behavior.js
@@ -30,7 +30,7 @@
      * @param {!Event} e
      */
     onFocus(row, e) {
-      const element = /** @type {!Element} */ (e.composedPath()[0]);
+      const element = /** @type {!HTMLElement} */ (e.composedPath()[0]);
       const focusableElement = cr.ui.FocusRow.getFocusableElement(element);
       if (element !== focusableElement) {
         focusableElement.focus();
@@ -119,7 +119,7 @@
         observer: 'focusRowIndexChanged',
       },
 
-      /** @type {Element} */
+      /** @type {HTMLElement} */
       lastFocused: {
         type: Object,
         notify: true,
@@ -394,7 +394,7 @@
       /** @type {number} */
       this.focusRowIndex;
 
-      /** @type {?Element} */
+      /** @type {?HTMLElement} */
       this.lastFocused;
 
       /** @type {number} */
diff --git a/weblayer/browser/overlay_popup_ad_intervention_browsertest.cc b/weblayer/browser/overlay_popup_ad_intervention_browsertest.cc
index 0424fe8..4455d2ba 100644
--- a/weblayer/browser/overlay_popup_ad_intervention_browsertest.cc
+++ b/weblayer/browser/overlay_popup_ad_intervention_browsertest.cc
@@ -134,8 +134,9 @@
   base::test::ScopedFeatureList feature_list_;
 };
 
+// TODO(https://crbug.com/1344280): Test is flaky.
 IN_PROC_BROWSER_TEST_F(OverlayPopupAdViolationBrowserTestWithoutEnforcement,
-                       OverlayPopupAd_NoAdInterventionTriggered) {
+                       DISABLED_OverlayPopupAd_NoAdInterventionTriggered) {
   base::HistogramTester histogram_tester;
 
   GURL url = embedded_test_server()->GetURL(