diff --git a/BUILD.gn b/BUILD.gn
index 21298781..19aee8c 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -24,6 +24,8 @@
 import("//gpu/vulkan/features.gni")
 import("//media/gpu/args.gni")
 import("//media/media_options.gni")
+import("//pdf/features.gni")
+import("//printing/buildflags/buildflags.gni")
 import("//remoting/remoting_enable.gni")
 import("//third_party/closure_compiler/compile_js.gni")
 import("//tools/ipc_fuzzer/ipc_fuzzer.gni")
@@ -163,8 +165,6 @@
       "//ppapi/examples/video_capture",
       "//ppapi/examples/video_decode",
       "//ppapi/examples/video_encode",
-      "//printing:printing_unittests",
-      "//third_party/pdfium/samples:pdfium_test",
       "//third_party/vulkan-deps/spirv-tools/src:SPIRV-Tools",
       "//third_party/vulkan-deps/spirv-tools/src/test/fuzzers",
       "//tools/perf/clear_system_cache",
@@ -272,6 +272,10 @@
 
   deps += root_extra_deps
 
+  if (enable_basic_printing) {
+    deps += [ "//printing:printing_unittests" ]
+  }
+
   if (enable_extensions) {
     deps += [
       "//extensions:extensions_browsertests",
@@ -281,6 +285,10 @@
     ]
   }
 
+  if (enable_pdf) {
+    deps += [ "//third_party/pdfium/samples:pdfium_test" ]
+  }
+
   if (enable_remoting) {
     deps += [ "//remoting:remoting_all" ]
   }
@@ -774,7 +782,6 @@
   if (((is_linux || is_chromeos) && !is_chromecast) ||
       (is_win && use_libfuzzer) || (use_libfuzzer && is_mac)) {
     deps += [
-      "//chrome/services/ipp_parser/public/cpp:fuzzers",
       "//testing/libfuzzer/fuzzers",
       "//third_party/freetype-testing:fuzzers",
       "//third_party/grpc:fuzzers",
@@ -784,8 +791,12 @@
       "//third_party/zlib/contrib/tests/fuzzers",
     ]
 
+    if (is_chromeos_ash) {
+      deps += [ "//chrome/services/ipp_parser/public/cpp:fuzzers" ]
+    }
+
     # TODO(crbug.com/906751): Get the libFuzzer tests working on Windows.
-    # Disable them for now becaue they cause the Windows clang ToT builder to
+    # Disable them for now because they cause the Windows clang ToT builder to
     # fail.
     if (!is_win) {
       deps += [ "//testing/libfuzzer/tests:libfuzzer_tests" ]
diff --git a/DEPS b/DEPS
index 49314c84..558e83f 100644
--- a/DEPS
+++ b/DEPS
@@ -177,7 +177,7 @@
   # luci-go CIPD package version.
   # Make sure the revision is uploaded by infra-packagers builder.
   # https://ci.chromium.org/p/infra-internal/g/infra-packagers/console
-  'luci_go': 'git_revision:5a038afb97f6b77e0fcefe1185317da216fced1f',
+  'luci_go': 'git_revision:22d464e2f8f3bd2bd33f69fe819326d63f881008',
 
   # This can be overridden, e.g. with custom_vars, to build clang from HEAD
   # instead of downloading the prebuilt pinned revision.
@@ -209,7 +209,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': '9e42ec30586dabd7afa3cd5cacf3b234c35b8d24',
+  'skia_revision': 'f519ab8f093b8f308dd537c660b66f000a1de08a',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
@@ -221,7 +221,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': '6f35e36686456ff453e15270b77eb429fc759981',
+  'angle_revision': 'f871545d293f8fd55357eb2bcdaacea3cca9569f',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -288,7 +288,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': 'c87525026921d80cffe27b08408ae318f614ec54',
+  'devtools_frontend_revision': '7b45ea0188a7149d1580f5cbe2452cb563dea5a6',
   # 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.
@@ -966,7 +966,7 @@
   },
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '8c9a5b84cba7e5ebce5d97bea447c9662b1d43e9',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '6b0a611c2c692684f94c0c3629f793feebd16b39',
 
   'src/third_party/devtools-frontend/src':
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
@@ -1349,7 +1349,7 @@
   },
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' + 'f96c36dc3cf3507a03dfeb416c8b5d75abcea85e',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' + '7d6375fd3e2f91b5880195a9c02de2334a3fa0d4',
 
   'src/third_party/perl': {
       'url': Var('chromium_git') + '/chromium/deps/perl.git' + '@' + '6f3e5028eb65d0b4c5fdd792106ac4c84eee1eb3',
@@ -1547,7 +1547,7 @@
   'src/third_party/usrsctp/usrsctplib':
     Var('chromium_git') + '/external/github.com/sctplab/usrsctp' + '@' + '22ba62ffe79c3881581ab430368bf3764d9533eb',
 
-  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@3d799e0e9b08dc14a3efa2e130d288f6ca33d3d0',
+  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@da00e22f60d1d150e6ee46d0e35528d39faaec7d',
 
   'src/third_party/vulkan_memory_allocator':
     Var('chromium_git') + '/external/github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git' + '@' + '732a76d9d3c70d6aa487216495eeb28518349c3a',
@@ -1574,7 +1574,7 @@
     Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + 'a03bb38c0e771a0b404753b8e65250e98719870f',
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + '92bd9020afa45194f6d02dd9deba527a08cef21e',
+    Var('webrtc_git') + '/src.git' + '@' + '567e84726055d891cb66232f7c6a9d555318815c',
 
   'src/third_party/libgifcodec':
      Var('skia_git') + '/libgifcodec' + '@'+  Var('libgifcodec_revision'),
@@ -1601,7 +1601,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/linux-amd64',
-          'version': 'BLHYEUi0wOYe5D_InWXtD8US7l_PMOrKHLuKt16L46QC',
+          'version': 'PXrLb1Vy_W4AEPcc6NqgDemX073vvLhugmMa5syybtcC',
         },
       ],
       'dep_type': 'cipd',
@@ -1611,7 +1611,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/windows-amd64',
-          'version': 'P9wyIW02W7EqtyT9cZJj6g0HOcjRHAhN5KECwmhgRjUC',
+          'version': 'oNxVMtwJ3j0wmiWBudkCQGv4Ke44tfCXvwWJ5wwlIDMC',
         },
       ],
       'dep_type': 'cipd',
@@ -1621,7 +1621,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/mac-amd64',
-          'version': 'qofjOtuSqFXnMKjZ9c7c-oVh9HeWjGW4h4F3y-vrfEkC',
+          'version': '5_KCTT_-CKBlEN-y4_KRfb6Uxn3TzRM9bUuZFIVO5fUC',
         },
       ],
       'dep_type': 'cipd',
@@ -1635,7 +1635,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@0a9e379f114a73a7356a1b70713348034d3bccec',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@adfcd2446f1daa6804db5612b09665c15bfacc88',
     'condition': 'checkout_src_internal',
   },
 
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 032995cb..9159e4ad 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -4663,7 +4663,7 @@
   """Check source code and known ascii text files for Windows style line
   endings.
   """
-  known_text_files = r'.*\.(txt|html|htm|mhtml|py|gyp|gypi|gn|isolate)$'
+  known_text_files = r'.*\.(txt|html|htm|mhtml|py|gyp|gypi|gn|isolate|icon)$'
 
   file_inclusion_pattern = (
     known_text_files,
@@ -4688,6 +4688,16 @@
 
   return []
 
+def CheckIconFilesForLicenseHeaders(input_api, output_api):
+  """Check that .icon files (which are fragments of C++) have license headers.
+  """
+
+  icon_files = (r'.*\.icon$',)
+
+  icons = lambda x: input_api.FilterSourceFile(x, files_to_check=icon_files)
+  return input_api.canned_checks.CheckLicense(
+      input_api, output_api, source_file_filter=icons)
+
 def CheckForUseOfChromeAppsDeprecations(input_api, output_api):
   """Check source code for use of Chrome App technologies being
   deprecated.
diff --git a/android_webview/browser/aw_contents_io_thread_client.cc b/android_webview/browser/aw_contents_io_thread_client.cc
index c3d9898..1e74a63 100644
--- a/android_webview/browser/aw_contents_io_thread_client.cc
+++ b/android_webview/browser/aw_contents_io_thread_client.cc
@@ -375,39 +375,6 @@
       "Android.WebView.ShouldInterceptRequest.InterceptionType", type);
 }
 
-// Record UMA for the custom response status code for the intercepted requests
-// where input stream is null. UMA is recorded only when the status codes and
-// reason phrases are actually valid.
-void RecordResponseStatusCode(
-    JNIEnv* env,
-    const std::unique_ptr<embedder_support::WebResourceResponse>& response) {
-  DCHECK(response);
-  DCHECK(!response->HasInputStream(env));
-
-  int status_code;
-  std::string reason_phrase;
-  bool status_info_valid =
-      response->GetStatusInfo(env, &status_code, &reason_phrase);
-
-  if (!status_info_valid) {
-    // Status code is not necessary set properly in the response,
-    // e.g. Webview's WebResourceResponse(String, String, InputStream) [*]
-    // does not actually set the status code or the reason phrase. In this case
-    // we just record a zero status code.
-    // The other constructor (long version) or the #setStatusCodeAndReasonPhrase
-    // method does actually perform validity checks on status code and reason
-    // phrase arguments.
-    // [*]
-    // https://developer.android.com/reference/android/webkit/WebResourceResponse.html
-    status_code = 0;
-  }
-
-  base::UmaHistogramSparse(
-      "Android.WebView.ShouldInterceptRequest.NullInputStream."
-      "ResponseStatusCode",
-      status_code);
-}
-
 std::unique_ptr<AwWebResourceInterceptResponse> NoInterceptRequest() {
   return nullptr;
 }
@@ -441,15 +408,7 @@
   if (!ret)
     return NoInterceptRequest();
 
-  auto response = std::make_unique<AwWebResourceInterceptResponse>(ret);
-  if (!response->RaisedException(env) && response->HasResponse(env) &&
-      !response->GetResponse(env)->HasInputStream(env)) {
-    // Only record UMA for cases where the input stream is null (see
-    // crbug.com/974273).
-    RecordResponseStatusCode(env, response->GetResponse(env));
-  }
-
-  return response;
+  return std::make_unique<AwWebResourceInterceptResponse>(ret);
 }
 
 }  // namespace
diff --git a/apps/DIR_METADATA b/apps/DIR_METADATA
index 5cc4c774..e2c4d38a 100644
--- a/apps/DIR_METADATA
+++ b/apps/DIR_METADATA
@@ -1,10 +1,10 @@
 # Metadata information for this directory.
 #
 # For more information on DIR_METADATA files, see:
-#   https://source.chromium.org/chromium/infra/infra/+/master:go/src/infra/tools/dirmd/README.md
+#   https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/tools/dirmd/README.md
 #
 # For the schema of this file, see Metadata message:
-#   https://source.chromium.org/chromium/infra/infra/+/master:go/src/infra/tools/dirmd/proto/dir_metadata.proto
+#   https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/tools/dirmd/proto/dir_metadata.proto
 
 monorail {
   component: "Platform>Apps"
diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index c9cc78e..cb4cd6d 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -919,28 +919,28 @@
     "system/accessibility/floating_menu_button.h",
     "system/accessibility/floating_menu_utils.cc",
     "system/accessibility/floating_menu_utils.h",
-    "system/accessibility/select_to_speak_constants.h",
-    "system/accessibility/select_to_speak_menu_bubble_controller.cc",
-    "system/accessibility/select_to_speak_menu_bubble_controller.h",
-    "system/accessibility/select_to_speak_menu_view.cc",
-    "system/accessibility/select_to_speak_menu_view.h",
-    "system/accessibility/select_to_speak_metrics_utils.h",
-    "system/accessibility/select_to_speak_speed_bubble_controller.cc",
-    "system/accessibility/select_to_speak_speed_bubble_controller.h",
-    "system/accessibility/select_to_speak_speed_view.cc",
-    "system/accessibility/select_to_speak_speed_view.h",
-    "system/accessibility/select_to_speak_tray.cc",
-    "system/accessibility/select_to_speak_tray.h",
-    "system/accessibility/switch_access_back_button_bubble_controller.cc",
-    "system/accessibility/switch_access_back_button_bubble_controller.h",
-    "system/accessibility/switch_access_back_button_view.cc",
-    "system/accessibility/switch_access_back_button_view.h",
-    "system/accessibility/switch_access_menu_bubble_controller.cc",
-    "system/accessibility/switch_access_menu_bubble_controller.h",
-    "system/accessibility/switch_access_menu_button.cc",
-    "system/accessibility/switch_access_menu_button.h",
-    "system/accessibility/switch_access_menu_view.cc",
-    "system/accessibility/switch_access_menu_view.h",
+    "system/accessibility/select_to_speak/select_to_speak_constants.h",
+    "system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.cc",
+    "system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.h",
+    "system/accessibility/select_to_speak/select_to_speak_menu_view.cc",
+    "system/accessibility/select_to_speak/select_to_speak_menu_view.h",
+    "system/accessibility/select_to_speak/select_to_speak_metrics_utils.h",
+    "system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller.cc",
+    "system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller.h",
+    "system/accessibility/select_to_speak/select_to_speak_speed_view.cc",
+    "system/accessibility/select_to_speak/select_to_speak_speed_view.h",
+    "system/accessibility/select_to_speak/select_to_speak_tray.cc",
+    "system/accessibility/select_to_speak/select_to_speak_tray.h",
+    "system/accessibility/switch_access/switch_access_back_button_bubble_controller.cc",
+    "system/accessibility/switch_access/switch_access_back_button_bubble_controller.h",
+    "system/accessibility/switch_access/switch_access_back_button_view.cc",
+    "system/accessibility/switch_access/switch_access_back_button_view.h",
+    "system/accessibility/switch_access/switch_access_menu_bubble_controller.cc",
+    "system/accessibility/switch_access/switch_access_menu_bubble_controller.h",
+    "system/accessibility/switch_access/switch_access_menu_button.cc",
+    "system/accessibility/switch_access/switch_access_menu_button.h",
+    "system/accessibility/switch_access/switch_access_menu_view.cc",
+    "system/accessibility/switch_access/switch_access_menu_view.h",
     "system/accessibility/tray_accessibility.cc",
     "system/accessibility/tray_accessibility.h",
     "system/accessibility/unified_accessibility_detailed_view_controller.cc",
@@ -2232,11 +2232,11 @@
     "system/accessibility/autoclick_menu_bubble_controller_unittest.cc",
     "system/accessibility/dictation_button_tray_unittest.cc",
     "system/accessibility/floating_accessibility_controller_unittest.cc",
-    "system/accessibility/select_to_speak_menu_bubble_controller_unittest.cc",
-    "system/accessibility/select_to_speak_speed_bubble_controller_unittest.cc",
-    "system/accessibility/select_to_speak_tray_unittest.cc",
-    "system/accessibility/switch_access_back_button_bubble_controller_unittest.cc",
-    "system/accessibility/switch_access_menu_bubble_controller_unittest.cc",
+    "system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller_unittest.cc",
+    "system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller_unittest.cc",
+    "system/accessibility/select_to_speak/select_to_speak_tray_unittest.cc",
+    "system/accessibility/switch_access/switch_access_back_button_bubble_controller_unittest.cc",
+    "system/accessibility/switch_access/switch_access_menu_bubble_controller_unittest.cc",
     "system/accessibility/tray_accessibility_unittest.cc",
     "system/audio/unified_audio_detailed_view_controller_unittest.cc",
     "system/bluetooth/bluetooth_notification_controller_unittest.cc",
@@ -2703,6 +2703,8 @@
     "metrics/task_switch_time_tracker_test_api.h",
     "metrics/user_metrics_recorder_test_api.cc",
     "metrics/user_metrics_recorder_test_api.h",
+    "projector/test/mock_projector_client.cc",
+    "projector/test/mock_projector_client.h",
     "projector/test/mock_projector_metadata_controller.cc",
     "projector/test/mock_projector_metadata_controller.h",
     "projector/test/mock_projector_ui_controller.cc",
diff --git a/ash/accelerators/accelerator_controller_impl.cc b/ash/accelerators/accelerator_controller_impl.cc
index 0aa44df..85b0d33 100644
--- a/ash/accelerators/accelerator_controller_impl.cc
+++ b/ash/accelerators/accelerator_controller_impl.cc
@@ -2505,6 +2505,14 @@
       HandleTopWindowMinimizeOnBack();
       break;
   }
+
+  // Reset any in progress composition.
+  if (::features::IsImprovedKeyboardShortcutsEnabled()) {
+    auto* input_method =
+        Shell::Get()->window_tree_host_manager()->input_method();
+
+    input_method->CancelComposition(input_method->GetTextInputClient());
+  }
 }
 
 bool AcceleratorControllerImpl::ShouldActionConsumeKeyEvent(
diff --git a/ash/accelerators/accelerator_controller_unittest.cc b/ash/accelerators/accelerator_controller_unittest.cc
index d95b046d..aec6d76 100644
--- a/ash/accelerators/accelerator_controller_unittest.cc
+++ b/ash/accelerators/accelerator_controller_unittest.cc
@@ -79,6 +79,8 @@
 #include "ui/base/ime/chromeos/fake_ime_keyboard.h"
 #include "ui/base/ime/chromeos/ime_keyboard.h"
 #include "ui/base/ime/chromeos/mock_input_method_manager.h"
+#include "ui/base/ime/init/input_method_factory.h"
+#include "ui/base/ime/mock_input_method.h"
 #include "ui/base/ui_base_features.h"
 #include "ui/display/manager/display_manager.h"
 #include "ui/display/screen.h"
@@ -2143,6 +2145,62 @@
   EXPECT_TRUE(ProcessInController(accelerator));
 }
 
+class AcceleratorControllerInputMethodTest : public AcceleratorControllerTest {
+ public:
+  AcceleratorControllerInputMethodTest() = default;
+  ~AcceleratorControllerInputMethodTest() override = default;
+
+  class AcceleratorMockInputMethod : public ui::MockInputMethod {
+   public:
+    AcceleratorMockInputMethod() : ui::MockInputMethod(nullptr) {}
+    void CancelComposition(const ui::TextInputClient* client) override {
+      cancel_composition_call_count++;
+    }
+
+    uint32_t cancel_composition_call_count = 0;
+  };
+
+  void SetUp() override {
+    scoped_feature_list_.InitAndEnableFeature(
+        ::features::kImprovedKeyboardShortcuts);
+
+    // Setup the mock input method to capture the calls to
+    // |CancelCompositionAfterAccelerator|. Ownersship is passed to
+    // ui::SetUpInputMethodForTesting().
+    mock_input_ = new AcceleratorMockInputMethod();
+    ui::SetUpInputMethodForTesting(mock_input_);
+    AcceleratorControllerTest::SetUp();
+  }
+
+ protected:
+  AcceleratorMockInputMethod* mock_input_ = nullptr;  // Not owned.
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+// In some layouts positional accelerators can be on dead/compose keys. To
+// ensure that the input method is not left in a partially composed state
+// the composition state is reset when an accelerator is matched.
+TEST_F(AcceleratorControllerInputMethodTest, AcceleratorClearsComposition) {
+  EXPECT_EQ(0u, mock_input_->cancel_composition_call_count);
+
+  // An acclerator that isn't recognized will not cause composition to be
+  // cancelled.
+  ui::Accelerator unknown_accelerator(ui::VKEY_OEM_MINUS, ui::EF_NONE);
+  EXPECT_FALSE(controller_->IsRegistered(unknown_accelerator));
+  EXPECT_FALSE(ProcessInController(unknown_accelerator));
+  EXPECT_EQ(0u, mock_input_->cancel_composition_call_count);
+
+  // A matching accelerator should cause CancelCompositionAfterAccelerator() to
+  // be called.
+  ui::Accelerator accelerator(ui::VKEY_OEM_MINUS,
+                              ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN);
+  EXPECT_TRUE(controller_->IsRegistered(accelerator));
+  EXPECT_TRUE(ProcessInController(accelerator));
+  EXPECT_EQ(1u, mock_input_->cancel_composition_call_count);
+}
+
 namespace {
 
 // TODO(crbug.com/1179893): Remove once the feature is enabled permantently.
diff --git a/ash/accessibility/accessibility_controller_impl.cc b/ash/accessibility/accessibility_controller_impl.cc
index b82f8de4..0862ca82 100644
--- a/ash/accessibility/accessibility_controller_impl.cc
+++ b/ash/accessibility/accessibility_controller_impl.cc
@@ -38,8 +38,8 @@
 #include "ash/strings/grit/ash_strings.h"
 #include "ash/system/accessibility/accessibility_feature_disable_dialog.h"
 #include "ash/system/accessibility/floating_accessibility_controller.h"
-#include "ash/system/accessibility/select_to_speak_menu_bubble_controller.h"
-#include "ash/system/accessibility/switch_access_menu_bubble_controller.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.h"
+#include "ash/system/accessibility/switch_access/switch_access_menu_bubble_controller.h"
 #include "ash/system/power/backlights_forced_off_setter.h"
 #include "ash/system/power/power_status.h"
 #include "ash/system/power/scoped_backlights_forced_off.h"
diff --git a/ash/ambient/ambient_photo_controller.cc b/ash/ambient/ambient_photo_controller.cc
index a4ddbce..30d0fec 100644
--- a/ash/ambient/ambient_photo_controller.cc
+++ b/ash/ambient/ambient_photo_controller.cc
@@ -428,6 +428,7 @@
       num_callbacks,
       base::BindOnce(&AmbientPhotoController::OnAllPhotoDecoded,
                      weak_factory_.GetWeakPtr(), from_downloading,
+                     cache_entry.details ? *cache_entry.details : std::string(),
                      /*hash=*/base::SHA1HashString(*cache_entry.image)));
 
   DecodePhotoRawData(from_downloading,
@@ -465,6 +466,7 @@
 }
 
 void AmbientPhotoController::OnAllPhotoDecoded(bool from_downloading,
+                                               const std::string& details,
                                                const std::string& hash) {
   if (image_.isNull()) {
     LOG(WARNING) << "Image decoding failed";
@@ -489,8 +491,7 @@
   PhotoWithDetails detailed_photo;
   detailed_photo.photo = image_;
   detailed_photo.related_photo = related_image_;
-  if (cache_entry_.details)
-    detailed_photo.details = *cache_entry_.details;
+  detailed_photo.details = details;
   detailed_photo.hash = hash;
 
   ResetImageData();
diff --git a/ash/ambient/ambient_photo_controller.h b/ash/ambient/ambient_photo_controller.h
index f463fd023..1880513 100644
--- a/ash/ambient/ambient_photo_controller.h
+++ b/ash/ambient/ambient_photo_controller.h
@@ -134,7 +134,9 @@
                       base::RepeatingClosure on_done,
                       const gfx::ImageSkia& image);
 
-  void OnAllPhotoDecoded(bool from_downloading, const std::string& hash);
+  void OnAllPhotoDecoded(bool from_downloading,
+                         const std::string& details,
+                         const std::string& hash);
 
   void StartDownloadingWeatherConditionIcon(
       const absl::optional<WeatherInfo>& weather_info);
diff --git a/ash/ambient/ambient_photo_controller_unittest.cc b/ash/ambient/ambient_photo_controller_unittest.cc
index 94f4775b..f2853c59 100644
--- a/ash/ambient/ambient_photo_controller_unittest.cc
+++ b/ash/ambient/ambient_photo_controller_unittest.cc
@@ -152,6 +152,22 @@
   photo_controller()->StopScreenUpdate();
 }
 
+// Tests that image details is correctly set.
+TEST_F(AmbientPhotoControllerTest, ShouldSetDetailsCorrectly) {
+  // Start to refresh images.
+  photo_controller()->StartScreenUpdate();
+  FastForwardToNextImage();
+  PhotoWithDetails image =
+      photo_controller()->ambient_backend_model()->GetNextImage();
+  EXPECT_FALSE(image.IsNull());
+
+  // Fake details defined in fake_ambient_backend_controller_impl.cc.
+  EXPECT_EQ(image.details, "fake-photo-attribution");
+
+  // Stop to refresh images.
+  photo_controller()->StopScreenUpdate();
+}
+
 // Test that image is saved.
 TEST_F(AmbientPhotoControllerTest, ShouldSaveImagesOnDisk) {
   // Start to refresh images. It will download two images immediately and write
@@ -255,6 +271,28 @@
   EXPECT_FALSE(image.IsNull());
 }
 
+// Test that image details is read from disk.
+TEST_F(AmbientPhotoControllerTest, ShouldPopulateDetailsWhenReadFromCache) {
+  FetchImage();
+  FastForwardToNextImage();
+  // Topics is empty. Will read from cache, which is empty.
+  auto image = photo_controller()->ambient_backend_model()->GetCurrentImage();
+  EXPECT_TRUE(image.IsNull());
+
+  // Save a file to check if it gets read for display.
+  std::string data("cached image");
+  std::string details("image details");
+  WriteCacheDataBlocking(/*cache_index=*/0, &data, &details);
+
+  // Reset variables in photo controller.
+  photo_controller()->StopScreenUpdate();
+  FetchImage();
+  FastForwardToNextImage();
+  image = photo_controller()->ambient_backend_model()->GetCurrentImage();
+  EXPECT_FALSE(image.IsNull());
+  EXPECT_EQ(image.details, details);
+}
+
 // Test that image is read from disk when image decoding failed.
 TEST_F(AmbientPhotoControllerTest, ShouldReadCacheWhenImageDecodingFailed) {
   SetDecodePhotoImage(gfx::ImageSkia());
diff --git a/ash/app_list/BUILD.gn b/ash/app_list/BUILD.gn
index 8da11b1..2196150 100644
--- a/ash/app_list/BUILD.gn
+++ b/ash/app_list/BUILD.gn
@@ -28,6 +28,12 @@
     "bubble/app_list_bubble_event_filter.h",
     "bubble/app_list_bubble_view.cc",
     "bubble/app_list_bubble_view.h",
+    "bubble/bubble_apps_page.cc",
+    "bubble/bubble_apps_page.h",
+    "bubble/bubble_assistant_page.cc",
+    "bubble/bubble_assistant_page.h",
+    "bubble/bubble_search_page.cc",
+    "bubble/bubble_search_page.h",
     "home_launcher_animation_info.h",
     "paged_view_structure.cc",
     "paged_view_structure.h",
diff --git a/ash/app_list/bubble/app_list_bubble_unittest.cc b/ash/app_list/bubble/app_list_bubble_unittest.cc
index 8056fdc..60ddd33f 100644
--- a/ash/app_list/bubble/app_list_bubble_unittest.cc
+++ b/ash/app_list/bubble/app_list_bubble_unittest.cc
@@ -14,6 +14,7 @@
 #include "ash/shelf/shelf_navigation_widget.h"
 #include "ash/shell.h"
 #include "ash/test/ash_test_base.h"
+#include "base/run_loop.h"
 #include "base/test/scoped_feature_list.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/display/display.h"
@@ -43,6 +44,13 @@
   }
   ~AppListBubbleTest() override = default;
 
+  // testing::Test:
+  void SetUp() override {
+    AshTestBase::SetUp();
+    // Use a realistic screen size so the default size bubble will fit.
+    UpdateDisplay("1366x768");
+  }
+
   // Returns the AppListBubble instance. Use this instead of creating a new
   // AppListBubble instance in each test to avoid situations where two bubbles
   // exist at the same time (the per-test one and the "production" one).
diff --git a/ash/app_list/bubble/app_list_bubble_view.cc b/ash/app_list/bubble/app_list_bubble_view.cc
index 08e2f39..65ca296 100644
--- a/ash/app_list/bubble/app_list_bubble_view.cc
+++ b/ash/app_list/bubble/app_list_bubble_view.cc
@@ -6,6 +6,9 @@
 
 #include <memory>
 
+#include "ash/app_list/bubble/bubble_apps_page.h"
+#include "ash/app_list/bubble/bubble_assistant_page.h"
+#include "ash/app_list/bubble/bubble_search_page.h"
 #include "ash/public/cpp/shelf_types.h"
 #include "ash/public/cpp/shell_window_ids.h"
 #include "ash/shelf/shelf.h"
@@ -16,7 +19,7 @@
 #include "ui/display/screen.h"
 #include "ui/gfx/geometry/rect.h"
 #include "ui/views/bubble/bubble_border.h"
-#include "ui/views/controls/label.h"
+#include "ui/views/controls/button/md_text_button.h"
 #include "ui/views/controls/textfield/textfield.h"
 #include "ui/views/layout/box_layout.h"
 
@@ -76,12 +79,38 @@
       std::make_unique<BoxLayout>(BoxLayout::Orientation::kVertical));
   layout->set_cross_axis_alignment(BoxLayout::CrossAxisAlignment::kStretch);
 
-  // TODO(https://crbug.com/1204551): Create real contents.
-  AddChildView(std::make_unique<views::Label>(u"Placeholder"));
+  // TODO(https://crbug.com/1204551): Replace with real search box.
   auto* textfield = AddChildView(std::make_unique<views::Textfield>());
   SetInitiallyFocusedView(textfield);
+
+  // TODO(https://crbug.com/1204551): Remove when search box is hooked up.
+  AddChildView(std::make_unique<views::MdTextButton>(
+      base::BindRepeating(&AppListBubbleView::FlipPage, base::Unretained(this)),
+      u"Flip page"));
+
+  apps_page_ = AddChildView(std::make_unique<BubbleAppsPage>());
+
+  search_page_ = AddChildView(std::make_unique<BubbleSearchPage>());
+  search_page_->SetVisible(false);
+
+  assistant_page_ = AddChildView(std::make_unique<BubbleAssistantPage>());
+  assistant_page_->SetVisible(false);
 }
 
 AppListBubbleView::~AppListBubbleView() = default;
 
+gfx::Size AppListBubbleView::CalculatePreferredSize() const {
+  constexpr gfx::Size kDefaultSizeDips(600, 550);
+  // TODO(https://crbug.com/1210522): Adjust size based on screen resolution.
+  return kDefaultSizeDips;
+}
+
+void AppListBubbleView::FlipPage() {
+  ++visible_page_;
+  visible_page_ %= 3;
+  apps_page_->SetVisible(visible_page_ == 0);
+  search_page_->SetVisible(visible_page_ == 1);
+  assistant_page_->SetVisible(visible_page_ == 2);
+}
+
 }  // namespace ash
diff --git a/ash/app_list/bubble/app_list_bubble_view.h b/ash/app_list/bubble/app_list_bubble_view.h
index 7adfdd2..7f64252 100644
--- a/ash/app_list/bubble/app_list_bubble_view.h
+++ b/ash/app_list/bubble/app_list_bubble_view.h
@@ -14,6 +14,9 @@
 
 namespace ash {
 
+class BubbleAppsPage;
+class BubbleAssistantPage;
+class BubbleSearchPage;
 enum class ShelfAlignment;
 
 // Contains the views for the bubble version of the launcher.
@@ -25,6 +28,21 @@
   AppListBubbleView(const AppListBubbleView&) = delete;
   AppListBubbleView& operator=(const AppListBubbleView&) = delete;
   ~AppListBubbleView() override;
+
+  // views::View:
+  gfx::Size CalculatePreferredSize() const override;
+
+ private:
+  // Flips between the apps, search and assistant pages.
+  // TODO(https://crbug.com/1204551): Delete this when search box is hooked up.
+  void FlipPage();
+
+  // TODO(https://crbug.com/1204551): Delete this when search box is hooked up.
+  int visible_page_ = 0;
+
+  BubbleAppsPage* apps_page_ = nullptr;
+  BubbleSearchPage* search_page_ = nullptr;
+  BubbleAssistantPage* assistant_page_ = nullptr;
 };
 
 }  // namespace ash
diff --git a/ash/app_list/bubble/bubble_apps_page.cc b/ash/app_list/bubble/bubble_apps_page.cc
new file mode 100644
index 0000000..59837e0
--- /dev/null
+++ b/ash/app_list/bubble/bubble_apps_page.cc
@@ -0,0 +1,86 @@
+// 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 "ash/app_list/bubble/bubble_apps_page.h"
+
+#include <limits>
+#include <memory>
+#include <utility>
+
+#include "ui/gfx/text_constants.h"
+#include "ui/views/controls/label.h"
+#include "ui/views/controls/scroll_view.h"
+#include "ui/views/controls/textfield/textfield.h"
+#include "ui/views/layout/box_layout.h"
+
+using views::BoxLayout;
+
+namespace ash {
+
+BubbleAppsPage::BubbleAppsPage() {
+  SetUseDefaultFillLayout(true);
+
+  // The entire page scrolls.
+  auto* scroll = AddChildView(std::make_unique<views::ScrollView>());
+  scroll->ClipHeightTo(0, std::numeric_limits<int>::max());
+  scroll->SetDrawOverflowIndicator(false);
+  scroll->SetHorizontalScrollBarMode(
+      views::ScrollView::ScrollBarMode::kDisabled);
+
+  auto scroll_contents = std::make_unique<views::View>();
+  scroll_contents->SetLayoutManager(
+      std::make_unique<BoxLayout>(BoxLayout::Orientation::kVertical));
+
+  // TODO(https://crbug.com/1204551): Localized strings.
+  // TODO(https://crbug.com/1204551): Styling.
+  auto* continue_label = scroll_contents->AddChildView(
+      std::make_unique<views::Label>(u"Continue"));
+  continue_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
+
+  auto* continue_section =
+      scroll_contents->AddChildView(std::make_unique<views::View>());
+  // TODO(https://crbug.com/1204551): Extract SimpleGridLayout from
+  // HoldingSpaceItemChipsContainer and use it here.
+  const int kContinueSpacing = 16;
+  auto* continue_layout =
+      continue_section->SetLayoutManager(std::make_unique<BoxLayout>(
+          BoxLayout::Orientation::kVertical, gfx::Insets(), kContinueSpacing));
+  continue_layout->set_cross_axis_alignment(
+      BoxLayout::CrossAxisAlignment::kStretch);
+  for (int i = 0; i < 4; ++i) {
+    continue_section->AddChildView(std::make_unique<views::Label>(u"Task"));
+  }
+
+  // TODO(https://crbug.com/1204551): Replace with real recent apps view.
+  auto* recent_apps =
+      scroll_contents->AddChildView(std::make_unique<views::View>());
+  const int kRecentAppsSpacing = 16;
+  auto* recent_apps_layout =
+      recent_apps->SetLayoutManager(std::make_unique<views::BoxLayout>(
+          views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
+          kRecentAppsSpacing));
+  recent_apps_layout->set_main_axis_alignment(
+      views::BoxLayout::MainAxisAlignment::kCenter);
+  for (int i = 0; i < 5; ++i) {
+    recent_apps->AddChildView(std::make_unique<views::Label>(u"Recent"));
+  }
+
+  // TODO(https://crbug.com/1204551): Replace with real apps grid. For now,
+  // create enough labels to force the scroll view to scroll.
+  auto* all_apps =
+      scroll_contents->AddChildView(std::make_unique<views::View>());
+  const int kAllAppsSpacing = 16;
+  all_apps->SetLayoutManager(std::make_unique<views::BoxLayout>(
+      views::BoxLayout::Orientation::kVertical, gfx::Insets(),
+      kAllAppsSpacing));
+  for (int i = 0; i < 20; ++i) {
+    all_apps->AddChildView(std::make_unique<views::Label>(u"App"));
+  }
+
+  scroll->SetContents(std::move(scroll_contents));
+}
+
+BubbleAppsPage::~BubbleAppsPage() = default;
+
+}  // namespace ash
diff --git a/ash/app_list/bubble/bubble_apps_page.h b/ash/app_list/bubble/bubble_apps_page.h
new file mode 100644
index 0000000..bff54a64
--- /dev/null
+++ b/ash/app_list/bubble/bubble_apps_page.h
@@ -0,0 +1,28 @@
+// 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 ASH_APP_LIST_BUBBLE_BUBBLE_APPS_PAGE_H_
+#define ASH_APP_LIST_BUBBLE_BUBBLE_APPS_PAGE_H_
+
+#include "ash/ash_export.h"
+#include "ui/views/view.h"
+
+namespace ash {
+
+// The default page for the app list bubble / clamshell launcher. Contains a
+// scroll view with:
+// - Continue section with recent tasks and recent apps
+// - Grid of all apps
+// Does not include the search box, which is owned by a parent view.
+class ASH_EXPORT BubbleAppsPage : public views::View {
+ public:
+  BubbleAppsPage();
+  BubbleAppsPage(const BubbleAppsPage&) = delete;
+  BubbleAppsPage& operator=(const BubbleAppsPage&) = delete;
+  ~BubbleAppsPage() override;
+};
+
+}  // namespace ash
+
+#endif  // ASH_APP_LIST_BUBBLE_BUBBLE_APPS_PAGE_H_
diff --git a/ash/app_list/bubble/bubble_assistant_page.cc b/ash/app_list/bubble/bubble_assistant_page.cc
new file mode 100644
index 0000000..6079c70
--- /dev/null
+++ b/ash/app_list/bubble/bubble_assistant_page.cc
@@ -0,0 +1,32 @@
+// 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 "ash/app_list/bubble/bubble_assistant_page.h"
+
+#include <memory>
+#include <utility>
+
+#include "ui/views/controls/label.h"
+#include "ui/views/layout/box_layout.h"
+
+using views::BoxLayout;
+
+namespace ash {
+
+BubbleAssistantPage::BubbleAssistantPage() {
+  SetLayoutManager(
+      std::make_unique<BoxLayout>(BoxLayout::Orientation::kVertical));
+
+  // TODO(https://crbug.com/1204551): Sort out whether this view needs its own
+  // search box, or if it can use the one owned by the parent. The assistant
+  // page in the tablet launcher owns its own search box, but that may be a side
+  // effect of animations or the historical need to host assistant in a widget.
+
+  // TODO(https://crbug.com/1204551): Embed the assistant.
+  AddChildView(std::make_unique<views::Label>(u"Assistant"));
+}
+
+BubbleAssistantPage::~BubbleAssistantPage() = default;
+
+}  // namespace ash
diff --git a/ash/app_list/bubble/bubble_assistant_page.h b/ash/app_list/bubble/bubble_assistant_page.h
new file mode 100644
index 0000000..ab3c2f82
--- /dev/null
+++ b/ash/app_list/bubble/bubble_assistant_page.h
@@ -0,0 +1,24 @@
+// 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 ASH_APP_LIST_BUBBLE_BUBBLE_ASSISTANT_PAGE_H_
+#define ASH_APP_LIST_BUBBLE_BUBBLE_ASSISTANT_PAGE_H_
+
+#include "ash/ash_export.h"
+#include "ui/views/view.h"
+
+namespace ash {
+
+// The assistant page for the app list bubble / clamshell launcher.
+class ASH_EXPORT BubbleAssistantPage : public views::View {
+ public:
+  BubbleAssistantPage();
+  BubbleAssistantPage(const BubbleAssistantPage&) = delete;
+  BubbleAssistantPage& operator=(const BubbleAssistantPage&) = delete;
+  ~BubbleAssistantPage() override;
+};
+
+}  // namespace ash
+
+#endif  // ASH_APP_LIST_BUBBLE_BUBBLE_ASSISTANT_PAGE_H_
diff --git a/ash/app_list/bubble/bubble_search_page.cc b/ash/app_list/bubble/bubble_search_page.cc
new file mode 100644
index 0000000..e00e4b7f
--- /dev/null
+++ b/ash/app_list/bubble/bubble_search_page.cc
@@ -0,0 +1,49 @@
+// 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 "ash/app_list/bubble/bubble_search_page.h"
+
+#include <limits>
+#include <memory>
+#include <utility>
+
+#include "ui/views/controls/label.h"
+#include "ui/views/controls/scroll_view.h"
+#include "ui/views/layout/box_layout.h"
+
+using views::BoxLayout;
+
+namespace ash {
+
+BubbleSearchPage::BubbleSearchPage() {
+  SetUseDefaultFillLayout(true);
+
+  // The entire page scrolls.
+  auto* scroll = AddChildView(std::make_unique<views::ScrollView>());
+  scroll->ClipHeightTo(0, std::numeric_limits<int>::max());
+  scroll->SetDrawOverflowIndicator(false);
+  scroll->SetHorizontalScrollBarMode(
+      views::ScrollView::ScrollBarMode::kDisabled);
+
+  auto scroll_contents = std::make_unique<views::View>();
+  scroll_contents->SetLayoutManager(
+      std::make_unique<BoxLayout>(BoxLayout::Orientation::kVertical));
+
+  // TODO(https://crbug.com/1204551): Replace with real search results. For now,
+  // create enough labels to force the scroll view to scroll.
+  auto* results =
+      scroll_contents->AddChildView(std::make_unique<views::View>());
+  const int kSpacing = 16;
+  results->SetLayoutManager(std::make_unique<views::BoxLayout>(
+      views::BoxLayout::Orientation::kVertical, gfx::Insets(), kSpacing));
+  for (int i = 0; i < 20; ++i) {
+    results->AddChildView(std::make_unique<views::Label>(u"Result"));
+  }
+
+  scroll->SetContents(std::move(scroll_contents));
+}
+
+BubbleSearchPage::~BubbleSearchPage() = default;
+
+}  // namespace ash
diff --git a/ash/app_list/bubble/bubble_search_page.h b/ash/app_list/bubble/bubble_search_page.h
new file mode 100644
index 0000000..d55acbb
--- /dev/null
+++ b/ash/app_list/bubble/bubble_search_page.h
@@ -0,0 +1,26 @@
+// 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 ASH_APP_LIST_BUBBLE_BUBBLE_SEARCH_PAGE_H_
+#define ASH_APP_LIST_BUBBLE_BUBBLE_SEARCH_PAGE_H_
+
+#include "ash/ash_export.h"
+#include "ui/views/view.h"
+
+namespace ash {
+
+// The search results page for the app list bubble / clamshell launcher.
+// Contains a scrolling list of search results. Does not include the search box,
+// which is owned by a parent view.
+class ASH_EXPORT BubbleSearchPage : public views::View {
+ public:
+  BubbleSearchPage();
+  BubbleSearchPage(const BubbleSearchPage&) = delete;
+  BubbleSearchPage& operator=(const BubbleSearchPage&) = delete;
+  ~BubbleSearchPage() override;
+};
+
+}  // namespace ash
+
+#endif  // ASH_APP_LIST_BUBBLE_BUBBLE_SEARCH_PAGE_H_
diff --git a/ash/ash_strings.grd b/ash/ash_strings.grd
index 8fe989e1..737c272 100644
--- a/ash/ash_strings.grd
+++ b/ash/ash_strings.grd
@@ -3591,6 +3591,93 @@
       <message name="IDS_ASH_SHIMLESS_RMA_APP_TITLE" translateable="false" desc="The title of shimless RMA flow SWA. Shimless RMA guides repair technicians through step-by-step repair flow.">
         Shimless RMA
       </message>
+
+      <!-- Strings for Projector-->
+
+      <message name="IDS_KEY_IDEA_BUTTON" desc="The name for the key idea button.">
+        Add key idea
+      </message>
+
+      <message name="IDS_LASER_POINTER_BUTTON" desc="The name for the laser pointer button.">
+        Laser pointer
+      </message>
+
+      <message name="IDS_MARKER_BUTTON" desc="The name for the marker button.">
+        Marker
+      </message>
+
+      <message name="IDS_INK_PEN_BUTTON" desc="The name for the ink pen button.">
+        Ink pen
+      </message>
+
+      <message name="IDS_MARKER_PEN_BUTTON" desc="The name for the marker pen button.">
+        Marker pen
+      </message>
+
+      <message name="IDS_MARKER_COLOR_BUTTON" desc="The name for the marker color button.">
+        <ph name="COLOR_PARAMETER">$1<ex>blue</ex></ph> marker color
+      </message>
+
+      <message name="IDS_UNDO_BUTTON" desc="The name for the undo button.">
+        Undo
+      </message>
+
+      <message name="IDS_CLEAR_ALL_MARKERS_BUTTON" desc="The name for the clear all markers button.">
+        Clear all markers
+      </message>
+
+      <message name="IDS_EXPAND_MARKER_TOOLS_BUTTON" desc="The name for the expand marker tools button.">
+        Expand marker tools
+      </message>
+
+      <message name="IDS_COLLAPSE_MARKER_TOOLS_BUTTON" desc="The name for the collapse marker tools button.">
+        Collapse marker tools
+      </message>
+
+      <message name="IDS_START_MAGNIFIER_BUTTON" desc="The name for the start magnifier button.">
+        Start magnifier
+      </message>
+
+      <message name="IDS_STOP_MAGNIFIER_BUTTON" desc="The name for the stop magnifier button.">
+        Stop magnifier
+      </message>
+
+      <message name="IDS_START_SELFIE_CAMERA_BUTTON" desc="The name for the start selfie camera button.">
+        Start selfie camera
+      </message>
+
+      <message name="IDS_STOP_SELFIE_CAMERA_BUTTON" desc="The name for the stop selfie camera button.">
+        Stop selfie camera
+      </message>
+
+      <message name="IDS_START_CLOSED_CAPTIONS_BUTTON" desc="The name for start closed captions button.">
+        Start closed captions
+      </message>
+
+      <message name="IDS_STOP_CLOSED_CAPTIONS_BUTTON" desc="The name for stop closed captions button.">
+        Stop closed captions
+      </message>
+
+      <message name="IDS_BAR_LOCATION_BUTTON" desc="The name for the bar location button.">
+        Toggle bar location
+      </message>
+
+      <message name="IDS_BLACK_COLOR_BUTTON" desc="The name for the black marker button.">
+        Black
+      </message>
+
+      <message name="IDS_WHITE_COLOR_BUTTON" desc="The name for the white marker button.">
+        White
+      </message>
+
+      <message name="IDS_BLUE_COLOR_BUTTON" desc="The name for the blue marker button.">
+        Blue
+      </message>
+
+      <message name="IDS_UNKNOWN_COLOR_BUTTON" desc="The name for the unknown marker button.">
+        Unknown
+      </message>
+
     </messages>
   </release>
 </grit>
diff --git a/ash/ash_strings_grd/IDS_BAR_LOCATION_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_BAR_LOCATION_BUTTON.png.sha1
new file mode 100644
index 0000000..dba4d64a0
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_BAR_LOCATION_BUTTON.png.sha1
@@ -0,0 +1 @@
+a5bd6a8c1710652534a9ec6baa5d2821e3e3e8bf
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_BLACK_COLOR_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_BLACK_COLOR_BUTTON.png.sha1
new file mode 100644
index 0000000..c925577f
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_BLACK_COLOR_BUTTON.png.sha1
@@ -0,0 +1 @@
+27f4eb1d76a2a686a79f93524334a22d3dfda12b
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_BLUE_COLOR_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_BLUE_COLOR_BUTTON.png.sha1
new file mode 100644
index 0000000..26cb665
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_BLUE_COLOR_BUTTON.png.sha1
@@ -0,0 +1 @@
+153471238880eb515a2340f5076228ca187a37d0
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_CLEAR_ALL_MARKERS_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_CLEAR_ALL_MARKERS_BUTTON.png.sha1
new file mode 100644
index 0000000..13e3b254
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_CLEAR_ALL_MARKERS_BUTTON.png.sha1
@@ -0,0 +1 @@
+876fcbe8ce6916f81d7f49ed105e24e424ffb729
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_COLLAPSE_MARKER_TOOLS_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_COLLAPSE_MARKER_TOOLS_BUTTON.png.sha1
new file mode 100644
index 0000000..7bc6983a
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_COLLAPSE_MARKER_TOOLS_BUTTON.png.sha1
@@ -0,0 +1 @@
+44f37b74dffb0d2387e16f998afe731ffbbccadd
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_EXPAND_MARKER_TOOLS_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_EXPAND_MARKER_TOOLS_BUTTON.png.sha1
new file mode 100644
index 0000000..b846563
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_EXPAND_MARKER_TOOLS_BUTTON.png.sha1
@@ -0,0 +1 @@
+79d4d0863a36aa153cc92176eb0ce9e6764404e5
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_EXTENSIONS_ENABLE_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_EXTENSIONS_ENABLE_BUTTON.png.sha1
new file mode 100644
index 0000000..301528a
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_EXTENSIONS_ENABLE_BUTTON.png.sha1
@@ -0,0 +1 @@
+f2a24389c19c6a880a5f1e7266d47e6e70b874b0
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_INK_PEN_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_INK_PEN_BUTTON.png.sha1
new file mode 100644
index 0000000..be90797
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_INK_PEN_BUTTON.png.sha1
@@ -0,0 +1 @@
+9621a79948d5f9804d5d24213a3698b8f2a0b5e9
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_KEY_IDEA_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_KEY_IDEA_BUTTON.png.sha1
new file mode 100644
index 0000000..624e8bd
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_KEY_IDEA_BUTTON.png.sha1
@@ -0,0 +1 @@
+4640217c15a4b968f27094c0a70fa499b749ae43
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_LASER_POINTER_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_LASER_POINTER_BUTTON.png.sha1
new file mode 100644
index 0000000..9190f58
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_LASER_POINTER_BUTTON.png.sha1
@@ -0,0 +1 @@
+7ff2ddde7110785a542df75117bf740e8ec9cde4
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_MARKER_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_MARKER_BUTTON.png.sha1
new file mode 100644
index 0000000..3dfccb7bc
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_MARKER_BUTTON.png.sha1
@@ -0,0 +1 @@
+3007dfdbbbb2ac01f7dcb91ff4b035ea6efdb118
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_MARKER_COLOR_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_MARKER_COLOR_BUTTON.png.sha1
new file mode 100644
index 0000000..c889abf7
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_MARKER_COLOR_BUTTON.png.sha1
@@ -0,0 +1 @@
+a22f2f1da5d6764d0d0752aacfb1a4905f6bb7f7
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_MARKER_PEN_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_MARKER_PEN_BUTTON.png.sha1
new file mode 100644
index 0000000..c2e3a67
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_MARKER_PEN_BUTTON.png.sha1
@@ -0,0 +1 @@
+6e0aad7230b25193689822690661cbb1eaebab70
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_START_CLOSED_CAPTIONS_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_START_CLOSED_CAPTIONS_BUTTON.png.sha1
new file mode 100644
index 0000000..2f496cb9
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_START_CLOSED_CAPTIONS_BUTTON.png.sha1
@@ -0,0 +1 @@
+b33e78eefa35815bc615bff941bfe44ef7824df1
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_START_MAGNIFIER_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_START_MAGNIFIER_BUTTON.png.sha1
new file mode 100644
index 0000000..25e3b07
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_START_MAGNIFIER_BUTTON.png.sha1
@@ -0,0 +1 @@
+e7a83d97040706131735a3559eb9a0e1c437557d
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_START_SELFIE_CAMERA_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_START_SELFIE_CAMERA_BUTTON.png.sha1
new file mode 100644
index 0000000..5e4f75b
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_START_SELFIE_CAMERA_BUTTON.png.sha1
@@ -0,0 +1 @@
+a6cc5126c2040f2bf221b54721c48dd8d2a421fd
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_STOP_CLOSED_CAPTIONS_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_STOP_CLOSED_CAPTIONS_BUTTON.png.sha1
new file mode 100644
index 0000000..82882b9
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_STOP_CLOSED_CAPTIONS_BUTTON.png.sha1
@@ -0,0 +1 @@
+9d06341f57fee98c4366bc3f85cf950525c985cb
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_STOP_MAGNIFIER_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_STOP_MAGNIFIER_BUTTON.png.sha1
new file mode 100644
index 0000000..4031133b
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_STOP_MAGNIFIER_BUTTON.png.sha1
@@ -0,0 +1 @@
+44eda6d367523f4fc69e9ebd19cba56f4ea70bb2
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_STOP_SELFIE_CAMERA_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_STOP_SELFIE_CAMERA_BUTTON.png.sha1
new file mode 100644
index 0000000..64141e4
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_STOP_SELFIE_CAMERA_BUTTON.png.sha1
@@ -0,0 +1 @@
+26e40ae7b76394489afbc0b6ef1b5ac5e6555b9d
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_UNDO_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_UNDO_BUTTON.png.sha1
new file mode 100644
index 0000000..5157fb6
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_UNDO_BUTTON.png.sha1
@@ -0,0 +1 @@
+29432a50d1c76fc76d230c165636a7947533c194
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_UNKNOWN_COLOR_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_UNKNOWN_COLOR_BUTTON.png.sha1
new file mode 100644
index 0000000..dc50e80d
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_UNKNOWN_COLOR_BUTTON.png.sha1
@@ -0,0 +1 @@
+122ce269326d14a6c0c8f575b6d9b4640a4c42af
\ No newline at end of file
diff --git a/ash/ash_strings_grd/IDS_WHITE_COLOR_BUTTON.png.sha1 b/ash/ash_strings_grd/IDS_WHITE_COLOR_BUTTON.png.sha1
new file mode 100644
index 0000000..1cf6cd71
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_WHITE_COLOR_BUTTON.png.sha1
@@ -0,0 +1 @@
+7693f2f43f03bc9f6b05134bbb6d0c8f207f8b34
\ No newline at end of file
diff --git a/ash/content/shortcut_customization_ui/resources/BUILD.gn b/ash/content/shortcut_customization_ui/resources/BUILD.gn
index ea809e3..43b86f06 100644
--- a/ash/content/shortcut_customization_ui/resources/BUILD.gn
+++ b/ash/content/shortcut_customization_ui/resources/BUILD.gn
@@ -12,10 +12,12 @@
 preprocessed_gen_manifest = "preprocessed_gen_manifest.json"
 
 polymer_element_files = [
-  "shortcut_customization_app.js",
-  "shortcut_input.js",
+  "accessibility_shortcuts_page.js",
+  "android_shortcuts_page.js",
   "browser_shortcuts_page.js",
   "chromeos_shortcuts_page.js",
+  "shortcut_customization_app.js",
+  "shortcut_input.js",
 ]
 
 generate_grd("build_grd") {
@@ -64,6 +66,20 @@
   ]
 }
 
+js_library("android_shortcuts_page") {
+  deps = [
+    ":shortcut_input",
+    "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
+  ]
+}
+
+js_library("accessibility_shortcuts_page") {
+  deps = [
+    ":shortcut_input",
+    "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
+  ]
+}
+
 preprocess_if_expr("preprocess_generated") {
   deps = [ ":web_components" ]
   in_folder = target_gen_dir
diff --git a/ash/content/shortcut_customization_ui/resources/accessibility_shortcuts_page.html b/ash/content/shortcut_customization_ui/resources/accessibility_shortcuts_page.html
new file mode 100644
index 0000000..2f167906
--- /dev/null
+++ b/ash/content/shortcut_customization_ui/resources/accessibility_shortcuts_page.html
@@ -0,0 +1,2 @@
+<div>Accessibility</div>
+<shortcut-input></shortcut-input>
\ No newline at end of file
diff --git a/ash/content/shortcut_customization_ui/resources/accessibility_shortcuts_page.js b/ash/content/shortcut_customization_ui/resources/accessibility_shortcuts_page.js
new file mode 100644
index 0000000..286b52e
--- /dev/null
+++ b/ash/content/shortcut_customization_ui/resources/accessibility_shortcuts_page.js
@@ -0,0 +1,26 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import './shortcut_input.js';
+
+import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+
+/**
+ * @fileoverview
+ * 'accessibility-shortcuts-page' is responsible for containing all shortcuts
+ * related to accessibility.
+ * TODO(jimmyxgong): Implement this skeleton element.
+ */
+export class AccessibilityShortcutsPageElement extends PolymerElement {
+  static get is() {
+    return 'accessibility-shortcuts-page';
+  }
+
+  static get template() {
+    return html`{__html_template__}`;
+  }
+}
+
+customElements.define(AccessibilityShortcutsPageElement.is,
+                      AccessibilityShortcutsPageElement);
\ No newline at end of file
diff --git a/ash/content/shortcut_customization_ui/resources/android_shortcuts_page.html b/ash/content/shortcut_customization_ui/resources/android_shortcuts_page.html
new file mode 100644
index 0000000..05a54cd8
--- /dev/null
+++ b/ash/content/shortcut_customization_ui/resources/android_shortcuts_page.html
@@ -0,0 +1,2 @@
+<div>Android</div>
+<shortcut-input></shortcut-input>
\ No newline at end of file
diff --git a/ash/content/shortcut_customization_ui/resources/android_shortcuts_page.js b/ash/content/shortcut_customization_ui/resources/android_shortcuts_page.js
new file mode 100644
index 0000000..c343b63
--- /dev/null
+++ b/ash/content/shortcut_customization_ui/resources/android_shortcuts_page.js
@@ -0,0 +1,26 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import './shortcut_input.js';
+
+import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+
+/**
+ * @fileoverview
+ * 'android-shortcuts-page' is responsible for containing all shortcuts
+ * related to Android apps.
+ * TODO(jimmyxgong): Implement this skeleton element.
+ */
+export class AndroidShortcutsPageElement extends PolymerElement {
+  static get is() {
+    return 'android-shortcuts-page';
+  }
+
+  static get template() {
+    return html`{__html_template__}`;
+  }
+}
+
+customElements.define(AndroidShortcutsPageElement.is,
+                      AndroidShortcutsPageElement);
\ No newline at end of file
diff --git a/ash/content/shortcut_customization_ui/resources/shortcut_customization_app.js b/ash/content/shortcut_customization_ui/resources/shortcut_customization_app.js
index 46ece69..e0ec642d 100644
--- a/ash/content/shortcut_customization_ui/resources/shortcut_customization_app.js
+++ b/ash/content/shortcut_customization_ui/resources/shortcut_customization_app.js
@@ -5,6 +5,8 @@
 import './shortcut_input.js';
 import './browser_shortcuts_page.js'
 import './chromeos_shortcuts_page.js'
+import './android_shortcuts_page.js'
+import './accessibility_shortcuts_page.js'
 
 import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import 'chrome://resources/ash/common/navigation_view_panel.js';
@@ -28,6 +30,9 @@
     this.$.navigationPanel.addSelector('Chrome OS',
                                        'chromeos-shortcuts-page');
     this.$.navigationPanel.addSelector('Browser', 'browser-shortcuts-page');
+    this.$.navigationPanel.addSelector('Android', 'android-shortcuts-page');
+    this.$.navigationPanel.addSelector('Accessibility',
+                                       'accessibility-shortcuts-page');
   }
 }
 
diff --git a/ash/projector/projector_controller_impl.cc b/ash/projector/projector_controller_impl.cc
index 21fe534..026d74b5 100644
--- a/ash/projector/projector_controller_impl.cc
+++ b/ash/projector/projector_controller_impl.cc
@@ -56,10 +56,16 @@
 void ProjectorControllerImpl::SetProjectorToolsVisible(bool is_visible) {
   // TODO(yilkal): Projector toolbar shouldn't be shown if soda is not
   // available.
-  if (is_visible)
+  if (is_visible) {
     ui_controller_->ShowToolbar();
-  else
-    ui_controller_->CloseToolbar();
+    OnRecordingStarted();
+    return;
+  }
+
+  OnRecordingEnded();
+  if (client_->IsSelfieCamVisible())
+    client_->CloseSelfieCam();
+  ui_controller_->CloseToolbar();
 }
 
 bool ProjectorControllerImpl::IsEligible() const {
@@ -98,20 +104,6 @@
   metadata_controller_->SaveMetadata(saved_video_path);
 }
 
-void ProjectorControllerImpl::OnRecordButtonPressed() {
-  // TODO(crbug.com/1165435): Start the recording session and update the button
-  // visibility based on recording state after integrating with capture mode and
-  // recording service.
-  OnRecordingStarted();
-}
-
-void ProjectorControllerImpl::OnStopRecordButtonPressed() {
-  // TODO(crbug.com/1165435): Stop the recording session and update the button
-  // visibility based on recording state after integrating with capture mode and
-  // recording service.
-  OnRecordingEnded();
-}
-
 void ProjectorControllerImpl::OnLaserPointerPressed() {
   ui_controller_->OnLaserPointerPressed();
 }
@@ -126,6 +118,16 @@
 
 void ProjectorControllerImpl::OnSelfieCamPressed(bool enabled) {
   ui_controller_->OnSelfieCamPressed(enabled);
+
+  DCHECK_NE(client_, nullptr);
+  if (enabled == client_->IsSelfieCamVisible())
+    return;
+
+  if (enabled) {
+    client_->ShowSelfieCam();
+    return;
+  }
+  client_->CloseSelfieCam();
 }
 
 void ProjectorControllerImpl::OnMagnifierButtonPressed(bool enabled) {
diff --git a/ash/projector/projector_controller_impl.h b/ash/projector/projector_controller_impl.h
index f5faa96..2f9376e 100644
--- a/ash/projector/projector_controller_impl.h
+++ b/ash/projector/projector_controller_impl.h
@@ -61,10 +61,6 @@
   // Saves the screencast including metadata.
   void SaveScreencast(const base::FilePath& saved_video_path);
 
-  // Invoked when record button is pressed.
-  void OnRecordButtonPressed();
-  void OnStopRecordButtonPressed();
-
   // Invoked when laser pointer button is pressed.
   void OnLaserPointerPressed();
   // Invoked when marker button is pressed.
diff --git a/ash/projector/projector_controller_unittest.cc b/ash/projector/projector_controller_unittest.cc
index 89a68a3b..7370595 100644
--- a/ash/projector/projector_controller_unittest.cc
+++ b/ash/projector/projector_controller_unittest.cc
@@ -9,6 +9,7 @@
 #include <vector>
 
 #include "ash/constants/ash_features.h"
+#include "ash/projector/test/mock_projector_client.h"
 #include "ash/projector/test/mock_projector_metadata_controller.h"
 #include "ash/projector/test/mock_projector_ui_controller.h"
 #include "ash/test/ash_test_base.h"
@@ -84,12 +85,16 @@
     mock_metadata_controller_ = mock_metadata_controller.get();
     controller_->SetProjectorMetadataControllerForTest(
         std::move(mock_metadata_controller));
+
+    controller_->SetClient(&mock_client_);
+    controller_->OnSpeechRecognitionAvailable(/*available=*/true);
   }
 
  protected:
   MockProjectorUiController* mock_ui_controller_ = nullptr;
   MockProjectorMetadataController* mock_metadata_controller_ = nullptr;
   ProjectorControllerImpl* controller_;
+  MockProjectorClient mock_client_;
 
  private:
   base::test::ScopedFeatureList scoped_feature_list_;
@@ -101,6 +106,15 @@
   controller_->SetProjectorToolsVisible(true);
 }
 
+TEST_F(ProjectorControllerTest, CloseToolbar) {
+  controller_->SetProjectorToolsVisible(/*is_visible=*/true);
+  mock_client_.SetSelfieCamVisible(/*visible=*/true);
+  // Verify that |CloseToolbar| in |ProjectorUiController| is called.
+  EXPECT_CALL(*mock_ui_controller_, CloseToolbar()).Times(1);
+  EXPECT_CALL(mock_client_, CloseSelfieCam()).Times(1);
+  controller_->SetProjectorToolsVisible(/*is_visible=*/false);
+}
+
 TEST_F(ProjectorControllerTest, SaveScreencast) {
   base::FilePath saved_path;
   // Verify that |SaveMetadata| in |ProjectorMetadataController| is called.
@@ -200,11 +214,15 @@
 
 TEST_F(ProjectorControllerTest, OnSelfieCamPressed) {
   // Verify that |OnSelfieCamPressed| in |ProjectorUiController| is called.
-  EXPECT_CALL(*mock_ui_controller_, OnSelfieCamPressed(true));
-  controller_->OnSelfieCamPressed(true);
+  EXPECT_CALL(*mock_ui_controller_, OnSelfieCamPressed(/*enabled=*/true));
+  EXPECT_CALL(mock_client_, ShowSelfieCam());
+  controller_->OnSelfieCamPressed(/*enabled=*/true);
+  mock_client_.SetSelfieCamVisible(/*visible=*/true);
 
-  EXPECT_CALL(*mock_ui_controller_, OnSelfieCamPressed(false));
-  controller_->OnSelfieCamPressed(false);
+  EXPECT_CALL(*mock_ui_controller_, OnSelfieCamPressed(/*enabled=*/false));
+  EXPECT_CALL(mock_client_, CloseSelfieCam());
+  controller_->OnSelfieCamPressed(/*enabled=*/false);
+  mock_client_.SetSelfieCamVisible(/*visible=*/false);
 }
 
 TEST_F(ProjectorControllerTest, SetCaptionBubbleState) {
@@ -222,4 +240,20 @@
   controller_->OnChangeMarkerColorPressed(SK_ColorBLACK);
 }
 
+TEST_F(ProjectorControllerTest, RecordingStarted) {
+  EXPECT_CALL(mock_client_, StartSpeechRecognition());
+  EXPECT_CALL(*mock_ui_controller_, OnRecordingStateChanged(/*started=*/true));
+  EXPECT_CALL(*mock_metadata_controller_, OnRecordingStarted());
+  controller_->OnRecordingStarted();
+}
+
+TEST_F(ProjectorControllerTest, RecordingEnded) {
+  controller_->OnRecordingStarted();
+  mock_client_.SetSelfieCamVisible(/*visible=*/true);
+  EXPECT_CALL(mock_client_, StopSpeechRecognition());
+  EXPECT_CALL(*mock_ui_controller_, OnRecordingStateChanged(/*started=*/false));
+  EXPECT_CALL(mock_client_, CloseSelfieCam());
+  controller_->SetProjectorToolsVisible(/*is_visible=*/false);
+}
+
 }  // namespace ash
diff --git a/ash/projector/projector_ui_controller.cc b/ash/projector/projector_ui_controller.cc
index 51a6de1..6db267d2 100644
--- a/ash/projector/projector_ui_controller.cc
+++ b/ash/projector/projector_ui_controller.cc
@@ -233,12 +233,11 @@
 }
 
 void ProjectorUiController::OnSelfieCamPressed(bool enabled) {
-  // TODO(crbug/1199396): If enabled, launch the web UI.
-
   // If the selfie cam is visible, then the button for turning on the selfie cam
   // should be hidden in the projector bar view. The button for turning off the
   // selfie cam should show instead.
-  projector_bar_view_->OnSelfieCamStateChanged(enabled);
+  if (projector_bar_view_)
+    projector_bar_view_->OnSelfieCamStateChanged(enabled);
 }
 
 void ProjectorUiController::OnRecordingStateChanged(bool started) {
diff --git a/ash/projector/projector_ui_controller_unittest.cc b/ash/projector/projector_ui_controller_unittest.cc
index c857625e..e6ccb54 100644
--- a/ash/projector/projector_ui_controller_unittest.cc
+++ b/ash/projector/projector_ui_controller_unittest.cc
@@ -180,16 +180,13 @@
 TEST_F(ProjectorUiControllerTest, RecordingState) {
   controller_->ShowToolbar();
   ProjectorBarView* bar_view_ = controller_->projector_bar_view();
-  EXPECT_TRUE(bar_view_->IsRecordButtonVisible());
   EXPECT_FALSE(bar_view_->IsKeyIdeaButtonEnabled());
 
   controller_->OnRecordingStateChanged(/* started = */ true);
-  EXPECT_FALSE(bar_view_->IsRecordButtonVisible());
   EXPECT_TRUE(bar_view_->IsKeyIdeaButtonEnabled());
   EXPECT_TRUE(bar_view_->IsClosedCaptionEnabled());
 
   controller_->OnRecordingStateChanged(/* started = */ false);
-  EXPECT_TRUE(bar_view_->IsRecordButtonVisible());
   EXPECT_FALSE(bar_view_->IsKeyIdeaButtonEnabled());
   EXPECT_FALSE(bar_view_->IsClosedCaptionEnabled());
 }
diff --git a/ash/projector/test/mock_projector_client.cc b/ash/projector/test/mock_projector_client.cc
new file mode 100644
index 0000000..3745f249
--- /dev/null
+++ b/ash/projector/test/mock_projector_client.cc
@@ -0,0 +1,20 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/projector/test/mock_projector_client.h"
+
+namespace ash {
+
+MockProjectorClient::MockProjectorClient() = default;
+MockProjectorClient::~MockProjectorClient() = default;
+
+bool MockProjectorClient::IsSelfieCamVisible() const {
+  return is_selfie_cam_visible_;
+}
+
+void MockProjectorClient::SetSelfieCamVisible(bool visible) {
+  is_selfie_cam_visible_ = visible;
+}
+
+}  // namespace ash
diff --git a/ash/projector/test/mock_projector_client.h b/ash/projector/test/mock_projector_client.h
new file mode 100644
index 0000000..d236104d
--- /dev/null
+++ b/ash/projector/test/mock_projector_client.h
@@ -0,0 +1,37 @@
+// 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 ASH_PROJECTOR_TEST_MOCK_PROJECTOR_CLIENT_H_
+#define ASH_PROJECTOR_TEST_MOCK_PROJECTOR_CLIENT_H_
+
+#include "ash/ash_export.h"
+#include "ash/public/cpp/projector/projector_client.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+namespace ash {
+
+// A mock implementation of ProjectorClient for use in tests.
+class ASH_EXPORT MockProjectorClient : public ProjectorClient {
+ public:
+  MockProjectorClient();
+  MockProjectorClient(const MockProjectorClient&) = delete;
+  MockProjectorClient& operator=(const MockProjectorClient&) = delete;
+  virtual ~MockProjectorClient();
+
+  // ProjectorClient:
+  MOCK_METHOD0(StartSpeechRecognition, void());
+  MOCK_METHOD0(StopSpeechRecognition, void());
+  MOCK_METHOD0(ShowSelfieCam, void());
+  MOCK_METHOD0(CloseSelfieCam, void());
+
+  bool IsSelfieCamVisible() const override;
+  void SetSelfieCamVisible(bool visible);
+
+ private:
+  bool is_selfie_cam_visible_ = false;
+};
+
+}  // namespace ash
+
+#endif  // ASH_PROJECTOR_TEST_MOCK_PROJECTOR_CLIENT_H_
diff --git a/ash/projector/test/mock_projector_ui_controller.cc b/ash/projector/test/mock_projector_ui_controller.cc
index 04d332f..d2930a22 100644
--- a/ash/projector/test/mock_projector_ui_controller.cc
+++ b/ash/projector/test/mock_projector_ui_controller.cc
@@ -1,5 +1,4 @@
 // Copyright 2021 The Chromium Authors. All rights reserved.
-// 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.
 
diff --git a/ash/projector/test/mock_projector_ui_controller.h b/ash/projector/test/mock_projector_ui_controller.h
index 05c8440..5992c76 100644
--- a/ash/projector/test/mock_projector_ui_controller.h
+++ b/ash/projector/test/mock_projector_ui_controller.h
@@ -5,6 +5,7 @@
 #ifndef ASH_PROJECTOR_TEST_MOCK_PROJECTOR_UI_CONTROLLER_H_
 #define ASH_PROJECTOR_TEST_MOCK_PROJECTOR_UI_CONTROLLER_H_
 
+#include "ash/ash_export.h"
 #include "ash/projector/projector_ui_controller.h"
 #include "testing/gmock/include/gmock/gmock.h"
 
@@ -26,6 +27,7 @@
 
   // ProjectorUiController:
   MOCK_METHOD0(ShowToolbar, void());
+  MOCK_METHOD0(CloseToolbar, void());
   MOCK_METHOD0(OnKeyIdeaMarked, void());
   MOCK_METHOD0(OnLaserPointerPressed, void());
   MOCK_METHOD0(OnMarkerPressed, void());
diff --git a/ash/projector/ui/projector_bar_view.cc b/ash/projector/ui/projector_bar_view.cc
index 26dde39..b064484 100644
--- a/ash/projector/ui/projector_bar_view.cc
+++ b/ash/projector/ui/projector_bar_view.cc
@@ -7,9 +7,12 @@
 #include "ash/projector/projector_controller_impl.h"
 #include "ash/resources/vector_icons/vector_icons.h"
 #include "ash/shell.h"
+#include "ash/strings/grit/ash_strings.h"
 #include "ash/style/ash_color_provider.h"
 #include "ash/wm/work_area_insets.h"
 #include "components/vector_icons/vector_icons.h"
+#include "third_party/skia/include/core/SkColor.h"
+#include "ui/base/l10n/l10n_util.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
 #include "ui/gfx/paint_vector_icon.h"
 #include "ui/views/background.h"
@@ -37,11 +40,6 @@
 // |ProjectorBarView|.
 constexpr int kBetweenChildSpacing = 16;
 
-// Recording buttons.
-constexpr int kRecordingButtonColorViewSize = 12;
-constexpr int kStartRecordingButtonColorViewRadius = 6;
-constexpr int kStopRecordingButtonColorViewRadius = 2;
-
 // Color selection buttons.
 constexpr int kColorButtonColorViewSize = 24;
 constexpr int kColorButtonViewRadius = 12;
@@ -52,6 +50,23 @@
 constexpr SkColor kProjectorColors[] = {SK_ColorBLACK, SK_ColorWHITE,
                                         SK_ColorBLUE};
 
+std::u16string GetColorName(SkColor color) {
+  int name = IDS_UNKNOWN_COLOR_BUTTON;
+  switch (color) {
+    case SK_ColorBLACK:
+      name = IDS_BLACK_COLOR_BUTTON;
+      break;
+    case SK_ColorWHITE:
+      name = IDS_WHITE_COLOR_BUTTON;
+      break;
+    case SK_ColorBLUE:
+      name = IDS_BLUE_COLOR_BUTTON;
+      break;
+  }
+  DCHECK_NE(name, IDS_UNKNOWN_COLOR_BUTTON);
+  return l10n_util::GetStringUTF16(name);
+}
+
 }  // namespace
 
 // static
@@ -87,9 +102,6 @@
 }
 
 void ProjectorBarView::OnRecordingStateChanged(bool started) {
-  record_button_->SetVisible(!started);
-  stop_button_->SetVisible(started);
-
   // Closed caption and key idea buttons states are dependent on the recording
   // state.
   auto recording_related_buttons_state =
@@ -143,10 +155,6 @@
   views::View::OnThemeChanged();
 }
 
-bool ProjectorBarView::IsRecordButtonVisible() const {
-  return record_button_->GetVisible();
-}
-
 bool ProjectorBarView::IsKeyIdeaButtonEnabled() const {
   return key_idea_button_->GetState() ==
          views::Button::ButtonState::STATE_NORMAL;
@@ -175,24 +183,11 @@
       views::Painter::CreateSolidRoundRectPainter(
           SkColorSetA(gfx::kGoogleGrey900, kBarAlpha_), kBarRadius)));
 
-  // Add recording buttons.
-  record_button_ = AddChildView(std::make_unique<ProjectorColorButton>(
-      base::BindRepeating(&ProjectorBarView::OnRecordButtonPressed,
-                          base::Unretained(this)),
-      SK_ColorWHITE, kRecordingButtonColorViewSize,
-      kStartRecordingButtonColorViewRadius));
-  stop_button_ = AddChildView(std::make_unique<ProjectorColorButton>(
-      base::BindRepeating(&ProjectorBarView::OnStopButtonPressed,
-                          base::Unretained(this)),
-      SK_ColorRED, kRecordingButtonColorViewSize,
-      kStopRecordingButtonColorViewRadius)),
-  stop_button_->SetVisible(false);
-
   // Add key idea button.
   key_idea_button_ = AddChildView(std::make_unique<ProjectorImageButton>(
       base::BindRepeating(&ProjectorBarView::OnKeyIdeaButtonPressed,
                           base::Unretained(this)),
-      kProjectorKeyIdeaIcon));
+      kProjectorKeyIdeaIcon, l10n_util::GetStringUTF16(IDS_KEY_IDEA_BUTTON)));
   key_idea_button_->SetState(views::Button::ButtonState::STATE_DISABLED);
 
   // Add separator view
@@ -202,13 +197,14 @@
   laser_pointer_button_ = AddChildView(std::make_unique<ProjectorImageButton>(
       base::BindRepeating(&ProjectorBarView::OnLaserPointerPressed,
                           base::Unretained(this)),
-      kPaletteTrayIconLaserPointerIcon));
+      kPaletteTrayIconLaserPointerIcon,
+      l10n_util::GetStringUTF16(IDS_LASER_POINTER_BUTTON)));
 
   // Add marker button.
   marker_button_ = AddChildView(std::make_unique<ProjectorImageButton>(
       base::BindRepeating(&ProjectorBarView::OnMarkerPressed,
                           base::Unretained(this)),
-      kProjectorMarkerIcon));
+      kProjectorMarkerIcon, l10n_util::GetStringUTF16(IDS_MARKER_BUTTON)));
 
   CreateMarkerOptionsBar();
 
@@ -238,33 +234,37 @@
       box_layout->AddChildView(std::make_unique<ProjectorImageButton>(
           base::BindRepeating(&ProjectorBarView::OnInkPenButtonPressed,
                               base::Unretained(this)),
-          kInkPenIcon));
+          kInkPenIcon, l10n_util::GetStringUTF16(IDS_INK_PEN_BUTTON)));
   marker_pen_button_ =
       box_layout->AddChildView(std::make_unique<ProjectorImageButton>(
           base::BindRepeating(&ProjectorBarView::OnMarkerPenButtonPressed,
                               base::Unretained(this)),
-          kMarkerIcon));
+          kMarkerIcon, l10n_util::GetStringUTF16(IDS_MARKER_PEN_BUTTON)));
 
   for (const auto& color : kProjectorColors) {
+    std::u16string button_name = l10n_util::GetStringFUTF16(
+        IDS_MARKER_COLOR_BUTTON, GetColorName(color));
     marker_color_buttons_.push_back(
         box_layout->AddChildView(std::make_unique<ProjectorColorButton>(
             base::BindRepeating(&ProjectorBarView::OnChangeMarkerColorPressed,
                                 base::Unretained(this), color),
-            color, kColorButtonColorViewSize, kColorButtonViewRadius)));
+            color, kColorButtonColorViewSize, kColorButtonViewRadius,
+            button_name)));
   }
 
   undo_button_ =
       box_layout->AddChildView(std::make_unique<ProjectorImageButton>(
           base::BindRepeating(&ProjectorBarView::OnUndoButtonPressed,
                               base::Unretained(this)),
-          kUndoIcon));
+          kUndoIcon, l10n_util::GetStringUTF16(IDS_UNDO_BUTTON)));
 
   // Add clear all markers button.
   clear_all_markers_button_ =
       box_layout->AddChildView(std::make_unique<ProjectorImageButton>(
           base::BindRepeating(&ProjectorBarView::OnClearAllMarkersPressed,
                               base::Unretained(this)),
-          kTrashCanIcon));
+          kTrashCanIcon,
+          l10n_util::GetStringUTF16(IDS_CLEAR_ALL_MARKERS_BUTTON)));
 
   // This button is disabled by default until marker mode activated.
   clear_all_markers_button_->SetEnabled(marker_button_->GetToggled());
@@ -273,11 +273,13 @@
       box_layout->AddChildView(std::make_unique<ProjectorImageButton>(
           base::BindRepeating(&ProjectorBarView::OnCaretButtonPressed,
                               base::Unretained(this), /* expand =*/true),
-          kCaretRightIcon));
+          kCaretRightIcon,
+          l10n_util::GetStringUTF16(IDS_EXPAND_MARKER_TOOLS_BUTTON)));
   caret_left_ = box_layout->AddChildView(std::make_unique<ProjectorImageButton>(
       base::BindRepeating(&ProjectorBarView::OnCaretButtonPressed,
                           base::Unretained(this), /* expand =*/false),
-      kCaretLeftIcon));
+      kCaretLeftIcon,
+      l10n_util::GetStringUTF16(IDS_COLLAPSE_MARKER_TOOLS_BUTTON)));
 
   marker_bar_ = AddChildView(std::move(box_layout));
   marker_bar_->SetVisible(false);
@@ -295,13 +297,13 @@
       box_layout->AddChildView(std::make_unique<ProjectorImageButton>(
           base::BindRepeating(&ProjectorBarView::OnMagnifierButtonPressed,
                               base::Unretained(this), /* enabled =*/true),
-          kZoomInIcon));
+          kZoomInIcon, l10n_util::GetStringUTF16(IDS_START_MAGNIFIER_BUTTON)));
   magnifier_start_button_->SetVisible(true);
   magnifier_stop_button_ =
       box_layout->AddChildView(std::make_unique<ProjectorImageButton>(
           base::BindRepeating(&ProjectorBarView::OnMagnifierButtonPressed,
                               base::Unretained(this), /* enabled =*/false),
-          kZoomOutIcon));
+          kZoomOutIcon, l10n_util::GetStringUTF16(IDS_STOP_MAGNIFIER_BUTTON)));
   magnifier_stop_button_->SetVisible(false);
 
   AddSeparatorViewToView(box_layout.get());
@@ -311,14 +313,16 @@
       box_layout->AddChildView(std::make_unique<ProjectorImageButton>(
           base::BindRepeating(&ProjectorBarView::OnSelfieCamPressed,
                               base::Unretained(this), /*enabled=*/true),
-          kProjectorSelfieCamOnIcon));
+          kProjectorSelfieCamOnIcon,
+          l10n_util::GetStringUTF16(IDS_START_SELFIE_CAMERA_BUTTON)));
   selfie_cam_on_button_->SetVisible(true);
 
   selfie_cam_off_button_ =
       box_layout->AddChildView(std::make_unique<ProjectorImageButton>(
           base::BindRepeating(&ProjectorBarView::OnSelfieCamPressed,
                               base::Unretained(this), /*enabled=*/false),
-          kProjectorSelfieCamOffIcon));
+          kProjectorSelfieCamOffIcon,
+          l10n_util::GetStringUTF16(IDS_STOP_SELFIE_CAMERA_BUTTON)));
   selfie_cam_off_button_->SetVisible(false);
 
   // Add closed caption show/hide buttons.
@@ -326,7 +330,8 @@
       box_layout->AddChildView(std::make_unique<ProjectorImageButton>(
           base::BindRepeating(&ProjectorBarView::SetCaptionState,
                               base::Unretained(this), false),
-          kHideClosedCaptionIcon));
+          kHideClosedCaptionIcon,
+          l10n_util::GetStringUTF16(IDS_STOP_CLOSED_CAPTIONS_BUTTON)));
   closed_caption_hide_button_->SetVisible(false);
   closed_caption_hide_button_->SetState(
       views::Button::ButtonState::STATE_DISABLED);
@@ -335,7 +340,8 @@
       box_layout->AddChildView(std::make_unique<ProjectorImageButton>(
           base::BindRepeating(&ProjectorBarView::SetCaptionState,
                               base::Unretained(this), true),
-          kShowClosedCaptionIcon));
+          kShowClosedCaptionIcon,
+          l10n_util::GetStringUTF16(IDS_START_CLOSED_CAPTIONS_BUTTON)));
   closed_caption_show_button_->SetVisible(true);
   closed_caption_show_button_->SetState(
       views::Button::ButtonState::STATE_DISABLED);
@@ -347,19 +353,12 @@
           base::BindRepeating(
               &ProjectorBarView::OnChangeBarLocationButtonPressed,
               base::Unretained(this)),
-          kAutoclickPositionBottomLeftIcon));
+          kAutoclickPositionBottomLeftIcon,
+          l10n_util::GetStringUTF16(IDS_BAR_LOCATION_BUTTON)));
   bar_location_button_->SetVisible(true);
   tools_bar_ = AddChildView(std::move(box_layout));
 }
 
-void ProjectorBarView::OnRecordButtonPressed() {
-  projector_controller_->OnRecordButtonPressed();
-}
-
-void ProjectorBarView::OnStopButtonPressed() {
-  projector_controller_->OnStopRecordButtonPressed();
-}
-
 void ProjectorBarView::OnKeyIdeaButtonPressed() {
   DCHECK(projector_controller_);
   projector_controller_->MarkKeyIdea();
@@ -403,7 +402,7 @@
       bar_location_ = BarLocation::kUpperLeft;
       bar_location_button_->SetVectorIcon(kAutoclickPositionTopLeftIcon);
       break;
-  };
+  }
   GetWidget()->SetBounds(CalculateBoundsInScreen());
 }
 
diff --git a/ash/projector/ui/projector_bar_view.h b/ash/projector/ui/projector_bar_view.h
index 564c708..9fb44e0 100644
--- a/ash/projector/ui/projector_bar_view.h
+++ b/ash/projector/ui/projector_bar_view.h
@@ -53,7 +53,6 @@
   // views::View:
   void OnThemeChanged() override;
 
-  bool IsRecordButtonVisible() const;
   bool IsKeyIdeaButtonEnabled() const;
   bool IsClosedCaptionEnabled() const;
 
@@ -72,8 +71,6 @@
   void CreateMarkerOptionsBar();
   void CreateTrailingButtonsBar();
 
-  void OnRecordButtonPressed();
-  void OnStopButtonPressed();
   void OnKeyIdeaButtonPressed();
   void OnLaserPointerPressed();
   void OnMarkerPressed();
@@ -91,8 +88,6 @@
   void UpdateToolbarButtonsVisibility();
   gfx::Rect CalculateBoundsInScreen() const;
 
-  ProjectorColorButton* record_button_ = nullptr;
-  ProjectorColorButton* stop_button_ = nullptr;
   ProjectorButton* key_idea_button_ = nullptr;
   ProjectorButton* laser_pointer_button_ = nullptr;
   ProjectorButton* marker_button_ = nullptr;
diff --git a/ash/projector/ui/projector_button.cc b/ash/projector/ui/projector_button.cc
index 36d3ce2..c0d123b 100644
--- a/ash/projector/ui/projector_button.cc
+++ b/ash/projector/ui/projector_button.cc
@@ -21,8 +21,9 @@
 
 }  // namespace
 
-ProjectorButton::ProjectorButton(views::Button::PressedCallback callback)
-    : ToggleImageButton(callback) {
+ProjectorButton::ProjectorButton(views::Button::PressedCallback callback,
+                                 const std::u16string& name)
+    : ToggleImageButton(callback), name_(name) {
   SetPreferredSize({kProjectorButtonSize, kProjectorButtonSize});
   SetBorder(views::CreateEmptyBorder(kButtonPadding));
 
@@ -61,4 +62,9 @@
   ink_drop()->SetHighlightOpacity(ripple_attributes.highlight_opacity);
 }
 
+void ProjectorButton::GetAccessibleNodeData(ui::AXNodeData* node_data) {
+  node_data->role = ax::mojom::Role::kButton;
+  node_data->SetName(name_);
+}
+
 }  // namespace ash
diff --git a/ash/projector/ui/projector_button.h b/ash/projector/ui/projector_button.h
index bab80be..71256554 100644
--- a/ash/projector/ui/projector_button.h
+++ b/ash/projector/ui/projector_button.h
@@ -5,6 +5,8 @@
 #ifndef ASH_PROJECTOR_UI_PROJECTOR_BUTTON_H_
 #define ASH_PROJECTOR_UI_PROJECTOR_BUTTON_H_
 
+#include <string>
+
 #include "ash/ash_export.h"
 #include "ui/views/controls/button/image_button.h"
 
@@ -17,7 +19,8 @@
  public:
   const int kProjectorButtonSize = 32;
 
-  explicit ProjectorButton(views::Button::PressedCallback callback);
+  ProjectorButton(views::Button::PressedCallback callback,
+                  const std::u16string& name);
   ProjectorButton(const ProjectorButton&) = delete;
   ProjectorButton& operator=(const ProjectorButton&) = delete;
   ~ProjectorButton() override = default;
@@ -25,6 +28,10 @@
   // views::ToggleImageButton:
   void OnPaintBackground(gfx::Canvas* canvas) override;
   void OnThemeChanged() override;
+  void GetAccessibleNodeData(ui::AXNodeData* node_data) override;
+
+ private:
+  std::u16string name_;
 };
 
 }  // namespace ash
diff --git a/ash/projector/ui/projector_color_button.cc b/ash/projector/ui/projector_color_button.cc
index 47276ef..2f4d3d3 100644
--- a/ash/projector/ui/projector_color_button.cc
+++ b/ash/projector/ui/projector_color_button.cc
@@ -14,8 +14,9 @@
     views::Button::PressedCallback callback,
     SkColor color,
     int size,
-    float radius)
-    : ProjectorButton(callback) {
+    float radius,
+    const std::u16string& name)
+    : ProjectorButton(callback, name) {
   // Add the color view.
   auto* color_view = AddChildView(std::make_unique<View>());
   color_view->SetBounds((kProjectorButtonSize - size) / 2,
diff --git a/ash/projector/ui/projector_color_button.h b/ash/projector/ui/projector_color_button.h
index f51d325..dc03fba 100644
--- a/ash/projector/ui/projector_color_button.h
+++ b/ash/projector/ui/projector_color_button.h
@@ -5,6 +5,8 @@
 #ifndef ASH_PROJECTOR_UI_PROJECTOR_COLOR_BUTTON_H_
 #define ASH_PROJECTOR_UI_PROJECTOR_COLOR_BUTTON_H_
 
+#include <string>
+
 #include "ash/ash_export.h"
 #include "ash/projector/ui/projector_button.h"
 
@@ -17,7 +19,8 @@
   ProjectorColorButton(views::Button::PressedCallback callback,
                        SkColor color,
                        int size,
-                       float radius);
+                       float radius,
+                       const std::u16string& name);
   ProjectorColorButton(const ProjectorColorButton&) = delete;
   ProjectorColorButton& operator=(const ProjectorColorButton&) = delete;
   ~ProjectorColorButton() override = default;
diff --git a/ash/projector/ui/projector_image_button.cc b/ash/projector/ui/projector_image_button.cc
index e4ab674..1363f55b 100644
--- a/ash/projector/ui/projector_image_button.cc
+++ b/ash/projector/ui/projector_image_button.cc
@@ -13,8 +13,9 @@
 
 ProjectorImageButton::ProjectorImageButton(
     views::Button::PressedCallback callback,
-    const gfx::VectorIcon& icon)
-    : ProjectorButton(callback) {
+    const gfx::VectorIcon& icon,
+    const std::u16string& name)
+    : ProjectorButton(callback, name) {
   SetVectorIcon(icon);
 }
 
diff --git a/ash/projector/ui/projector_image_button.h b/ash/projector/ui/projector_image_button.h
index 1c99777..71bca31 100644
--- a/ash/projector/ui/projector_image_button.h
+++ b/ash/projector/ui/projector_image_button.h
@@ -5,6 +5,8 @@
 #ifndef ASH_PROJECTOR_UI_PROJECTOR_IMAGE_BUTTON_H_
 #define ASH_PROJECTOR_UI_PROJECTOR_IMAGE_BUTTON_H_
 
+#include <string>
+
 #include "ash/ash_export.h"
 #include "ash/projector/ui/projector_button.h"
 
@@ -19,7 +21,8 @@
 class ASH_EXPORT ProjectorImageButton : public ProjectorButton {
  public:
   ProjectorImageButton(views::Button::PressedCallback callback,
-                       const gfx::VectorIcon& icon);
+                       const gfx::VectorIcon& icon,
+                       const std::u16string& name);
   ProjectorImageButton(const ProjectorImageButton&) = delete;
   ProjectorImageButton& operator=(const ProjectorImageButton&) = delete;
   ~ProjectorImageButton() override = default;
diff --git a/ash/public/cpp/OWNERS b/ash/public/cpp/OWNERS
index f3c487d..ea1744a 100644
--- a/ash/public/cpp/OWNERS
+++ b/ash/public/cpp/OWNERS
@@ -1,2 +1,3 @@
 per-file *login*=file://ash/login/OWNERS
 per-file *shelf*=file://ash/shelf/OWNERS
+per-file *wallpaper*=file://ash/wallpaper/OWNERS
diff --git a/ash/public/cpp/app_list/app_list_features.cc b/ash/public/cpp/app_list/app_list_features.cc
index c6f062c..7cdcff0 100644
--- a/ash/public/cpp/app_list/app_list_features.cc
+++ b/ash/public/cpp/app_list/app_list_features.cc
@@ -52,7 +52,7 @@
 const base::Feature kCategoricalSearch{"CategoricalSearch",
                                        base::FEATURE_DISABLED_BY_DEFAULT};
 const base::Feature kLauncherQueryHighlighting{
-    "LauncherQueryHighlighting", base::FEATURE_DISABLED_BY_DEFAULT};
+    "LauncherQueryHighlighting", base::FEATURE_ENABLED_BY_DEFAULT};
 
 bool IsAppDataSearchEnabled() {
   return base::FeatureList::IsEnabled(kEnableAppDataSearch);
diff --git a/ash/public/cpp/projector/projector_client.h b/ash/public/cpp/projector/projector_client.h
index 317d1b7..b96609c 100644
--- a/ash/public/cpp/projector/projector_client.h
+++ b/ash/public/cpp/projector/projector_client.h
@@ -20,6 +20,12 @@
 
   virtual void StartSpeechRecognition() = 0;
   virtual void StopSpeechRecognition() = 0;
+
+  // TODO(crbug/1199396): Migrate to IPC after Lacros launch and ash-chrome
+  // deprecation.
+  virtual void ShowSelfieCam() = 0;
+  virtual void CloseSelfieCam() = 0;
+  virtual bool IsSelfieCamVisible() const = 0;
 };
 
 }  // namespace ash
diff --git a/ash/system/accessibility/floating_accessibility_view.cc b/ash/system/accessibility/floating_accessibility_view.cc
index aadd4c5b..575ab620 100644
--- a/ash/system/accessibility/floating_accessibility_view.cc
+++ b/ash/system/accessibility/floating_accessibility_view.cc
@@ -15,7 +15,7 @@
 #include "ash/style/ash_color_provider.h"
 #include "ash/system/accessibility/dictation_button_tray.h"
 #include "ash/system/accessibility/floating_menu_button.h"
-#include "ash/system/accessibility/select_to_speak_tray.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_tray.h"
 #include "ash/system/tray/tray_constants.h"
 #include "ash/system/virtual_keyboard/virtual_keyboard_tray.h"
 #include "base/bind.h"
diff --git a/ash/system/accessibility/select_to_speak_constants.h b/ash/system/accessibility/select_to_speak/select_to_speak_constants.h
similarity index 82%
rename from ash/system/accessibility/select_to_speak_constants.h
rename to ash/system/accessibility/select_to_speak/select_to_speak_constants.h
index 8265fc2..4e0c775 100644
--- a/ash/system/accessibility/select_to_speak_constants.h
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_constants.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 ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_CONSTANTS_H_
-#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_CONSTANTS_H_
+#ifndef ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_CONSTANTS_H_
+#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_CONSTANTS_H_
 
 namespace ash {
 
@@ -28,4 +28,4 @@
 
 }  // namespace ash
 
-#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_CONSTANTS_H_
\ No newline at end of file
+#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_CONSTANTS_H_
diff --git a/ash/system/accessibility/select_to_speak_menu_bubble_controller.cc b/ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.cc
similarity index 96%
rename from ash/system/accessibility/select_to_speak_menu_bubble_controller.cc
rename to ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.cc
index a9fc72a0b..1085727 100644
--- a/ash/system/accessibility/select_to_speak_menu_bubble_controller.cc
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.cc
@@ -2,14 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/select_to_speak_menu_bubble_controller.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.h"
 
 #include "ash/accessibility/accessibility_controller_impl.h"
 #include "ash/public/cpp/shell_window_ids.h"
 #include "ash/shell.h"
 #include "ash/strings/grit/ash_strings.h"
 #include "ash/system/accessibility/floating_menu_utils.h"
-#include "ash/system/accessibility/select_to_speak_constants.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_constants.h"
 #include "ash/system/tray/tray_background_view.h"
 #include "ash/system/tray/tray_constants.h"
 #include "ash/system/unified/unified_system_tray_view.h"
diff --git a/ash/system/accessibility/select_to_speak_menu_bubble_controller.h b/ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.h
similarity index 80%
rename from ash/system/accessibility/select_to_speak_menu_bubble_controller.h
rename to ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.h
index 1966cb25..4d24733 100644
--- a/ash/system/accessibility/select_to_speak_menu_bubble_controller.h
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.h
@@ -2,13 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_MENU_BUBBLE_CONTROLLER_H_
-#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_MENU_BUBBLE_CONTROLLER_H_
+#ifndef ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_MENU_BUBBLE_CONTROLLER_H_
+#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_MENU_BUBBLE_CONTROLLER_H_
 
 #include "ash/ash_export.h"
 #include "ash/public/cpp/accessibility_controller_enums.h"
-#include "ash/system/accessibility/select_to_speak_menu_view.h"
-#include "ash/system/accessibility/select_to_speak_speed_bubble_controller.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_menu_view.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller.h"
 #include "ash/system/tray/tray_bubble_view.h"
 #include "ui/wm/public/activation_change_observer.h"
 
@@ -62,4 +62,4 @@
 
 }  // namespace ash
 
-#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_MENU_BUBBLE_CONTROLLER_H_
+#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_MENU_BUBBLE_CONTROLLER_H_
diff --git a/ash/system/accessibility/select_to_speak_menu_bubble_controller_unittest.cc b/ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller_unittest.cc
similarity index 97%
rename from ash/system/accessibility/select_to_speak_menu_bubble_controller_unittest.cc
rename to ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller_unittest.cc
index 7f23a32..b804802a 100644
--- a/ash/system/accessibility/select_to_speak_menu_bubble_controller_unittest.cc
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller_unittest.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/select_to_speak_menu_bubble_controller.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.h"
 
 #include "ash/accessibility/accessibility_controller_impl.h"
 #include "ash/accessibility/test_accessibility_controller_client.h"
@@ -10,7 +10,7 @@
 #include "ash/shell.h"
 #include "ash/strings/grit/ash_strings.h"
 #include "ash/system/accessibility/floating_menu_button.h"
-#include "ash/system/accessibility/select_to_speak_menu_view.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_menu_view.h"
 #include "ash/test/ash_test_base.h"
 #include "base/test/scoped_feature_list.h"
 #include "ui/accessibility/accessibility_features.h"
@@ -274,4 +274,4 @@
               !GetSpeedBubbleController()->IsVisible());
 }
 
-}  // namespace ash
\ No newline at end of file
+}  // namespace ash
diff --git a/ash/system/accessibility/select_to_speak_menu_view.cc b/ash/system/accessibility/select_to_speak/select_to_speak_menu_view.cc
similarity index 97%
rename from ash/system/accessibility/select_to_speak_menu_view.cc
rename to ash/system/accessibility/select_to_speak/select_to_speak_menu_view.cc
index 9089141..37d91f9 100644
--- a/ash/system/accessibility/select_to_speak_menu_view.cc
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_menu_view.cc
@@ -2,15 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/select_to_speak_menu_view.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_menu_view.h"
 
 #include "ash/public/cpp/accessibility_controller_enums.h"
 #include "ash/resources/vector_icons/vector_icons.h"
 #include "ash/strings/grit/ash_strings.h"
 #include "ash/style/ash_color_provider.h"
 #include "ash/system/accessibility/floating_menu_button.h"
-#include "ash/system/accessibility/select_to_speak_constants.h"
-#include "ash/system/accessibility/select_to_speak_metrics_utils.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_constants.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_metrics_utils.h"
 #include "ash/system/tray/tray_constants.h"
 #include "base/bind.h"
 #include "base/i18n/rtl.h"
diff --git a/ash/system/accessibility/select_to_speak_menu_view.h b/ash/system/accessibility/select_to_speak/select_to_speak_menu_view.h
similarity index 91%
rename from ash/system/accessibility/select_to_speak_menu_view.h
rename to ash/system/accessibility/select_to_speak/select_to_speak_menu_view.h
index 8d6a1d6e..0044369 100644
--- a/ash/system/accessibility/select_to_speak_menu_view.h
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_menu_view.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 ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_MENU_VIEW_H_
-#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_MENU_VIEW_H_
+#ifndef ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_MENU_VIEW_H_
+#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_MENU_VIEW_H_
 
 #include "ash/ash_export.h"
 #include "ash/public/cpp/accessibility_controller_enums.h"
@@ -88,4 +88,4 @@
 
 DEFINE_VIEW_BUILDER(/* no export */, ash::SelectToSpeakMenuView)
 
-#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_MENU_VIEW_H_
\ No newline at end of file
+#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_MENU_VIEW_H_
diff --git a/ash/system/accessibility/select_to_speak_metrics_utils.h b/ash/system/accessibility/select_to_speak/select_to_speak_metrics_utils.h
similarity index 76%
rename from ash/system/accessibility/select_to_speak_metrics_utils.h
rename to ash/system/accessibility/select_to_speak/select_to_speak_metrics_utils.h
index ed33bee..460fe413 100644
--- a/ash/system/accessibility/select_to_speak_metrics_utils.h
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_metrics_utils.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 ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_METRICS_UTILS_H_
-#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_METRICS_UTILS_H_
+#ifndef ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_METRICS_UTILS_H_
+#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_METRICS_UTILS_H_
 
 namespace ash {
 
@@ -23,4 +23,4 @@
 
 }  // namespace ash
 
-#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_METRICS_UTILS_H_
+#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_METRICS_UTILS_H_
diff --git a/ash/system/accessibility/select_to_speak_speed_bubble_controller.cc b/ash/system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller.cc
similarity index 93%
rename from ash/system/accessibility/select_to_speak_speed_bubble_controller.cc
rename to ash/system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller.cc
index cbe045c3..2ec2262 100644
--- a/ash/system/accessibility/select_to_speak_speed_bubble_controller.cc
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller.cc
@@ -2,15 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/select_to_speak_speed_bubble_controller.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller.h"
 
 #include "ash/accessibility/accessibility_controller_impl.h"
 #include "ash/public/cpp/accessibility_controller_enums.h"
 #include "ash/public/cpp/shell_window_ids.h"
 #include "ash/shell.h"
 #include "ash/system/accessibility/floating_menu_utils.h"
-#include "ash/system/accessibility/select_to_speak_constants.h"
-#include "ash/system/accessibility/select_to_speak_speed_view.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_constants.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_speed_view.h"
 #include "ash/system/tray/tray_background_view.h"
 #include "ash/system/tray/tray_constants.h"
 #include "ash/system/unified/unified_system_tray_view.h"
diff --git a/ash/system/accessibility/select_to_speak_speed_bubble_controller.h b/ash/system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller.h
similarity index 84%
rename from ash/system/accessibility/select_to_speak_speed_bubble_controller.h
rename to ash/system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller.h
index f0ecc46..681ea356 100644
--- a/ash/system/accessibility/select_to_speak_speed_bubble_controller.h
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller.h
@@ -2,11 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SPEED_BUBBLE_CONTROLLER_H_
-#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SPEED_BUBBLE_CONTROLLER_H_
+#ifndef ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_SPEED_BUBBLE_CONTROLLER_H_
+#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_SPEED_BUBBLE_CONTROLLER_H_
 
 #include "ash/ash_export.h"
-#include "ash/system/accessibility/select_to_speak_speed_view.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_speed_view.h"
 #include "ash/system/tray/tray_bubble_view.h"
 #include "ash/system/tray/view_click_listener.h"
 #include "ui/wm/public/activation_change_observer.h"
@@ -61,4 +61,4 @@
 
 }  // namespace ash
 
-#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SPEED_BUBBLE_CONTROLLER_H_
+#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_SPEED_BUBBLE_CONTROLLER_H_
diff --git a/ash/system/accessibility/select_to_speak_speed_bubble_controller_unittest.cc b/ash/system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller_unittest.cc
similarity index 94%
rename from ash/system/accessibility/select_to_speak_speed_bubble_controller_unittest.cc
rename to ash/system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller_unittest.cc
index 0677a44e..871acc2d 100644
--- a/ash/system/accessibility/select_to_speak_speed_bubble_controller_unittest.cc
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller_unittest.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/select_to_speak_speed_bubble_controller.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_speed_bubble_controller.h"
 
 #include "ash/accessibility/accessibility_controller_impl.h"
 #include "ash/accessibility/test_accessibility_controller_client.h"
@@ -10,9 +10,9 @@
 #include "ash/shell.h"
 #include "ash/strings/grit/ash_strings.h"
 #include "ash/system/accessibility/floating_menu_button.h"
-#include "ash/system/accessibility/select_to_speak_menu_bubble_controller.h"
-#include "ash/system/accessibility/select_to_speak_menu_view.h"
-#include "ash/system/accessibility/select_to_speak_speed_view.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_menu_view.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_speed_view.h"
 #include "ash/system/tray/hover_highlight_view.h"
 #include "ash/test/ash_test_base.h"
 #include "base/test/scoped_feature_list.h"
@@ -173,4 +173,4 @@
   EXPECT_TRUE(speed_button->HasFocus());
 }
 
-}  // namespace ash
\ No newline at end of file
+}  // namespace ash
diff --git a/ash/system/accessibility/select_to_speak_speed_view.cc b/ash/system/accessibility/select_to_speak/select_to_speak_speed_view.cc
similarity index 94%
rename from ash/system/accessibility/select_to_speak_speed_view.cc
rename to ash/system/accessibility/select_to_speak/select_to_speak_speed_view.cc
index 8e87701..d3fcf02c 100644
--- a/ash/system/accessibility/select_to_speak_speed_view.cc
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_speed_view.cc
@@ -2,11 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/select_to_speak_speed_view.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_speed_view.h"
 
 #include "ash/strings/grit/ash_strings.h"
 #include "ash/style/ash_color_provider.h"
-#include "ash/system/accessibility/select_to_speak_constants.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_constants.h"
 #include "ash/system/tray/hover_highlight_view.h"
 #include "ash/system/tray/tray_popup_utils.h"
 #include "base/bind.h"
diff --git a/ash/system/accessibility/select_to_speak_speed_view.h b/ash/system/accessibility/select_to_speak/select_to_speak_speed_view.h
similarity index 88%
rename from ash/system/accessibility/select_to_speak_speed_view.h
rename to ash/system/accessibility/select_to_speak/select_to_speak_speed_view.h
index 376b836e..9dc13b1 100644
--- a/ash/system/accessibility/select_to_speak_speed_view.h
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_speed_view.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 ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SPEED_VIEW_H_
-#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SPEED_VIEW_H_
+#ifndef ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_SPEED_VIEW_H_
+#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_SPEED_VIEW_H_
 
 #include "ash/ash_export.h"
 #include "ash/system/tray/view_click_listener.h"
@@ -64,4 +64,4 @@
 
 DEFINE_VIEW_BUILDER(/* no export */, ash::SelectToSpeakSpeedView)
 
-#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SPEED_VIEW_H_
\ No newline at end of file
+#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_SPEED_VIEW_H_
diff --git a/ash/system/accessibility/select_to_speak_tray.cc b/ash/system/accessibility/select_to_speak/select_to_speak_tray.cc
similarity index 97%
rename from ash/system/accessibility/select_to_speak_tray.cc
rename to ash/system/accessibility/select_to_speak/select_to_speak_tray.cc
index fd57751..fb3e0d2 100644
--- a/ash/system/accessibility/select_to_speak_tray.cc
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_tray.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/select_to_speak_tray.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_tray.h"
 
 #include "ash/accessibility/accessibility_controller_impl.h"
 #include "ash/resources/vector_icons/vector_icons.h"
@@ -31,7 +31,6 @@
 
 SelectToSpeakTray::SelectToSpeakTray(Shelf* shelf)
     : TrayBackgroundView(shelf), icon_(new views::ImageView()) {
-
   UpdateIconsForSession();
   icon_->SetImage(inactive_image_);
   const int vertical_padding = (kTrayItemSize - inactive_image_.height()) / 2;
diff --git a/ash/system/accessibility/select_to_speak_tray.h b/ash/system/accessibility/select_to_speak/select_to_speak_tray.h
similarity index 90%
rename from ash/system/accessibility/select_to_speak_tray.h
rename to ash/system/accessibility/select_to_speak/select_to_speak_tray.h
index 69ed5c9..c1bacf5 100644
--- a/ash/system/accessibility/select_to_speak_tray.h
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_tray.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 ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_TRAY_H_
-#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_TRAY_H_
+#ifndef ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_TRAY_H_
+#define ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_TRAY_H_
 
 #include "ash/accessibility/accessibility_observer.h"
 #include "ash/ash_export.h"
@@ -67,4 +67,4 @@
 
 }  // namespace ash
 
-#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_TRAY_H_
+#endif  // ASH_SYSTEM_ACCESSIBILITY_SELECT_TO_SPEAK_SELECT_TO_SPEAK_TRAY_H_
diff --git a/ash/system/accessibility/select_to_speak_tray_unittest.cc b/ash/system/accessibility/select_to_speak/select_to_speak_tray_unittest.cc
similarity index 98%
rename from ash/system/accessibility/select_to_speak_tray_unittest.cc
rename to ash/system/accessibility/select_to_speak/select_to_speak_tray_unittest.cc
index 7fbfdb47..1e869ab 100644
--- a/ash/system/accessibility/select_to_speak_tray_unittest.cc
+++ b/ash/system/accessibility/select_to_speak/select_to_speak_tray_unittest.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/select_to_speak_tray.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_tray.h"
 
 #include "ash/accelerators/accelerator_controller_impl.h"
 #include "ash/accessibility/accessibility_controller_impl.h"
diff --git a/ash/system/accessibility/switch_access_back_button_bubble_controller.cc b/ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller.cc
similarity index 96%
rename from ash/system/accessibility/switch_access_back_button_bubble_controller.cc
rename to ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller.cc
index 571936d..8abb16fcc 100644
--- a/ash/system/accessibility/switch_access_back_button_bubble_controller.cc
+++ b/ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller.cc
@@ -2,11 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/switch_access_back_button_bubble_controller.h"
+#include "ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller.h"
 
 #include "ash/public/cpp/shell_window_ids.h"
 #include "ash/shell.h"
-#include "ash/system/accessibility/switch_access_back_button_view.h"
+#include "ash/system/accessibility/switch_access/switch_access_back_button_view.h"
 #include "ash/system/tray/tray_background_view.h"
 #include "ash/system/tray/tray_constants.h"
 #include "ui/accessibility/ax_enums.mojom.h"
diff --git a/ash/system/accessibility/switch_access_back_button_bubble_controller.h b/ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller.h
similarity index 84%
rename from ash/system/accessibility/switch_access_back_button_bubble_controller.h
rename to ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller.h
index 77f6599..a303238 100644
--- a/ash/system/accessibility/switch_access_back_button_bubble_controller.h
+++ b/ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller.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 ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_BACK_BUTTON_BUBBLE_CONTROLLER_H_
-#define ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_BACK_BUTTON_BUBBLE_CONTROLLER_H_
+#ifndef ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_BACK_BUTTON_BUBBLE_CONTROLLER_H_
+#define ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_BACK_BUTTON_BUBBLE_CONTROLLER_H_
 
 #include "ash/system/tray/tray_bubble_view.h"
 
@@ -54,4 +54,4 @@
 
 }  // namespace ash
 
-#endif  // ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_BACK_BUTTON_BUBBLE_CONTROLLER_H_
+#endif  // ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_BACK_BUTTON_BUBBLE_CONTROLLER_H_
diff --git a/ash/system/accessibility/switch_access_back_button_bubble_controller_unittest.cc b/ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller_unittest.cc
similarity index 91%
rename from ash/system/accessibility/switch_access_back_button_bubble_controller_unittest.cc
rename to ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller_unittest.cc
index 5d9e745..538fc6a 100644
--- a/ash/system/accessibility/switch_access_back_button_bubble_controller_unittest.cc
+++ b/ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller_unittest.cc
@@ -2,13 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/switch_access_back_button_bubble_controller.h"
+#include "ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller.h"
 
 #include "ash/accessibility/accessibility_controller_impl.h"
 #include "ash/shell.h"
-#include "ash/system/accessibility/switch_access_back_button_bubble_controller.h"
-#include "ash/system/accessibility/switch_access_back_button_view.h"
-#include "ash/system/accessibility/switch_access_menu_bubble_controller.h"
+#include "ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller.h"
+#include "ash/system/accessibility/switch_access/switch_access_back_button_view.h"
+#include "ash/system/accessibility/switch_access/switch_access_menu_bubble_controller.h"
 #include "ash/test/ash_test_base.h"
 #include "ui/display/display.h"
 #include "ui/display/screen.h"
diff --git a/ash/system/accessibility/switch_access_back_button_view.cc b/ash/system/accessibility/switch_access/switch_access_back_button_view.cc
similarity index 97%
rename from ash/system/accessibility/switch_access_back_button_view.cc
rename to ash/system/accessibility/switch_access/switch_access_back_button_view.cc
index 763c5f9..0e14318 100644
--- a/ash/system/accessibility/switch_access_back_button_view.cc
+++ b/ash/system/accessibility/switch_access/switch_access_back_button_view.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/switch_access_back_button_view.h"
+#include "ash/system/accessibility/switch_access/switch_access_back_button_view.h"
 
 #include "ash/resources/vector_icons/vector_icons.h"
 #include "ash/strings/grit/ash_strings.h"
diff --git a/ash/system/accessibility/switch_access_back_button_view.h b/ash/system/accessibility/switch_access/switch_access_back_button_view.h
similarity index 85%
rename from ash/system/accessibility/switch_access_back_button_view.h
rename to ash/system/accessibility/switch_access/switch_access_back_button_view.h
index 2b23d9b..88d4023 100644
--- a/ash/system/accessibility/switch_access_back_button_view.h
+++ b/ash/system/accessibility/switch_access/switch_access_back_button_view.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 ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_BACK_BUTTON_VIEW_H_
-#define ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_BACK_BUTTON_VIEW_H_
+#ifndef ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_BACK_BUTTON_VIEW_H_
+#define ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_BACK_BUTTON_VIEW_H_
 
 #include "ui/base/metadata/metadata_header_macros.h"
 #include "ui/views/layout/box_layout_view.h"
@@ -53,4 +53,4 @@
 
 DEFINE_VIEW_BUILDER(/* no export */, ash::SwitchAccessBackButtonView)
 
-#endif  // ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_BACK_BUTTON_VIEW_H_
+#endif  // ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_BACK_BUTTON_VIEW_H_
diff --git a/ash/system/accessibility/switch_access_menu_bubble_controller.cc b/ash/system/accessibility/switch_access/switch_access_menu_bubble_controller.cc
similarity index 94%
rename from ash/system/accessibility/switch_access_menu_bubble_controller.cc
rename to ash/system/accessibility/switch_access/switch_access_menu_bubble_controller.cc
index 6b268707..8c98b01 100644
--- a/ash/system/accessibility/switch_access_menu_bubble_controller.cc
+++ b/ash/system/accessibility/switch_access/switch_access_menu_bubble_controller.cc
@@ -2,12 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/switch_access_menu_bubble_controller.h"
+#include "ash/system/accessibility/switch_access/switch_access_menu_bubble_controller.h"
 
 #include "ash/public/cpp/shell_window_ids.h"
 #include "ash/shell.h"
-#include "ash/system/accessibility/switch_access_back_button_bubble_controller.h"
-#include "ash/system/accessibility/switch_access_menu_view.h"
+#include "ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller.h"
+#include "ash/system/accessibility/switch_access/switch_access_menu_view.h"
 #include "ash/system/tray/tray_background_view.h"
 #include "ash/system/tray/tray_constants.h"
 #include "ash/system/unified/unified_system_tray_view.h"
diff --git a/ash/system/accessibility/switch_access_menu_bubble_controller.h b/ash/system/accessibility/switch_access/switch_access_menu_bubble_controller.h
similarity index 84%
rename from ash/system/accessibility/switch_access_menu_bubble_controller.h
rename to ash/system/accessibility/switch_access/switch_access_menu_bubble_controller.h
index 4a629f6..cbbf7212 100644
--- a/ash/system/accessibility/switch_access_menu_bubble_controller.h
+++ b/ash/system/accessibility/switch_access/switch_access_menu_bubble_controller.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 ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_MENU_BUBBLE_CONTROLLER_H_
-#define ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_MENU_BUBBLE_CONTROLLER_H_
+#ifndef ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_MENU_BUBBLE_CONTROLLER_H_
+#define ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_MENU_BUBBLE_CONTROLLER_H_
 
 #include "ash/system/tray/tray_bubble_view.h"
 
@@ -53,4 +53,4 @@
 
 }  // namespace ash
 
-#endif  // ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_MENU_BUBBLE_CONTROLLER_H_
+#endif  // ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_MENU_BUBBLE_CONTROLLER_H_
diff --git a/ash/system/accessibility/switch_access_menu_bubble_controller_unittest.cc b/ash/system/accessibility/switch_access/switch_access_menu_bubble_controller_unittest.cc
similarity index 92%
rename from ash/system/accessibility/switch_access_menu_bubble_controller_unittest.cc
rename to ash/system/accessibility/switch_access/switch_access_menu_bubble_controller_unittest.cc
index 303f8c4..0e35f1c7 100644
--- a/ash/system/accessibility/switch_access_menu_bubble_controller_unittest.cc
+++ b/ash/system/accessibility/switch_access/switch_access_menu_bubble_controller_unittest.cc
@@ -2,14 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/switch_access_menu_bubble_controller.h"
+#include "ash/system/accessibility/switch_access/switch_access_menu_bubble_controller.h"
 
 #include "ash/accessibility/accessibility_controller_impl.h"
 #include "ash/shell.h"
-#include "ash/system/accessibility/switch_access_back_button_bubble_controller.h"
-#include "ash/system/accessibility/switch_access_back_button_view.h"
-#include "ash/system/accessibility/switch_access_menu_button.h"
-#include "ash/system/accessibility/switch_access_menu_view.h"
+#include "ash/system/accessibility/switch_access/switch_access_back_button_bubble_controller.h"
+#include "ash/system/accessibility/switch_access/switch_access_back_button_view.h"
+#include "ash/system/accessibility/switch_access/switch_access_menu_button.h"
+#include "ash/system/accessibility/switch_access/switch_access_menu_view.h"
 #include "ash/system/unified/unified_system_tray.h"
 #include "ash/test/ash_test_base.h"
 
diff --git a/ash/system/accessibility/switch_access_menu_button.cc b/ash/system/accessibility/switch_access/switch_access_menu_button.cc
similarity index 97%
rename from ash/system/accessibility/switch_access_menu_button.cc
rename to ash/system/accessibility/switch_access/switch_access_menu_button.cc
index 965a9e1..5c8f156d 100644
--- a/ash/system/accessibility/switch_access_menu_button.cc
+++ b/ash/system/accessibility/switch_access/switch_access_menu_button.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/switch_access_menu_button.h"
+#include "ash/system/accessibility/switch_access/switch_access_menu_button.h"
 
 #include "ash/style/ash_color_provider.h"
 #include "base/bind.h"
diff --git a/ash/system/accessibility/switch_access_menu_button.h b/ash/system/accessibility/switch_access/switch_access_menu_button.h
similarity index 85%
rename from ash/system/accessibility/switch_access_menu_button.h
rename to ash/system/accessibility/switch_access/switch_access_menu_button.h
index 3af0b4bb..819c6cf 100644
--- a/ash/system/accessibility/switch_access_menu_button.h
+++ b/ash/system/accessibility/switch_access/switch_access_menu_button.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 ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_MENU_BUTTON_H_
-#define ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_MENU_BUTTON_H_
+#ifndef ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_MENU_BUTTON_H_
+#define ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_MENU_BUTTON_H_
 
 #include "ui/base/metadata/metadata_header_macros.h"
 #include "ui/views/controls/button/button.h"
@@ -56,4 +56,4 @@
 
 DEFINE_VIEW_BUILDER(/* no export */, ash::SwitchAccessMenuButton)
 
-#endif  // ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_MENU_BUTTON_H_
+#endif  // ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_MENU_BUTTON_H_
diff --git a/ash/system/accessibility/switch_access_menu_view.cc b/ash/system/accessibility/switch_access/switch_access_menu_view.cc
similarity index 97%
rename from ash/system/accessibility/switch_access_menu_view.cc
rename to ash/system/accessibility/switch_access/switch_access_menu_view.cc
index 7ae9dd9..191720b 100644
--- a/ash/system/accessibility/switch_access_menu_view.cc
+++ b/ash/system/accessibility/switch_access/switch_access_menu_view.cc
@@ -2,13 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/system/accessibility/switch_access_menu_view.h"
+#include "ash/system/accessibility/switch_access/switch_access_menu_view.h"
 
 #include "ash/accessibility/accessibility_controller_impl.h"
 #include "ash/resources/vector_icons/vector_icons.h"
 #include "ash/shell.h"
 #include "ash/strings/grit/ash_strings.h"
-#include "ash/system/accessibility/switch_access_menu_button.h"
+#include "ash/system/accessibility/switch_access/switch_access_menu_button.h"
 #include "ash/system/tray/tray_constants.h"
 #include "base/containers/flat_map.h"
 #include "base/no_destructor.h"
diff --git a/ash/system/accessibility/switch_access_menu_view.h b/ash/system/accessibility/switch_access/switch_access_menu_view.h
similarity index 79%
rename from ash/system/accessibility/switch_access_menu_view.h
rename to ash/system/accessibility/switch_access/switch_access_menu_view.h
index 0643cf1..08a0367 100644
--- a/ash/system/accessibility/switch_access_menu_view.h
+++ b/ash/system/accessibility/switch_access/switch_access_menu_view.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 ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_MENU_VIEW_H_
-#define ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_MENU_VIEW_H_
+#ifndef ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_MENU_VIEW_H_
+#define ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_MENU_VIEW_H_
 
 #include <vector>
 
@@ -34,4 +34,4 @@
 
 }  // namespace ash
 
-#endif  // ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_MENU_VIEW_H_
+#endif  // ASH_SYSTEM_ACCESSIBILITY_SWITCH_ACCESS_SWITCH_ACCESS_MENU_VIEW_H_
diff --git a/ash/system/status_area_widget.cc b/ash/system/status_area_widget.cc
index 03a1591c..5f068f1 100644
--- a/ash/system/status_area_widget.cc
+++ b/ash/system/status_area_widget.cc
@@ -16,7 +16,7 @@
 #include "ash/shelf/shelf_widget.h"
 #include "ash/shell.h"
 #include "ash/system/accessibility/dictation_button_tray.h"
-#include "ash/system/accessibility/select_to_speak_tray.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_tray.h"
 #include "ash/system/holding_space/holding_space_tray.h"
 #include "ash/system/ime_menu/ime_menu_tray.h"
 #include "ash/system/media/media_tray.h"
diff --git a/ash/system/status_area_widget_unittest.cc b/ash/system/status_area_widget_unittest.cc
index bb3a80c..e6c75f7 100644
--- a/ash/system/status_area_widget_unittest.cc
+++ b/ash/system/status_area_widget_unittest.cc
@@ -18,7 +18,7 @@
 #include "ash/session/test_session_controller_client.h"
 #include "ash/shell.h"
 #include "ash/system/accessibility/dictation_button_tray.h"
-#include "ash/system/accessibility/select_to_speak_tray.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_tray.h"
 #include "ash/system/ime_menu/ime_menu_tray.h"
 #include "ash/system/model/system_tray_model.h"
 #include "ash/system/model/virtual_keyboard_model.h"
diff --git a/ash/system/unified/system_tray_test_api.cc b/ash/system/unified/system_tray_test_api.cc
index 72e9b40..c6ecfbd 100644
--- a/ash/system/unified/system_tray_test_api.cc
+++ b/ash/system/unified/system_tray_test_api.cc
@@ -8,7 +8,7 @@
 
 #include "ash/root_window_controller.h"
 #include "ash/shell.h"
-#include "ash/system/accessibility/select_to_speak_tray.h"
+#include "ash/system/accessibility/select_to_speak/select_to_speak_tray.h"
 #include "ash/system/status_area_widget.h"
 #include "ash/system/time/time_tray_item_view.h"
 #include "ash/system/time/time_view.h"
diff --git a/ash/wm/overview/overview_session.cc b/ash/wm/overview/overview_session.cc
index 60e7888..837ba95 100644
--- a/ash/wm/overview/overview_session.cc
+++ b/ash/wm/overview/overview_session.cc
@@ -391,7 +391,12 @@
           }
           wm::ActivateWindow(window_state->window());
         }));
-    window->Show();
+    // If we are in split mode, use Show() here to delegate un-minimizing to
+    // SplitViewController as it handles auto snapping cases.
+    if (SplitViewController::Get(window)->InSplitViewMode())
+      window->Show();
+    else
+      window_state->Unminimize();
     return;
   }
 
diff --git a/ash/wm/overview/overview_session_unittest.cc b/ash/wm/overview/overview_session_unittest.cc
index c914e10..ad6f27f 100644
--- a/ash/wm/overview/overview_session_unittest.cc
+++ b/ash/wm/overview/overview_session_unittest.cc
@@ -155,27 +155,28 @@
   DISALLOW_COPY_AND_ASSIGN(TweenTester);
 };
 
-// Class which tracks if a given widget has been closed.
-class TestClosedWidgetObserver : public views::WidgetObserver {
+// Class which tracks if a given widget has been destroyed.
+class TestDestroyedWidgetObserver : public views::WidgetObserver {
  public:
-  explicit TestClosedWidgetObserver(views::Widget* widget) {
+  explicit TestDestroyedWidgetObserver(views::Widget* widget) {
     DCHECK(widget);
     observation_.Observe(widget);
   }
-  TestClosedWidgetObserver(const TestClosedWidgetObserver&) = delete;
-  TestClosedWidgetObserver& operator=(const TestClosedWidgetObserver&) = delete;
-  ~TestClosedWidgetObserver() override = default;
+  TestDestroyedWidgetObserver(const TestDestroyedWidgetObserver&) = delete;
+  TestDestroyedWidgetObserver& operator=(const TestDestroyedWidgetObserver&) =
+      delete;
+  ~TestDestroyedWidgetObserver() override = default;
 
   // views::WidgetObserver:
-  void OnWidgetClosing(views::Widget* widget) override {
-    DCHECK(!widget_closed_);
-    widget_closed_ = true;
+  void OnWidgetDestroyed(views::Widget* widget) override {
+    DCHECK(!widget_destroyed_);
+    widget_destroyed_ = true;
   }
 
-  bool widget_closed() const { return widget_closed_; }
+  bool widget_destroyed() const { return widget_destroyed_; }
 
  private:
-  bool widget_closed_ = false;
+  bool widget_destroyed_ = false;
 
   base::ScopedObservation<views::Widget, views::WidgetObserver> observation_{
       this};
@@ -3259,22 +3260,27 @@
   ToggleOverview();
 }
 
-// Tests that closing the overview item closes the entire transient tree.
+// Tests that closing the overview item destroys the entire transient tree. Note
+// that closing does not destroy transient children which are ShellSurfaceBase,
+// but this test covers the regular case.
 TEST_F(OverviewSessionTest, ClosingTransientTree) {
   auto widget = CreateTestWidget();
   aura::Window* window = widget->GetNativeWindow();
   auto child_widget = CreateTestWidget();
   ::wm::AddTransientChild(window, child_widget->GetNativeWindow());
 
-  TestClosedWidgetObserver widget_observer(widget.get());
-  TestClosedWidgetObserver child_widget_observer(child_widget.get());
+  TestDestroyedWidgetObserver widget_observer(widget.get());
+  TestDestroyedWidgetObserver child_widget_observer(child_widget.get());
 
   ToggleOverview();
   OverviewItem* item = GetOverviewItemForWindow(window);
   item->CloseWindow();
 
-  EXPECT_TRUE(widget_observer.widget_closed());
-  EXPECT_TRUE(child_widget_observer.widget_closed());
+  // `NativeWidgetAura::Close()` fires a post task.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_TRUE(widget_observer.widget_destroyed());
+  EXPECT_TRUE(child_widget_observer.widget_destroyed());
 }
 
 class TabletModeOverviewSessionTest : public OverviewSessionTest {
diff --git a/ash/wm/overview/scoped_overview_transform_window.cc b/ash/wm/overview/scoped_overview_transform_window.cc
index 99dd92d..9e47192e 100644
--- a/ash/wm/overview/scoped_overview_transform_window.cc
+++ b/ash/wm/overview/scoped_overview_transform_window.cc
@@ -595,13 +595,9 @@
 }
 
 void ScopedOverviewTransformWindow::CloseWidget() {
-  // Close all the windows in the transient tree. Note that is only really
-  // necessary for exo windows, as non-exo windows we would only need to close
-  // the tranisent root. We will close all widgets in the tree for both exo and
-  // non-exo anyways to simplify things and Widget::CloseWithReason handles this
-  // nicely.
-  for (auto* transient : GetTransientTreeIterator(window_))
-    window_util::CloseWidgetForWindow(transient);
+  aura::Window* parent_window = wm::GetTransientRoot(window_);
+  if (parent_window)
+    window_util::CloseWidgetForWindow(parent_window);
 }
 
 void ScopedOverviewTransformWindow::AddHiddenTransientWindows(
diff --git a/base/allocator/allocator_shim_default_dispatch_to_partition_alloc.cc b/base/allocator/allocator_shim_default_dispatch_to_partition_alloc.cc
index 91d0ad3..049fda0 100644
--- a/base/allocator/allocator_shim_default_dispatch_to_partition_alloc.cc
+++ b/base/allocator/allocator_shim_default_dispatch_to_partition_alloc.cc
@@ -258,7 +258,9 @@
   if (alignment <= base::kAlignment) {
     // This is mandated by |posix_memalign()| and friends, so should never fire.
     PA_CHECK(base::bits::IsPowerOfTwo(alignment));
-    return Allocator()->AllocFlagsNoHooks(0, size);
+    // TODO(bartekn): See if the compiler optimizes branches down the stack on
+    // Mac, where PartitionPageSize() isn't constexpr.
+    return Allocator()->AllocFlagsNoHooks(0, size, base::PartitionPageSize());
   }
 
   return AlignedAllocator()->AlignedAllocFlags(base::PartitionAllocNoHooks,
@@ -271,14 +273,16 @@
 namespace internal {
 
 void* PartitionMalloc(const AllocatorDispatch*, size_t size, void* context) {
-  return Allocator()->AllocFlagsNoHooks(0, MaybeAdjustSize(size));
+  return Allocator()->AllocFlagsNoHooks(0, MaybeAdjustSize(size),
+                                        PartitionPageSize());
 }
 
 void* PartitionMallocUnchecked(const AllocatorDispatch*,
                                size_t size,
                                void* context) {
   return Allocator()->AllocFlagsNoHooks(base::PartitionAllocReturnNull,
-                                        MaybeAdjustSize(size));
+                                        MaybeAdjustSize(size),
+                                        PartitionPageSize());
 }
 
 void* PartitionCalloc(const AllocatorDispatch*,
@@ -286,7 +290,8 @@
                       size_t size,
                       void* context) {
   const size_t total = base::CheckMul(n, MaybeAdjustSize(size)).ValueOrDie();
-  return Allocator()->AllocFlagsNoHooks(base::PartitionAllocZeroFill, total);
+  return Allocator()->AllocFlagsNoHooks(base::PartitionAllocZeroFill, total,
+                                        PartitionPageSize());
 }
 
 void* PartitionMemalign(const AllocatorDispatch*,
diff --git a/base/allocator/partition_allocator/address_pool_manager.cc b/base/allocator/partition_allocator/address_pool_manager.cc
index e368a440..19e0466 100644
--- a/base/allocator/partition_allocator/address_pool_manager.cc
+++ b/base/allocator/partition_allocator/address_pool_manager.cc
@@ -279,7 +279,7 @@
 char* AddressPoolManager::Reserve(pool_handle handle,
                                   void* requested_address,
                                   size_t length) {
-  PA_DCHECK(!(length & PageAllocationGranularityOffsetMask()));
+  PA_DCHECK(!(length & DirectMapAllocationGranularityOffsetMask()));
   char* ptr = reinterpret_cast<char*>(
       AllocPages(requested_address, length, kSuperPageSize, PageInaccessible,
                  PageTag::kPartitionAlloc));
@@ -295,7 +295,7 @@
                                               size_t length) {
   uintptr_t ptr_as_uintptr = reinterpret_cast<uintptr_t>(ptr);
   PA_DCHECK(!(ptr_as_uintptr & kSuperPageOffsetMask));
-  PA_DCHECK(!(length & PageAllocationGranularityOffsetMask()));
+  PA_DCHECK(!(length & DirectMapAllocationGranularityOffsetMask()));
   MarkUnused(handle, ptr_as_uintptr, length);
   FreePages(ptr, length);
 }
@@ -307,8 +307,8 @@
   AutoLock guard(AddressPoolManagerBitmap::GetLock());
   if (handle == kNonBRPPoolHandle) {
     SetBitmap(AddressPoolManagerBitmap::non_brp_pool_bits_,
-              ptr_as_uintptr / PageAllocationGranularity(),
-              length / PageAllocationGranularity());
+              ptr_as_uintptr / DirectMapAllocationGranularity(),
+              length / DirectMapAllocationGranularity());
   } else {
     PA_DCHECK(handle == kBRPPoolHandle);
     PA_DCHECK(!(length & kSuperPageOffsetMask));
@@ -352,8 +352,8 @@
   // allocated from there. Thus LIKELY is used.
   if (LIKELY(handle == kNonBRPPoolHandle)) {
     ResetBitmap(AddressPoolManagerBitmap::non_brp_pool_bits_,
-                address / PageAllocationGranularity(),
-                length / PageAllocationGranularity());
+                address / DirectMapAllocationGranularity(),
+                length / DirectMapAllocationGranularity());
   } else {
     PA_DCHECK(handle == kBRPPoolHandle);
     PA_DCHECK(!(length & kSuperPageOffsetMask));
diff --git a/base/allocator/partition_allocator/address_pool_manager_bitmap.h b/base/allocator/partition_allocator/address_pool_manager_bitmap.h
index c3f949b..00a9078f 100644
--- a/base/allocator/partition_allocator/address_pool_manager_bitmap.h
+++ b/base/allocator/partition_allocator/address_pool_manager_bitmap.h
@@ -43,16 +43,17 @@
       kAddressSpaceSize / kBytesPer1BitOfBRPPoolBitmap;
 
   // Non-BRP pool includes both normal bucket and direct map allocations, so
-  // PageAllocationGranularity() has to be used. No need to eliminate guard
+  // DirectMapAllocationGranularity() has to be used. No need to eliminate guard
   // pages at the ends, as this is a BackupRefPtr-specific concern.
   static constexpr size_t kNonBRPPoolBits =
-      kAddressSpaceSize / PageAllocationGranularity();
+      kAddressSpaceSize / DirectMapAllocationGranularity();
 
   // Returns false for nullptr.
   static bool IsManagedByNonBRPPool(const void* address) {
     uintptr_t address_as_uintptr = reinterpret_cast<uintptr_t>(address);
     static_assert(
-        std::numeric_limits<uintptr_t>::max() / PageAllocationGranularity() <
+        std::numeric_limits<uintptr_t>::max() /
+                DirectMapAllocationGranularity() <
             non_brp_pool_bits_.size(),
         "The bitmap is too small, will result in unchecked out of bounds "
         "accesses.");
@@ -60,7 +61,8 @@
     // is responsible for guaranteeing that the address is inside a valid
     // allocation and the deallocation call won't race with this call.
     return TS_UNCHECKED_READ(
-        non_brp_pool_bits_)[address_as_uintptr / PageAllocationGranularity()];
+        non_brp_pool_bits_)[address_as_uintptr /
+                            DirectMapAllocationGranularity()];
   }
 
   // Returns false for nullptr.
diff --git a/base/allocator/partition_allocator/address_pool_manager_unittest.cc b/base/allocator/partition_allocator/address_pool_manager_unittest.cc
index ab4c2257..854ba5be 100644
--- a/base/allocator/partition_allocator/address_pool_manager_unittest.cc
+++ b/base/allocator/partition_allocator/address_pool_manager_unittest.cc
@@ -192,16 +192,18 @@
   void* addrs[kAllocCount];
   for (size_t i = 0; i < kAllocCount; ++i) {
     addrs[i] = AddressPoolManager::GetInstance()->Reserve(
-        GetNonBRPPool(), nullptr, PageAllocationGranularity() * kNumPages[i]);
+        GetNonBRPPool(), nullptr,
+        DirectMapAllocationGranularity() * kNumPages[i]);
     EXPECT_TRUE(addrs[i]);
     EXPECT_TRUE(
         !(reinterpret_cast<uintptr_t>(addrs[i]) & kSuperPageOffsetMask));
   }
   for (size_t i = 0; i < kAllocCount; ++i) {
     const char* ptr = reinterpret_cast<const char*>(addrs[i]);
-    size_t num_pages = bits::AlignUp(kNumPages[i] * PageAllocationGranularity(),
-                                     kSuperPageSize) /
-                       PageAllocationGranularity();
+    size_t num_pages =
+        bits::AlignUp(kNumPages[i] * DirectMapAllocationGranularity(),
+                      kSuperPageSize) /
+        DirectMapAllocationGranularity();
     for (size_t j = 0; j < num_pages; ++j) {
       if (j < kNumPages[i]) {
         EXPECT_TRUE(AddressPoolManager::IsManagedByNonBRPPool(ptr));
@@ -209,12 +211,13 @@
         EXPECT_FALSE(AddressPoolManager::IsManagedByNonBRPPool(ptr));
       }
       EXPECT_FALSE(AddressPoolManager::IsManagedByBRPPool(ptr));
-      ptr += PageAllocationGranularity();
+      ptr += DirectMapAllocationGranularity();
     }
   }
   for (size_t i = 0; i < kAllocCount; ++i) {
     AddressPoolManager::GetInstance()->UnreserveAndDecommit(
-        GetNonBRPPool(), addrs[i], PageAllocationGranularity() * kNumPages[i]);
+        GetNonBRPPool(), addrs[i],
+        DirectMapAllocationGranularity() * kNumPages[i]);
     EXPECT_FALSE(AddressPoolManager::IsManagedByNonBRPPool(addrs[i]));
     EXPECT_FALSE(AddressPoolManager::IsManagedByBRPPool(addrs[i]));
   }
diff --git a/base/allocator/partition_allocator/partition_alloc.cc b/base/allocator/partition_allocator/partition_alloc.cc
index 882b6ba6..3f73926 100644
--- a/base/allocator/partition_allocator/partition_alloc.cc
+++ b/base/allocator/partition_allocator/partition_alloc.cc
@@ -52,7 +52,7 @@
 
   // Limit to prevent callers accidentally overflowing an int size.
   STATIC_ASSERT_OR_PA_CHECK(
-      MaxDirectMapped() <= (1UL << 31) + PageAllocationGranularity(),
+      MaxDirectMapped() <= (1UL << 31) + DirectMapAllocationGranularity(),
       "maximum direct mapped allocation");
 
   // Check that some of our zanier calculations worked out as expected.
diff --git a/base/allocator/partition_allocator/partition_alloc_constants.h b/base/allocator/partition_allocator/partition_alloc_constants.h
index 73f3566f..b08d32c 100644
--- a/base/allocator/partition_allocator/partition_alloc_constants.h
+++ b/base/allocator/partition_allocator/partition_alloc_constants.h
@@ -12,6 +12,7 @@
 
 #include "base/allocator/buildflags.h"
 #include "base/allocator/partition_allocator/page_allocator_constants.h"
+#include "base/allocator/partition_allocator/partition_alloc_config.h"
 #include "build/build_config.h"
 
 #if defined(OS_APPLE)
@@ -185,6 +186,28 @@
   return kSuperPageSize / PartitionPageSize();
 }
 
+#if defined(PA_HAS_64_BITS_POINTERS)
+// In 64-bit mode, the direct map allocation granularity is super page size,
+// because this is the reservation granularit of the GigaCage.
+constexpr ALWAYS_INLINE size_t DirectMapAllocationGranularity() {
+  return kSuperPageSize;
+}
+#else
+// In 32-bit mode, address space is space is a scarce resource. Use the system
+// allocation granularity, which is the lowest possible address space allocation
+// unit. However, don't go below partition page size, so that GigaCage bitmaps
+// don't get too large.
+PAGE_ALLOCATOR_CONSTANTS_DECLARE_CONSTEXPR ALWAYS_INLINE size_t
+DirectMapAllocationGranularity() {
+  return std::max(PageAllocationGranularity(), PartitionPageSize());
+}
+#endif  // defined(PA_HAS_64_BITS_POINTERS)
+
+PAGE_ALLOCATOR_CONSTANTS_DECLARE_CONSTEXPR ALWAYS_INLINE size_t
+DirectMapAllocationGranularityOffsetMask() {
+  return DirectMapAllocationGranularity() - 1;
+}
+
 // Alignment has two constraints:
 // - Alignment requirement for scalar types: alignof(std::max_align_t)
 // - Alignment requirement for operator new().
@@ -248,10 +271,16 @@
 // crbug.com/998048 for details.
 PAGE_ALLOCATOR_CONSTANTS_DECLARE_CONSTEXPR ALWAYS_INLINE size_t
 MaxDirectMapped() {
-  // Subtract kSuperPageSize to accommodate for alignment inside
+  // Subtract kSuperPageSize to accommodate for granularity inside
   // PartitionRoot::GetDirectMapReservedSize.
   return (1UL << 31) - kSuperPageSize;
 }
+
+// Max alignment supported by AlignedAllocFlags().
+// kSuperPageSize alignment can't be easily supported, because each super page
+// starts with guard pages & metadata.
+static const size_t kMaxSupportedAlignment = kSuperPageSize / 2;
+
 static const size_t kBitsPerSizeT = sizeof(void*) * CHAR_BIT;
 
 // Constant for the memory reclaim logic.
diff --git a/base/allocator/partition_allocator/partition_alloc_perftest.cc b/base/allocator/partition_allocator/partition_alloc_perftest.cc
index 545dd80..c4e4d56 100644
--- a/base/allocator/partition_allocator/partition_alloc_perftest.cc
+++ b/base/allocator/partition_allocator/partition_alloc_perftest.cc
@@ -85,7 +85,7 @@
   ~PartitionAllocator() override = default;
 
   void* Alloc(size_t size) override {
-    return alloc_.AllocFlagsNoHooks(0, size);
+    return alloc_.AllocFlagsNoHooks(0, size, PartitionPageSize());
   }
   void Free(void* data) override { ThreadSafePartitionRoot::FreeNoHooks(data); }
 
@@ -115,7 +115,7 @@
   ~PartitionAllocatorWithThreadCache() override = default;
 
   void* Alloc(size_t size) override {
-    return g_partition_root->AllocFlagsNoHooks(0, size);
+    return g_partition_root->AllocFlagsNoHooks(0, size, PartitionPageSize());
   }
   void Free(void* data) override { ThreadSafePartitionRoot::FreeNoHooks(data); }
 };
diff --git a/base/allocator/partition_allocator/partition_alloc_unittest.cc b/base/allocator/partition_allocator/partition_alloc_unittest.cc
index 11fadd7..36d608b 100644
--- a/base/allocator/partition_allocator/partition_alloc_unittest.cc
+++ b/base/allocator/partition_allocator/partition_alloc_unittest.cc
@@ -2609,7 +2609,7 @@
 TEST_F(PartitionAllocTest, Alignment) {
   std::vector<void*> allocated_ptrs;
 
-  for (size_t size = 1; size <= base::SystemPageSize(); size <<= 1) {
+  for (size_t size = 1; size <= base::PartitionPageSize(); size <<= 1) {
     // All allocations which are not direct-mapped occupy contiguous slots of a
     // span, starting on a page boundary. This means that allocations are first
     // rounded up to the nearest bucket size, then have an address of the form:
@@ -2714,11 +2714,12 @@
 #define MAYBE_AlignedAllocations AlignedAllocations
 #endif
 TEST_F(PartitionAllocTest, MAYBE_AlignedAllocations) {
-  size_t alloc_sizes[] = {1, 10, 100, 1000, 10000, 100000, 1000000};
-  size_t alignemnts[] = {8, 16, 32, 64, 256, 1024, 4096, 8192};
+  size_t alloc_sizes[] = {1,     10,    100,    1000,   10000,
+                          60000, 70000, 130000, 500000, 900000};
+  size_t max_alignment = 1048576;
 
   for (size_t alloc_size : alloc_sizes) {
-    for (size_t alignment : alignemnts) {
+    for (size_t alignment = 1; alignment <= max_alignment; alignment <<= 1) {
       VerifyAlignment(aligned_allocator.root(), alloc_size, alignment);
 
       // AlignedAllocFlags() can't be called on regular allocator, if there are
@@ -2862,6 +2863,7 @@
       kSuperPageSize + PartitionPageSize(),
       kSuperPageSize + SystemPageSize() + PartitionPageSize(),
       kSuperPageSize + PageAllocationGranularity(),
+      kSuperPageSize + DirectMapAllocationGranularity(),
   };
   for (size_t huge_size : huge_sizes) {
     // For direct map, we commit only as many pages as needed.
@@ -2870,12 +2872,9 @@
     expected_committed_size += aligned_size;
     size_t surrounding_pages_size =
         PartitionRoot<ThreadSafe>::GetDirectMapMetadataAndGuardPagesSize();
-    size_t alignment = PageAllocationGranularity();
-#if defined(PA_HAS_64_BITS_POINTERS)
-    alignment = kSuperPageSize;
-#endif
     size_t expected_direct_map_size =
-        bits::AlignUp(aligned_size + surrounding_pages_size, alignment);
+        bits::AlignUp(aligned_size + surrounding_pages_size,
+                      DirectMapAllocationGranularity());
     EXPECT_EQ(expected_committed_size, root.total_size_of_committed_pages);
     EXPECT_EQ(expected_super_pages_size, root.total_size_of_super_pages);
     EXPECT_EQ(expected_direct_map_size, root.total_size_of_direct_mapped_pages);
@@ -3032,24 +3031,24 @@
   size_t allocation_size = 64;
   // The very first allocation is never a fast path one, since it needs a new
   // super page and a new partition page.
-  EXPECT_FALSE(allocator.root()->AllocFlagsNoHooks(
-      PartitionAllocFastPathOrReturnNull, allocation_size));
-  void* ptr = allocator.root()->AllocFlagsNoHooks(0, allocation_size);
+  EXPECT_FALSE(allocator.root()->AllocFlags(PartitionAllocFastPathOrReturnNull,
+                                            allocation_size, ""));
+  void* ptr = allocator.root()->AllocFlags(0, allocation_size, "");
   ASSERT_TRUE(ptr);
 
   // Next one is, since the partition page has been activated.
-  void* ptr2 = allocator.root()->AllocFlagsNoHooks(
-      PartitionAllocFastPathOrReturnNull, allocation_size);
+  void* ptr2 = allocator.root()->AllocFlags(PartitionAllocFastPathOrReturnNull,
+                                            allocation_size, "");
   EXPECT_TRUE(ptr2);
 
   // First allocation of a different bucket is slow.
-  EXPECT_FALSE(allocator.root()->AllocFlagsNoHooks(
-      PartitionAllocFastPathOrReturnNull, 2 * allocation_size));
+  EXPECT_FALSE(allocator.root()->AllocFlags(PartitionAllocFastPathOrReturnNull,
+                                            2 * allocation_size, ""));
 
   size_t allocated_size = 2 * allocation_size;
   std::vector<void*> ptrs;
-  while (void* new_ptr = allocator.root()->AllocFlagsNoHooks(
-             PartitionAllocFastPathOrReturnNull, allocation_size)) {
+  while (void* new_ptr = allocator.root()->AllocFlags(
+             PartitionAllocFastPathOrReturnNull, allocation_size, "")) {
     ptrs.push_back(new_ptr);
     allocated_size += allocation_size;
   }
diff --git a/base/allocator/partition_allocator/partition_bucket.cc b/base/allocator/partition_allocator/partition_bucket.cc
index f658a731..0bdd2f1 100644
--- a/base/allocator/partition_allocator/partition_bucket.cc
+++ b/base/allocator/partition_allocator/partition_bucket.cc
@@ -153,12 +153,11 @@
     PA_DCHECK(!metadata->extent.super_page_base);
     PA_DCHECK(!metadata->extent.super_pages_end);
     PA_DCHECK(!metadata->extent.next);
-    // Call FromSlotInnerPtr instead of FromSlotStartPtr, because the bucket
-    // isn't set up yet to properly assert the slot start.
-    PA_DCHECK(PartitionPage<thread_safe>::FromSlotInnerPtr(slot) ==
-              &metadata->page);
+    PA_DCHECK(PartitionPage<thread_safe>::FromPtr(slot) == &metadata->page);
 
     page = &metadata->page;
+    page->is_valid = true;
+    PA_DCHECK(!page->has_valid_span_after_this);
     PA_DCHECK(!page->slot_span_metadata_offset);
     PA_DCHECK(!page->slot_span_metadata.next_slot_span);
     PA_DCHECK(!page->slot_span_metadata.num_allocated_slots);
@@ -279,7 +278,8 @@
 template <bool thread_safe>
 ALWAYS_INLINE SlotSpanMetadata<thread_safe>*
 PartitionBucket<thread_safe>::AllocNewSlotSpan(PartitionRoot<thread_safe>* root,
-                                               int flags) {
+                                               int flags,
+                                               size_t slot_span_alignment) {
   PA_DCHECK(!(reinterpret_cast<uintptr_t>(root->next_partition_page) %
               PartitionPageSize()));
   PA_DCHECK(!(reinterpret_cast<uintptr_t>(root->next_partition_page_end) %
@@ -292,25 +292,39 @@
   PA_DCHECK(slot_span_committed_size % SystemPageSize() == 0);
   PA_DCHECK(slot_span_committed_size <= slot_span_reserved_size);
 
-  size_t num_partition_pages_left =
-      (root->next_partition_page_end - root->next_partition_page) >>
-      PartitionPageShift();
-  if (UNLIKELY(num_partition_pages_left < num_partition_pages)) {
+  auto adjusted_next_partition_page =
+      bits::AlignUp(root->next_partition_page, slot_span_alignment);
+  if (UNLIKELY(adjusted_next_partition_page + slot_span_reserved_size >
+               root->next_partition_page_end)) {
     // In this case, we can no longer hand out pages from the current super page
     // allocation. Get a new super page.
     if (!AllocNewSuperPage(root)) {
       return nullptr;
     }
+    // AllocNewSuperPage() updates root->next_partition_page, re-query.
+    adjusted_next_partition_page =
+        bits::AlignUp(root->next_partition_page, slot_span_alignment);
+    PA_CHECK(adjusted_next_partition_page + slot_span_reserved_size <=
+             root->next_partition_page_end);
   }
 
-  void* slot_span_start = root->next_partition_page;
-  root->next_partition_page += slot_span_reserved_size;
+  auto* gap_start_page =
+      PartitionPage<thread_safe>::FromPtr(root->next_partition_page);
+  auto* gap_end_page =
+      PartitionPage<thread_safe>::FromPtr(adjusted_next_partition_page);
+  for (auto* page = gap_start_page; page < gap_end_page; ++page) {
+    PA_DCHECK(!page->is_valid);
+    page->has_valid_span_after_this = 1;
+  }
+  root->next_partition_page =
+      adjusted_next_partition_page + slot_span_reserved_size;
 
-  // Call FromSlotInnerPtr instead of FromSlotStartPtr, because the slot_span's
-  // bucket isn't set up yet to properly assert the slot start.
-  auto* slot_span =
-      SlotSpanMetadata<thread_safe>::FromSlotInnerPtr(slot_span_start);
+  void* slot_span_start = adjusted_next_partition_page;
+  auto* slot_span = &gap_end_page->slot_span_metadata;
   InitializeSlotSpan(slot_span);
+  // Now that slot span is initialized, it's safe to call FromSlotStartPtr.
+  PA_DCHECK(slot_span ==
+            SlotSpanMetadata<thread_safe>::FromSlotStartPtr(slot_span_start));
 
   // System pages in the super page come in a decommited state. Commit them
   // before vending them back.
@@ -483,9 +497,10 @@
 
   uint16_t num_partition_pages = get_pages_per_slot_span();
   auto* page = reinterpret_cast<PartitionPage<thread_safe>*>(slot_span);
-  for (uint16_t i = 1; i < num_partition_pages; ++i) {
-    auto* secondary_page = page + i;
-    secondary_page->slot_span_metadata_offset = i;
+  for (uint16_t i = 0; i < num_partition_pages; ++i, ++page) {
+    PA_DCHECK(i <= PartitionPage<thread_safe>::kMaxSlotSpanMetadataOffset);
+    page->slot_span_metadata_offset = i;
+    page->is_valid = true;
   }
 }
 
@@ -633,9 +648,17 @@
     PartitionRoot<thread_safe>* root,
     int flags,
     size_t raw_size,
+    size_t slot_span_alignment,
     bool* is_already_zeroed) {
-  // The slow path is called when the freelist is empty.
-  PA_DCHECK(!active_slot_spans_head->freelist_head);
+  PA_DCHECK(slot_span_alignment &&
+            !(slot_span_alignment & PartitionPageOffsetMask()));
+
+  // The slow path is called when the freelist is empty. The only exception is
+  // when a higher-order alignment is requested, in which case the freelist
+  // logic is bypassed and we go directly for slot span allocation.
+  bool allocate_aligned_slot_span = slot_span_alignment > PartitionPageSize();
+  PA_DCHECK(!active_slot_spans_head->freelist_head ||
+            allocate_aligned_slot_span);
 
   SlotSpanMetadata<thread_safe>* new_slot_span = nullptr;
   // |new_slot_span->bucket| will always be |this|, except when |this| is the
@@ -659,6 +682,7 @@
     PA_DCHECK(this == &root->sentinel_bucket);
     PA_DCHECK(active_slot_spans_head ==
               SlotSpanMetadata<thread_safe>::get_sentinel_slot_span());
+    PA_DCHECK(!allocate_aligned_slot_span);  // not supported for direct map
 
     // No fast path for direct-mapped allocations.
     if (flags & PartitionAllocFastPathOrReturnNull)
@@ -669,12 +693,13 @@
       new_bucket = new_slot_span->bucket;
     // Memory from PageAllocator is always zeroed.
     *is_already_zeroed = true;
-  } else if (LIKELY(SetNewActiveSlotSpan())) {
+  } else if (LIKELY(!allocate_aligned_slot_span && SetNewActiveSlotSpan())) {
     // First, did we find an active slot span in the active list?
     new_slot_span = active_slot_spans_head;
     PA_DCHECK(new_slot_span->is_active());
-  } else if (LIKELY(empty_slot_spans_head != nullptr) ||
-             LIKELY(decommitted_slot_spans_head != nullptr)) {
+  } else if (LIKELY(!allocate_aligned_slot_span &&
+                    (empty_slot_spans_head != nullptr ||
+                     decommitted_slot_spans_head != nullptr))) {
     // Second, look in our lists of empty and decommitted slot spans.
     // Check empty slot spans first, which are preferred, but beware that an
     // empty slot span might have been decommitted.
@@ -732,7 +757,7 @@
     // Third. If we get here, we need a brand new slot span.
     // TODO(bartekn): For single-slot slot spans, we can use rounded raw_size
     // as slot_span_committed_size.
-    new_slot_span = AllocNewSlotSpan(root, flags);
+    new_slot_span = AllocNewSlotSpan(root, flags, slot_span_alignment);
     // New memory from PageAllocator is always zeroed.
     *is_already_zeroed = true;
   }
diff --git a/base/allocator/partition_allocator/partition_bucket.h b/base/allocator/partition_allocator/partition_bucket.h
index ac3a8e3..0f4b844 100644
--- a/base/allocator/partition_allocator/partition_bucket.h
+++ b/base/allocator/partition_allocator/partition_bucket.h
@@ -61,6 +61,7 @@
   BASE_EXPORT NOINLINE void* SlowPathAlloc(PartitionRoot<thread_safe>* root,
                                            int flags,
                                            size_t raw_size,
+                                           size_t slot_span_alignment,
                                            bool* is_already_zeroed)
       EXCLUSIVE_LOCKS_REQUIRED(root->lock_);
 
@@ -148,7 +149,8 @@
   // Returns nullptr on error.
   ALWAYS_INLINE SlotSpanMetadata<thread_safe>* AllocNewSlotSpan(
       PartitionRoot<thread_safe>* root,
-      int flags) EXCLUSIVE_LOCKS_REQUIRED(root->lock_);
+      int flags,
+      size_t slot_span_alignment) EXCLUSIVE_LOCKS_REQUIRED(root->lock_);
 
   // Allocates a new super page from the current extent. All slot-spans will be
   // in the decommitted state. Returns nullptr on error.
diff --git a/base/allocator/partition_allocator/partition_page.cc b/base/allocator/partition_allocator/partition_page.cc
index 0e36927..65f88e2 100644
--- a/base/allocator/partition_allocator/partition_page.cc
+++ b/base/allocator/partition_allocator/partition_page.cc
@@ -47,10 +47,10 @@
   size_t reserved_size =
       extent->map_size +
       PartitionRoot<thread_safe>::GetDirectMapMetadataAndGuardPagesSize();
-  PA_DCHECK(!(reserved_size & PageAllocationGranularityOffsetMask()));
+  PA_DCHECK(!(reserved_size & DirectMapAllocationGranularityOffsetMask()));
   PA_DCHECK(root->total_size_of_direct_mapped_pages >= reserved_size);
   root->total_size_of_direct_mapped_pages -= reserved_size;
-  PA_DCHECK(!(reserved_size & PageAllocationGranularityOffsetMask()));
+  PA_DCHECK(!(reserved_size & DirectMapAllocationGranularityOffsetMask()));
 
   char* ptr = reinterpret_cast<char*>(
       SlotSpanMetadata<thread_safe>::ToSlotSpanStartPtr(slot_span));
diff --git a/base/allocator/partition_allocator/partition_page.h b/base/allocator/partition_allocator/partition_page.h
index cf2d615b..a19d74ae 100644
--- a/base/allocator/partition_allocator/partition_page.h
+++ b/base/allocator/partition_allocator/partition_page.h
@@ -6,6 +6,7 @@
 #define BASE_ALLOCATOR_PARTITION_ALLOCATOR_PARTITION_PAGE_H_
 
 #include <string.h>
+#include <cstdint>
 #include <limits>
 
 #include "base/allocator/partition_allocator/address_pool_manager.h"
@@ -231,7 +232,7 @@
 // information.
 template <bool thread_safe>
 struct PartitionPage {
-  // "Pack" the union so that slot_span_metadata_offset still fits within
+  // "Pack" the union so that common page metadata still fits within
   // kPageMetadataSize. (SlotSpanMetadata is also "packed".)
   union __attribute__((packed)) {
     SlotSpanMetadata<thread_safe> slot_span_metadata;
@@ -243,15 +244,28 @@
     // - below kPageMetadataSize
     //
     // This makes sure that this is respected no matter the architecture.
-    char optional_padding[kPageMetadataSize - sizeof(uint16_t)];
+    char optional_padding[kPageMetadataSize - sizeof(uint8_t) - sizeof(bool)];
   };
 
   // The first PartitionPage of the slot span holds its metadata. This offset
   // tells how many pages in from that first page we are.
-  uint16_t slot_span_metadata_offset;
+  // 6 bits is enough to represent all possible offsets, given that the smallest
+  // partition page is 16kiB and normal buckets won't exceed 1MiB.
+  static constexpr uint16_t kMaxSlotSpanMetadataBits = 6;
+  static constexpr uint16_t kMaxSlotSpanMetadataOffset =
+      (1 << kMaxSlotSpanMetadataBits) - 1;
+  uint8_t slot_span_metadata_offset;
 
-  ALWAYS_INLINE static PartitionPage* FromSlotStartPtr(void* slot_start);
-  ALWAYS_INLINE static PartitionPage* FromSlotInnerPtr(void* ptr);
+  // |is_valid| tells whether the page is part of a slot span. If |false|,
+  // |has_valid_span_after_this| tells whether it's an unused region in between
+  // slot spans within the super page.
+  // Note, |is_valid| has been added for clarity, but if we ever need to save
+  // this bit, it can be inferred from:
+  //   |!slot_span_metadata_offset && slot_span_metadata->bucket|.
+  bool is_valid : 1;
+  bool has_valid_span_after_this : 1;
+
+  ALWAYS_INLINE static PartitionPage* FromPtr(void* slot_start);
 
  private:
   ALWAYS_INLINE static void* ToSlotSpanStartPtr(const PartitionPage* page);
@@ -327,12 +341,12 @@
 
 // CAUTION! Use only for normal buckets. Using on direct-mapped allocations may
 // lead to undefined behavior.
-ALWAYS_INLINE bool IsWithinSuperPagePayload(char* ptr, bool with_quarantine) {
+ALWAYS_INLINE bool IsWithinSuperPagePayload(void* ptr, bool with_quarantine) {
   // TODO(bartekn): Add a "is in normal buckets" DCHECK.
   char* super_page_base = reinterpret_cast<char*>(
       reinterpret_cast<uintptr_t>(ptr) & kSuperPageBaseMask);
-  char* payload_start = SuperPagePayloadBegin(super_page_base, with_quarantine);
-  char* payload_end = SuperPagePayloadEnd(super_page_base);
+  void* payload_start = SuperPagePayloadBegin(super_page_base, with_quarantine);
+  void* payload_end = SuperPagePayloadEnd(super_page_base);
   return ptr >= payload_start && ptr < payload_end;
 }
 
@@ -390,21 +404,28 @@
 template <bool thread_safe>
 ALWAYS_INLINE void* PartitionPage<thread_safe>::ToSlotSpanStartPtr(
     const PartitionPage* page) {
+  PA_DCHECK(page->is_valid);
   PA_DCHECK(!page->slot_span_metadata_offset);
   return SlotSpanMetadata<thread_safe>::ToSlotSpanStartPtr(
       &page->slot_span_metadata);
 }
 
-// Converts from a pointer inside a slot into a pointer to the PartitionPage
-// object (within super pages's metadata) that describes the first partition
-// page of a slot span containing that slot.
+// Converts from a pointer inside a super page into a pointer to the
+// PartitionPage object (within super pages's metadata) that describes the
+// partition page where |ptr| is located. |ptr| doesn't have to be a located
+// within a valid (i.e. allocated) slot span, but must be within the super
+// page's payload (i.e. region devoted to slot spans).
 //
-// CAUTION! Use only for normal buckets. Using on direct-mapped allocations may
-// lead to undefined behavior.
+// While it is generally valid for |ptr| to be in the middle of an allocation,
+// care has to be taken with direct maps that span multiple super pages. This
+// function's behavior is undefined if |ptr| lies in a subsequent super page.
 template <bool thread_safe>
-ALWAYS_INLINE PartitionPage<thread_safe>*
-PartitionPage<thread_safe>::FromSlotInnerPtr(void* ptr) {
-  // TODO(bartekn): Add a "is in normal buckets" DCHECK.
+ALWAYS_INLINE PartitionPage<thread_safe>* PartitionPage<thread_safe>::FromPtr(
+    void* ptr) {
+  // Force |with_quarantine=false|, because we don't know whether its direct map
+  // and direct map allocations don't have quarantine bitmaps.
+  PA_DCHECK(IsWithinSuperPagePayload(ptr, /* with_quarantine= */ false));
+
   uintptr_t pointer_as_uint = reinterpret_cast<uintptr_t>(ptr);
   char* super_page_ptr =
       reinterpret_cast<char*>(pointer_as_uint & kSuperPageBaseMask);
@@ -412,32 +433,13 @@
       (pointer_as_uint & kSuperPageOffsetMask) >> PartitionPageShift();
   // Index 0 is invalid because it is the super page extent metadata and the
   // last index is invalid because the whole PartitionPage is set as guard
-  // pages.
+  // pages. This repeats part of the payload PA_DCHECK above, which may check
+  // for other exclusions.
   PA_DCHECK(partition_page_index);
   PA_DCHECK(partition_page_index < NumPartitionPagesPerSuperPage() - 1);
-  auto* page = reinterpret_cast<PartitionPage<thread_safe>*>(
+  return reinterpret_cast<PartitionPage<thread_safe>*>(
       PartitionSuperPageToMetadataArea(super_page_ptr) +
       (partition_page_index << kPageMetadataShift));
-  // Partition pages in the same slot span share the same slot span metadata
-  // object (located in the first PartitionPage object of that span). Adjust
-  // for that.
-  page -= page->slot_span_metadata_offset;
-  return page;
-}
-
-// Like |FromSlotInnerPtr|, but asserts that pointer points to the beginning of
-// the slot.
-template <bool thread_safe>
-ALWAYS_INLINE PartitionPage<thread_safe>*
-PartitionPage<thread_safe>::FromSlotStartPtr(void* slot_start) {
-  auto* page = FromSlotInnerPtr(slot_start);
-
-  // Checks that the pointer is a multiple of slot size.
-  auto* slot_span_start = ToSlotSpanStartPtr(page);
-  PA_DCHECK(!((reinterpret_cast<uintptr_t>(slot_start) -
-               reinterpret_cast<uintptr_t>(slot_span_start)) %
-              page->slot_span_metadata.bucket->slot_size));
-  return page;
 }
 
 // Converts from a pointer to the SlotSpanMetadata object (within super pages's
@@ -475,10 +477,20 @@
 // Converts from a pointer inside a slot into a pointer to the SlotSpanMetadata
 // object (within super pages's metadata) that describes the slot span
 // containing that slot.
+//
+// CAUTION! Use only for normal buckets. Using on direct-mapped allocations may
+// lead to undefined behavior.
 template <bool thread_safe>
 ALWAYS_INLINE SlotSpanMetadata<thread_safe>*
 SlotSpanMetadata<thread_safe>::FromSlotInnerPtr(void* ptr) {
-  auto* page = PartitionPage<thread_safe>::FromSlotInnerPtr(ptr);
+  // TODO(bartekn): Add a "is in normal buckets" DCHECK.
+  auto* page = PartitionPage<thread_safe>::FromPtr(ptr);
+  PA_DCHECK(page->is_valid);
+  // Partition pages in the same slot span share the same slot span metadata
+  // object (located in the first PartitionPage object of that span). Adjust
+  // for that.
+  page -= page->slot_span_metadata_offset;
+  PA_DCHECK(page->is_valid);
   PA_DCHECK(!page->slot_span_metadata_offset);
   return &page->slot_span_metadata;
 }
@@ -488,9 +500,13 @@
 template <bool thread_safe>
 ALWAYS_INLINE SlotSpanMetadata<thread_safe>*
 SlotSpanMetadata<thread_safe>::FromSlotStartPtr(void* slot_start) {
-  auto* page = PartitionPage<thread_safe>::FromSlotStartPtr(slot_start);
-  PA_DCHECK(!page->slot_span_metadata_offset);
-  return &page->slot_span_metadata;
+  auto* slot_span = FromSlotInnerPtr(slot_start);
+  // Checks that the pointer is a multiple of slot size.
+  auto* slot_span_start = ToSlotSpanStartPtr(slot_span);
+  PA_DCHECK(!((reinterpret_cast<uintptr_t>(slot_start) -
+               reinterpret_cast<uintptr_t>(slot_span_start)) %
+              slot_span->bucket->slot_size));
+  return slot_span;
 }
 
 template <bool thread_safe>
@@ -632,21 +648,38 @@
 #endif
 
   using Page = PartitionPage<thread_safe>;
-  auto* const first_page = Page::FromSlotStartPtr(
-      SuperPagePayloadBegin(super_page_base, with_quarantine));
-  // Call FromSlotInnerPtr instead of FromSlotStartPtr, because this slot span
-  // doesn't exist, hence its bucket isn't set up to properly assert the slot
-  // start.
-  auto* const last_page = Page::FromSlotInnerPtr(
-      SuperPagePayloadEnd(super_page_base) - PartitionPageSize());
+  using SlotSpan = SlotSpanMetadata<thread_safe>;
+  auto* const first_page =
+      Page::FromPtr(SuperPagePayloadBegin(super_page_base, with_quarantine));
+  auto* const last_page =
+      Page::FromPtr(SuperPagePayloadEnd(super_page_base) - PartitionPageSize());
   size_t visited = 0;
-  for (auto* page = first_page;
-       page <= last_page && page->slot_span_metadata.bucket;
-       page += page->slot_span_metadata.bucket->get_pages_per_slot_span()) {
-    auto* slot_span = &page->slot_span_metadata;
+  Page* page;
+  SlotSpan* slot_span;
+  for (page = first_page; page <= last_page;) {
+    PA_DCHECK(!page->slot_span_metadata_offset);  // Ensure slot span beginning.
+    if (!page->is_valid) {
+      if (page->has_valid_span_after_this) {
+        // The page doesn't represent a valid slot span, but there is another
+        // one somewhere after this. Keep iterating to find it.
+        ++page;
+        continue;
+      }
+      // There are currently no valid spans from here on. No need to iterate
+      // the rest of the super page.
+      break;
+    }
+    slot_span = &page->slot_span_metadata;
     if (callback(slot_span))
       ++visited;
+    page += slot_span->bucket->get_pages_per_slot_span();
   }
+  // Each super page must have at least one valid slot span.
+  PA_DCHECK(page > first_page);
+  // Just a quick check that the search ended at a valid slot span and there
+  // was no unnecessary iteration over gaps afterwards.
+  PA_DCHECK(page == reinterpret_cast<Page*>(slot_span) +
+                        slot_span->bucket->get_pages_per_slot_span());
   return visited;
 }
 
diff --git a/base/allocator/partition_allocator/partition_root.cc b/base/allocator/partition_allocator/partition_root.cc
index 5057cd1..9c97aea 100644
--- a/base/allocator/partition_allocator/partition_root.cc
+++ b/base/allocator/partition_allocator/partition_root.cc
@@ -10,6 +10,7 @@
 #include "base/allocator/partition_allocator/partition_address_space.h"
 #include "base/allocator/partition_allocator/partition_alloc_check.h"
 #include "base/allocator/partition_allocator/partition_alloc_config.h"
+#include "base/allocator/partition_allocator/partition_alloc_constants.h"
 #include "base/allocator/partition_allocator/partition_alloc_features.h"
 #include "base/allocator/partition_allocator/partition_bucket.h"
 #include "base/allocator/partition_allocator/partition_cookie.h"
@@ -767,8 +768,9 @@
 #else
   bool no_hooks = flags & PartitionAllocNoHooks;
   if (UNLIKELY(!ptr)) {
-    return no_hooks ? AllocFlagsNoHooks(flags, new_size)
-                    : AllocFlags(flags, new_size, type_name);
+    return no_hooks ? AllocFlagsNoHooks(flags, new_size, PartitionPageSize())
+                    : AllocFlagsInternal(flags, new_size, PartitionPageSize(),
+                                         type_name);
   }
 
   if (UNLIKELY(!new_size)) {
@@ -820,8 +822,9 @@
   }
 
   // This realloc cannot be resized in-place. Sadness.
-  void* ret = no_hooks ? AllocFlagsNoHooks(flags, new_size)
-                       : AllocFlags(flags, new_size, type_name);
+  void* ret = no_hooks ? AllocFlagsNoHooks(flags, new_size, PartitionPageSize())
+                       : AllocFlagsInternal(flags, new_size,
+                                            PartitionPageSize(), type_name);
   if (!ret) {
     if (flags & PartitionAllocReturnNull)
       return nullptr;
diff --git a/base/allocator/partition_allocator/partition_root.h b/base/allocator/partition_allocator/partition_root.h
index 319d572d..a31b4be1 100644
--- a/base/allocator/partition_allocator/partition_root.h
+++ b/base/allocator/partition_allocator/partition_root.h
@@ -300,6 +300,16 @@
   ALWAYS_INLINE void* AllocFlags(int flags,
                                  size_t requested_size,
                                  const char* type_name);
+  // Same as |AllocFlags()|, but allows specifying |slot_span_alignment|. It has
+  // to be a multiple of partition page size, greater than 0 and no greater than
+  // kMaxSupportedAlignment. If it equals exactly 1 partition page, no special
+  // action is taken as PartitoinAlloc naturally guarantees this alignment,
+  // otherwise a sub-optimial allocation strategy is used to guarantee the
+  // higher-order alignment.
+  ALWAYS_INLINE void* AllocFlagsInternal(int flags,
+                                         size_t requested_size,
+                                         size_t slot_span_alignment,
+                                         const char* type_name);
   // Same as |AllocFlags()|, but bypasses the allocator hooks.
   //
   // This is separate from AllocFlags() because other callers of AllocFlags()
@@ -309,7 +319,9 @@
   // taking the extra branch in the non-malloc() case doesn't hurt. In addition,
   // for the malloc() case, the compiler correctly removes the branch, since
   // this is marked |ALWAYS_INLINE|.
-  ALWAYS_INLINE void* AllocFlagsNoHooks(int flags, size_t requested_size);
+  ALWAYS_INLINE void* AllocFlagsNoHooks(int flags,
+                                        size_t requested_size,
+                                        size_t slot_span_alignment);
 
   ALWAYS_INLINE void* Realloc(void* ptr, size_t newize, const char* type_name);
   // Overload that may return nullptr if reallocation isn't possible. In this
@@ -405,14 +417,8 @@
     // limit before calling. This also guards against integer overflow in the
     // calculation here.
     PA_DCHECK(raw_size <= MaxDirectMapped());
-    // Align to allocation granularity. However, in 64-bit mode, the granularity
-    // is super page size.
-    size_t alignment = PageAllocationGranularity();
-#if defined(PA_HAS_64_BITS_POINTERS)
-    alignment = kSuperPageSize;
-#endif
     return bits::AlignUp(raw_size + GetDirectMapMetadataAndGuardPagesSize(),
-                         alignment);
+                         DirectMapAllocationGranularity());
   }
 
 // PartitionRefCount contains a cookie if slow checks are enabled or
@@ -562,11 +568,13 @@
   ALWAYS_INLINE void* RawAlloc(Bucket* bucket,
                                int flags,
                                size_t raw_size,
+                               size_t slot_span_alignment,
                                size_t* usable_size,
                                bool* is_already_zeroed);
   ALWAYS_INLINE void* AllocFromBucket(Bucket* bucket,
                                       int flags,
                                       size_t raw_size,
+                                      size_t slot_span_alignment,
                                       size_t* usable_size,
                                       bool* is_already_zeroed)
       EXCLUSIVE_LOCKS_REQUIRED(lock_);
@@ -893,15 +901,22 @@
     Bucket* bucket,
     int flags,
     size_t raw_size,
+    size_t slot_span_alignment,
     size_t* usable_size,
     bool* is_already_zeroed) {
+  PA_DCHECK(slot_span_alignment &&
+            !(slot_span_alignment & PartitionPageOffsetMask()));
   SlotSpan* slot_span = bucket->active_slot_spans_head;
   // Check that this slot span is neither full nor freed.
   PA_DCHECK(slot_span);
   PA_DCHECK(slot_span->num_allocated_slots >= 0);
 
   void* slot_start = slot_span->freelist_head;
-  if (LIKELY(slot_start)) {
+  // Use the fast path when a slot is readily available on the free list of the
+  // first active slot span. However, fall back to the slow path if a
+  // higher-order alignment is requested, because an inner slot of an existing
+  // slot span is unlikely to satisfy it.
+  if (LIKELY(slot_span_alignment <= PartitionPageSize() && slot_start)) {
     *is_already_zeroed = false;
     // This is a fast path, so avoid calling GetUsableSize() on Release builds
     // as it is more costly. Copy its small bucket path instead.
@@ -923,8 +938,8 @@
 
     PA_DCHECK(slot_span->bucket == bucket);
   } else {
-    slot_start =
-        bucket->SlowPathAlloc(this, flags, raw_size, is_already_zeroed);
+    slot_start = bucket->SlowPathAlloc(this, flags, raw_size,
+                                       slot_span_alignment, is_already_zeroed);
     // TODO(palmer): See if we can afford to make this a CHECK.
     PA_DCHECK(!slot_start ||
               IsValidSlotSpan(SlotSpan::FromSlotStartPtr(slot_start)));
@@ -1276,6 +1291,19 @@
     int flags,
     size_t requested_size,
     const char* type_name) {
+  return AllocFlagsInternal(flags, requested_size, PartitionPageSize(),
+                            type_name);
+}
+
+template <bool thread_safe>
+ALWAYS_INLINE void* PartitionRoot<thread_safe>::AllocFlagsInternal(
+    int flags,
+    size_t requested_size,
+    size_t slot_span_alignment,
+    const char* type_name) {
+  PA_DCHECK(slot_span_alignment &&
+            !(slot_span_alignment & PartitionPageOffsetMask()));
+
   PA_DCHECK(flags < PartitionAllocLastFlag << 1);
   PA_DCHECK((flags & PartitionAllocNoHooks) == 0);  // Internal only.
   PA_DCHECK(initialized);
@@ -1299,7 +1327,7 @@
     }
   }
 
-  ret = AllocFlagsNoHooks(flags, requested_size);
+  ret = AllocFlagsNoHooks(flags, requested_size, slot_span_alignment);
 
   if (UNLIKELY(hooks_enabled)) {
     PartitionAllocHooks::AllocationObserverHookIfEnabled(ret, requested_size,
@@ -1313,7 +1341,11 @@
 template <bool thread_safe>
 ALWAYS_INLINE void* PartitionRoot<thread_safe>::AllocFlagsNoHooks(
     int flags,
-    size_t requested_size) {
+    size_t requested_size,
+    size_t slot_span_alignment) {
+  PA_DCHECK(slot_span_alignment &&
+            !(slot_span_alignment & PartitionPageOffsetMask()));
+
   // The thread cache is added "in the middle" of the main allocator, that is:
   // - After all the cookie/ref-count management
   // - Before the "raw" allocator.
@@ -1341,9 +1373,13 @@
   // !thread_safe => !with_thread_cache, but adding the condition allows the
   // compiler to statically remove this branch for the thread-unsafe variant.
   //
+  // Don't use thread cache if higher order alignment is requested, because the
+  // thread cache will not be able to satisfy it.
+  //
   // LIKELY: performance-sensitive partitions are either thread-unsafe or use
   // the thread cache.
-  if (thread_safe && LIKELY(with_thread_cache)) {
+  if (thread_safe &&
+      LIKELY(with_thread_cache && slot_span_alignment <= PartitionPageSize())) {
     auto* tcache = internal::ThreadCache::Get();
     // LIKELY: Typically always true, except for the very first allocation of
     // this thread.
@@ -1374,12 +1410,14 @@
       PA_DCHECK(!slot_span->bucket->is_direct_mapped());
 #endif
     } else {
-      slot_start = RawAlloc(buckets + bucket_index, flags, raw_size,
-                            &usable_size, &is_already_zeroed);
+      slot_start =
+          RawAlloc(buckets + bucket_index, flags, raw_size, slot_span_alignment,
+                   &usable_size, &is_already_zeroed);
     }
   } else {
-    slot_start = RawAlloc(buckets + bucket_index, flags, raw_size, &usable_size,
-                          &is_already_zeroed);
+    slot_start =
+        RawAlloc(buckets + bucket_index, flags, raw_size, slot_span_alignment,
+                 &usable_size, &is_already_zeroed);
   }
 
   if (UNLIKELY(!slot_start))
@@ -1483,11 +1521,12 @@
     Bucket* bucket,
     int flags,
     size_t raw_size,
+    size_t slot_span_alignment,
     size_t* usable_size,
     bool* is_already_zeroed) {
   internal::ScopedGuard<thread_safe> guard{lock_};
-  return AllocFromBucket(bucket, flags, raw_size, usable_size,
-                         is_already_zeroed);
+  return AllocFromBucket(bucket, flags, raw_size, slot_span_alignment,
+                         usable_size, is_already_zeroed);
 }
 
 template <bool thread_safe>
@@ -1496,12 +1535,23 @@
     size_t alignment,
     size_t requested_size) {
   // Aligned allocation support relies on the natural alignment guarantees of
-  // PartitionAlloc. Specifically, it relies on the fact that slot spans are
-  // partition-page aligned, and slots within are aligned to slot size, from
-  // the beginning of the span. As a conseqneuce, allocations of sizes that are
-  // a power of two, up to partition page size, will always be aligned to its
-  // size. The code below adjusts the request size to be a power of two.
-  // TODO(bartekn): Handle requests larger than partition page.
+  // PartitionAlloc. Specifically, it relies on the fact that slots within a
+  // slot span are aligned to slot size, from the beginning of the span.
+  //
+  // For alignments <=PartitionPageSize(), the code below adjusts the request
+  // size to be a power of two, no less than alignment. Since slot spans are
+  // aligned to PartitionPageSize(), which is also a power of two, this will
+  // automatically guarantee alignment on the adjusted size boundary, thanks to
+  // the natural alignment described above.
+  //
+  // For alignments >PartitionPageSize(), we need to pass the request down the
+  // stack to only give us a slot span aligned to this more restrictive
+  // boundary. In the current implementation, this code path will always
+  // allocate a new slot span and hand us the first slot, so no need to adjust
+  // the request size. As a consequence, allocating many small objects with
+  // such a high alignment can cause a non-negligable fragmentation,
+  // particularly if these allocations are back to back.
+  // TODO(bartekn): We should check that this is not causing issues in practice.
   //
   // Extras before the allocation are forbidden as they shift the returned
   // allocation from the beginning of the slot, thus messing up alignment.
@@ -1511,40 +1561,51 @@
   PA_DCHECK(!extras_offset);
   // This is mandated by |posix_memalign()|, so should never fire.
   PA_CHECK(base::bits::IsPowerOfTwo(alignment));
-
+  // Catch unsupported alignment requests early.
+  PA_CHECK(alignment <= kMaxSupportedAlignment);
   size_t raw_size = AdjustSizeForExtrasAdd(requested_size);
-  // Handle cases such as size = 16, alignment = 64.
-  // Wastes memory when a large alignment is requested with a small size, but
-  // this is hard to avoid, and should not be too common.
-  if (UNLIKELY(raw_size < alignment)) {
-    raw_size = alignment;
-  } else {
-    // PartitionAlloc only guarantees alignment for power-of-two sized
-    // allocations. To make sure this applies here, round up the allocation
-    // size.
-    raw_size = static_cast<size_t>(1)
-               << (sizeof(size_t) * 8 -
-                   base::bits::CountLeadingZeroBits(raw_size - 1));
-  }
-  PA_DCHECK(base::bits::IsPowerOfTwo(raw_size));
-  // Adjust back, because AllocFlagsNoHooks/Alloc will adjust it again.
-  size_t adjusted_size = AdjustSizeForExtrasSubtract(raw_size);
+  // TODO(bartekn): Support direct map. Until then, catch unsupported requests.
+  PA_CHECK(alignment <= PartitionPageSize() || raw_size <= kMaxBucketed);
 
-  // Overflow check. adjusted_size must be larger or equal to requested_size.
-  if (UNLIKELY(adjusted_size < requested_size)) {
-    if (flags & PartitionAllocReturnNull)
-      return nullptr;
-    // OutOfMemoryDeathTest.AlignedAlloc requires
-    // base::TerminateBecauseOutOfMemory (invoked by
-    // PartitionExcessiveAllocationSize).
-    internal::PartitionExcessiveAllocationSize(requested_size);
-    // internal::PartitionExcessiveAllocationSize(size) causes OOM_CRASH.
-    NOTREACHED();
+  size_t adjusted_size = requested_size;
+  if (alignment <= PartitionPageSize()) {
+    // Handle cases such as size = 16, alignment = 64.
+    // Wastes memory when a large alignment is requested with a small size, but
+    // this is hard to avoid, and should not be too common.
+    if (UNLIKELY(raw_size < alignment)) {
+      raw_size = alignment;
+    } else {
+      // PartitionAlloc only guarantees alignment for power-of-two sized
+      // allocations. To make sure this applies here, round up the allocation
+      // size.
+      raw_size = static_cast<size_t>(1)
+                 << (sizeof(size_t) * 8 -
+                     base::bits::CountLeadingZeroBits(raw_size - 1));
+    }
+    PA_DCHECK(base::bits::IsPowerOfTwo(raw_size));
+    // Adjust back, because AllocFlagsNoHooks/Alloc will adjust it again.
+    adjusted_size = AdjustSizeForExtrasSubtract(raw_size);
+
+    // Overflow check. adjusted_size must be larger or equal to requested_size.
+    if (UNLIKELY(adjusted_size < requested_size)) {
+      if (flags & PartitionAllocReturnNull)
+        return nullptr;
+      // OutOfMemoryDeathTest.AlignedAlloc requires
+      // base::TerminateBecauseOutOfMemory (invoked by
+      // PartitionExcessiveAllocationSize).
+      internal::PartitionExcessiveAllocationSize(requested_size);
+      // internal::PartitionExcessiveAllocationSize(size) causes OOM_CRASH.
+      NOTREACHED();
+    }
   }
 
+  // Slot spans are naturally aligned on partition page size, but make sure you
+  // don't pass anything less, because it'll mess up callee's calculations.
+  size_t slot_span_alignment = std::max(alignment, PartitionPageSize());
   bool no_hooks = flags & PartitionAllocNoHooks;
   void* ptr =
-      no_hooks ? AllocFlagsNoHooks(0, adjusted_size) : Alloc(adjusted_size, "");
+      no_hooks ? AllocFlagsNoHooks(0, adjusted_size, slot_span_alignment)
+               : AllocFlagsInternal(0, adjusted_size, slot_span_alignment, "");
 
   // |alignment| is a power of two, but the compiler doesn't necessarily know
   // that. A regular % operation is very slow, make sure to use the equivalent,
diff --git a/base/allocator/partition_allocator/starscan/metadata_allocator.h b/base/allocator/partition_allocator/starscan/metadata_allocator.h
index cc61db8..ed2d8ff 100644
--- a/base/allocator/partition_allocator/starscan/metadata_allocator.h
+++ b/base/allocator/partition_allocator/starscan/metadata_allocator.h
@@ -5,6 +5,7 @@
 #ifndef BASE_ALLOCATOR_PARTITION_ALLOCATOR_STARSCAN_METADATA_ALLOCATOR_H_
 #define BASE_ALLOCATOR_PARTITION_ALLOCATOR_STARSCAN_METADATA_ALLOCATOR_H_
 
+#include "base/allocator/partition_allocator/partition_alloc_constants.h"
 #include "base/allocator/partition_allocator/partition_root.h"
 
 namespace base {
@@ -35,7 +36,7 @@
 
   value_type* allocate(size_t size) {
     return static_cast<value_type*>(PCScanMetadataAllocator().AllocFlagsNoHooks(
-        0, size * sizeof(value_type)));
+        0, size * sizeof(value_type), PartitionPageSize()));
   }
 
   void deallocate(value_type* ptr, size_t size) {
@@ -46,7 +47,8 @@
 // Inherit from it to make a class allocated on the metadata partition.
 struct AllocatedOnPCScanMetadataPartition {
   static void* operator new(size_t size) {
-    return PCScanMetadataAllocator().AllocFlagsNoHooks(0, size);
+    return PCScanMetadataAllocator().AllocFlagsNoHooks(0, size,
+                                                       PartitionPageSize());
   }
   static void operator delete(void* ptr) {
     PCScanMetadataAllocator().FreeNoHooks(ptr);
@@ -55,8 +57,8 @@
 
 template <typename T, typename... Args>
 T* MakePCScanMetadata(Args&&... args) {
-  auto* memory = static_cast<T*>(
-      PCScanMetadataAllocator().AllocFlagsNoHooks(0, sizeof(T)));
+  auto* memory = static_cast<T*>(PCScanMetadataAllocator().AllocFlagsNoHooks(
+      0, sizeof(T), PartitionPageSize()));
   return new (memory) T(std::forward<Args>(args)...);
 }
 
diff --git a/base/allocator/partition_allocator/starscan/pcscan_unittest.cc b/base/allocator/partition_allocator/starscan/pcscan_unittest.cc
index 4687768..82b0433 100644
--- a/base/allocator/partition_allocator/starscan/pcscan_unittest.cc
+++ b/base/allocator/partition_allocator/starscan/pcscan_unittest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/allocator/partition_allocator/partition_alloc_constants.h"
 #include "base/allocator/partition_allocator/partition_root.h"
 #if !defined(MEMORY_TOOL_REPLACES_ALLOCATOR)
 
@@ -44,10 +45,10 @@
     // Previous test runs within the same process decommit GigaCage, therefore
     // we need to make sure that the card table is recommitted for each run.
     PCScan::ReinitForTesting();
-    allocator_.init({PartitionOptions::AlignedAlloc::kDisallowed,
+    allocator_.init({PartitionOptions::AlignedAlloc::kAllowed,
                      PartitionOptions::ThreadCache::kDisabled,
                      PartitionOptions::Quarantine::kAllowed,
-                     PartitionOptions::Cookies::kAllowed,
+                     PartitionOptions::Cookies::kDisallowed,
                      PartitionOptions::RefCount::kDisallowed});
     PCScan::RegisterScannableRoot(allocator_.root());
   }
@@ -110,7 +111,7 @@
   void* first = nullptr;
   void* last = nullptr;
   for (size_t i = 0; i < num_slots; ++i) {
-    void* ptr = root.AllocFlagsNoHooks(0, object_size);
+    void* ptr = root.AllocFlagsNoHooks(0, object_size, PartitionPageSize());
     EXPECT_TRUE(ptr);
     if (i == 0)
       first = root.AdjustPointerForExtrasSubtract(ptr);
@@ -148,12 +149,18 @@
   ListBase* next = nullptr;
 };
 
-template <size_t Size>
+template <size_t Size, size_t Alignment = 0>
 struct List final : ListBase {
   char buffer[Size];
 
   static List* Create(ThreadSafePartitionRoot& root, ListBase* next = nullptr) {
-    auto* list = static_cast<List*>(root.Alloc(sizeof(List), nullptr));
+    List* list;
+    if (Alignment) {
+      list = static_cast<List*>(
+          root.AlignedAllocFlags(0, Alignment, sizeof(List)));
+    } else {
+      list = static_cast<List*>(root.Alloc(sizeof(List), nullptr));
+    }
     list->next = next;
     return list;
   }
@@ -255,6 +262,52 @@
   TestDanglingReference(*this, source, value);
 }
 
+TEST_F(PCScanTest, DanglingReferenceDifferentBucketsAligned) {
+  // Choose a high alignment that almost certainly will cause a gap between slot
+  // spans. But make it less than kMaxSupportedAlignment, or else two
+  // allocations will end up on different super pages.
+  constexpr size_t alignment = kMaxSupportedAlignment / 2;
+  using SourceList = List<8, alignment>;
+  using ValueList = List<128, alignment>;
+
+  // Create two objects, where |source| references |value|.
+  auto* value = ValueList::Create(root(), nullptr);
+  auto* source = SourceList::Create(root(), value);
+
+  // Double check the setup -- make sure that exactly two slot spans were
+  // allocated, within the same super page, with a gap in between.
+  {
+    auto* value_root = ThreadSafePartitionRoot::FromPointerInNormalBuckets(
+        reinterpret_cast<char*>(value));
+    ScopedGuard<ThreadSafe> guard{value_root->lock_};
+
+    auto super_page = reinterpret_cast<uintptr_t>(value) & kSuperPageBaseMask;
+    ASSERT_EQ(super_page,
+              reinterpret_cast<uintptr_t>(source) & kSuperPageBaseMask);
+    size_t i = 0;
+    void* first_slot_span_end = nullptr;
+    void* second_slot_span_start = nullptr;
+    auto visited = IterateSlotSpans<ThreadSafe>(
+        reinterpret_cast<char*>(super_page), true,
+        [&](SlotSpan* slot_span) -> bool {
+          if (i == 0) {
+            first_slot_span_end = reinterpret_cast<char*>(
+                                      SlotSpan::ToSlotSpanStartPtr(slot_span)) +
+                                  slot_span->bucket->get_pages_per_slot_span() *
+                                      PartitionPageSize();
+          } else {
+            second_slot_span_start = SlotSpan::ToSlotSpanStartPtr(slot_span);
+          }
+          ++i;
+          return true;
+        });
+    ASSERT_EQ(visited, 2u);
+    ASSERT_GT(second_slot_span_start, first_slot_span_end);
+  }
+
+  TestDanglingReference(*this, source, value);
+}
+
 TEST_F(PCScanTest, DanglingReferenceSameSlotSpanButDifferentPages) {
   using SourceList = List<8>;
   using ValueList = SourceList;
@@ -291,7 +344,8 @@
   void* source_addr = full_slot_span.first;
   // This allocation must go through the slow path and call SetNewActivePage(),
   // which will flush the full page from the active page list.
-  void* value_addr = root().AllocFlagsNoHooks(0, sizeof(ValueList));
+  void* value_addr =
+      root().AllocFlagsNoHooks(0, sizeof(ValueList), PartitionPageSize());
 
   // Assert that the first and the last objects are in different slot spans but
   // in the same bucket.
diff --git a/base/allocator/partition_allocator/thread_cache.cc b/base/allocator/partition_allocator/thread_cache.cc
index 011ec2e8..df069a8 100644
--- a/base/allocator/partition_allocator/thread_cache.cc
+++ b/base/allocator/partition_allocator/thread_cache.cc
@@ -10,6 +10,7 @@
 
 #include "base/allocator/partition_allocator/partition_alloc_check.h"
 #include "base/allocator/partition_allocator/partition_alloc_config.h"
+#include "base/allocator/partition_allocator/partition_alloc_constants.h"
 #include "base/allocator/partition_allocator/partition_root.h"
 #include "base/base_export.h"
 #include "base/dcheck_is_on.h"
@@ -376,8 +377,9 @@
   auto* bucket =
       root->buckets +
       PartitionRoot<internal::ThreadSafe>::SizeToBucketIndex(raw_size);
-  void* buffer = root->RawAlloc(bucket, PartitionAllocZeroFill, raw_size,
-                                &usable_size, &already_zeroed);
+  void* buffer =
+      root->RawAlloc(bucket, PartitionAllocZeroFill, raw_size,
+                     PartitionPageSize(), &usable_size, &already_zeroed);
   ThreadCache* tcache = new (buffer) ThreadCache(root);
 
   // This may allocate.
@@ -503,8 +505,8 @@
     void* ptr = root_->AllocFromBucket(
         &root_->buckets[bucket_index],
         PartitionAllocFastPathOrReturnNull | PartitionAllocReturnNull,
-        root_->buckets[bucket_index].slot_size /* raw_size */, &usable_size,
-        &is_already_zeroed);
+        root_->buckets[bucket_index].slot_size /* raw_size */,
+        PartitionPageSize(), &usable_size, &is_already_zeroed);
 
     // Either the previous allocation would require a slow path allocation, or
     // the central allocator is out of memory. If the bucket was filled with
diff --git a/base/memory/nonscannable_memory.cc b/base/memory/nonscannable_memory.cc
index 35b627a4..81f730c 100644
--- a/base/memory/nonscannable_memory.cc
+++ b/base/memory/nonscannable_memory.cc
@@ -30,10 +30,11 @@
   // TODO(bikineev): Change to LIKELY once PCScan is enabled by default.
   if (UNLIKELY(pcscan_enabled_.load(std::memory_order_acquire))) {
     PA_DCHECK(allocator_.get());
-    return allocator_->root()->AllocFlagsNoHooks(0, size);
+    return allocator_->root()->AllocFlagsNoHooks(0, size, PartitionPageSize());
   }
   // Otherwise, dispatch to default partition.
-  return PartitionAllocMalloc::Allocator()->AllocFlagsNoHooks(0, size);
+  return PartitionAllocMalloc::Allocator()->AllocFlagsNoHooks(
+      0, size, PartitionPageSize());
 }
 
 void NonScannableAllocator::Free(void* ptr) {
diff --git a/base/optional.h b/base/optional.h
index b105cc0..bdbbf9c2 100644
--- a/base/optional.h
+++ b/base/optional.h
@@ -14,9 +14,9 @@
 template <typename T>
 using Optional [[deprecated]] = absl::optional<T>;
 
-using absl::make_optional [[deprecated]];
-using absl::nullopt [[deprecated]];
-using absl::nullopt_t [[deprecated]];
+using absl::make_optional;
+using absl::nullopt;
+using absl::nullopt_t;
 
 }  // namespace base
 
diff --git a/build/config/linux/pangocairo/pangocairo.gni b/build/config/linux/pangocairo/pangocairo.gni
index ecfe663..6bc7529 100644
--- a/build/config/linux/pangocairo/pangocairo.gni
+++ b/build/config/linux/pangocairo/pangocairo.gni
@@ -6,8 +6,5 @@
 import("//build/config/ui.gni")
 
 declare_args() {
-  use_pangocairo =
-      # TODO(crbug.com/1052397): Remove !chromeos_is_browser_only once
-      # lacros-chrome switches to target_os="chromeos"
-      is_linux && !is_chromecast && !chromeos_is_browser_only
+  use_pangocairo = is_linux && !is_chromecast
 }
diff --git a/build/fuchsia/linux.sdk.sha1 b/build/fuchsia/linux.sdk.sha1
index 0519f7e..431de86 100644
--- a/build/fuchsia/linux.sdk.sha1
+++ b/build/fuchsia/linux.sdk.sha1
@@ -1 +1 @@
-4.20210518.1.1
+4.20210518.2.1
diff --git a/build/fuchsia/mac.sdk.sha1 b/build/fuchsia/mac.sdk.sha1
index 0519f7e..431de86 100644
--- a/build/fuchsia/mac.sdk.sha1
+++ b/build/fuchsia/mac.sdk.sha1
@@ -1 +1 @@
-4.20210518.1.1
+4.20210518.2.1
diff --git a/chrome/VERSION b/chrome/VERSION
index b7f1808..839e4665 100644
--- a/chrome/VERSION
+++ b/chrome/VERSION
@@ -1,4 +1,4 @@
 MAJOR=92
 MINOR=0
-BUILD=4512
+BUILD=4513
 PATCH=0
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index 7065e7c..f8aa62f 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -20,6 +20,9 @@
 import("//chrome/android/modules/chrome_feature_module_tmpl.gni")
 import("//chrome/android/monochrome_android_manifest_jinja_variables.gni")
 import("//chrome/browser/commerce/price_tracking/android/java_sources.gni")
+import("//chrome/browser/commerce/subscriptions/android/java_sources.gni")
+import(
+    "//chrome/browser/commerce/subscriptions/test/android/test_java_sources.gni")
 import("//chrome/browser/feed/android/web_feed_java_sources.gni")
 import("//chrome/browser/share/android/java_sources.gni")
 import("//chrome/chrome_paks.gni")
@@ -313,7 +316,6 @@
     "//chrome/browser/banners/android:java",
     "//chrome/browser/browser_controls/android:java",
     "//chrome/browser/commerce/merchant_viewer/android:java",
-    "//chrome/browser/commerce/subscriptions/android:java",
     "//chrome/browser/consent_auditor/android:java",
     "//chrome/browser/contextmenu:java",
     "//chrome/browser/continuous_search:data_structures_java",
@@ -657,6 +659,11 @@
   # dependency when circular deps is resolved.
   sources += price_tracking_java_sources
 
+  # TODO(crbug/1210158): Instead of adding source files, add it as a separate
+  # dependency when circular deps is resolved.
+  sources += commerce_subscriptions_java_sources
+  deps += commerce_subscriptions_java_deps
+
   if (enable_basic_printing) {
     deps += [ "//printing:printing_java" ]
   }
@@ -693,7 +700,6 @@
     "//chrome/android/features/keyboard_accessory:internal_java",
     "//chrome/browser/attribution_reporting/android/internal:java",
     "//chrome/browser/commerce/merchant_viewer/android:java",
-    "//chrome/browser/commerce/subscriptions/android:java",
     "//chrome/browser/content_creation/notes/internal/android:java",
     "//chrome/browser/download/internal/android:java",
     "//chrome/browser/page_annotations/android:java",
@@ -878,7 +884,6 @@
     "//chrome/browser/browser_controls/android:java",
     "//chrome/browser/browser_controls/android:junit",
     "//chrome/browser/commerce/merchant_viewer/android:junit",
-    "//chrome/browser/commerce/subscriptions/test/android:junit",
     "//chrome/browser/contextmenu:java",
     "//chrome/browser/continuous_search:junit",
     "//chrome/browser/continuous_search/internal:junit",
@@ -1174,7 +1179,6 @@
     "//chrome/browser/banners/android:java",
     "//chrome/browser/browser_controls/android:java",
     "//chrome/browser/commerce/merchant_viewer/android:javatests",
-    "//chrome/browser/commerce/subscriptions/test/android:javatests",
     "//chrome/browser/contextmenu:java",
     "//chrome/browser/continuous_search:data_structures_java",
     "//chrome/browser/continuous_search:javatests",
@@ -1440,6 +1444,8 @@
 
   deps += feed_test_deps
 
+  deps += commerce_subscriptions_java_test_deps
+
   if (enable_basic_printing) {
     deps += [ "//printing:printing_java" ]
   }
diff --git a/chrome/android/features/cablev2_authenticator/java/src/org/chromium/chrome/browser/webauth/authenticator/BLEAdvert.java b/chrome/android/features/cablev2_authenticator/java/src/org/chromium/chrome/browser/webauth/authenticator/BLEAdvert.java
index c5c08d9..b831009 100644
--- a/chrome/android/features/cablev2_authenticator/java/src/org/chromium/chrome/browser/webauth/authenticator/BLEAdvert.java
+++ b/chrome/android/features/cablev2_authenticator/java/src/org/chromium/chrome/browser/webauth/authenticator/BLEAdvert.java
@@ -49,27 +49,9 @@
                         .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
                         .build();
         ParcelUuid fidoUuid = new ParcelUuid(UUID.fromString(CABLE_UUID));
-
-        // The first 16 bytes of the payload are encoded into a 16-byte UUID.
-        ByteBuffer bb = ByteBuffer.wrap(payload);
-        long high = bb.getLong();
-        long low = bb.getLong();
-        final UUID uuid16 = new UUID(high, low);
-
-        // The final four bytes of the payload are turned into a 4-byte UUID.
-        // Depending on the value of those four bytes, this might happen to be a
-        // 2-byte UUID, but the desktop handles that.
-        high = (long) bb.getInt();
-        high <<= 32;
-        // This is the fixed suffix for short UUIDs in Bluetooth.
-        high |= 0x1000;
-        low = 0x800000805f9b34fbL;
-        final UUID uuid4 = new UUID(high, low);
-
         AdvertiseData data = (new AdvertiseData.Builder())
                                      .addServiceUuid(fidoUuid)
-                                     .addServiceUuid(new ParcelUuid(uuid16))
-                                     .addServiceUuid(new ParcelUuid(uuid4))
+                                     .addServiceData(fidoUuid, payload)
                                      .setIncludeDeviceName(false)
                                      .setIncludeTxPowerLevel(false)
                                      .build();
diff --git a/chrome/android/features/keyboard_accessory/internal/java/res/layout/keyboard_accessory_sheet_tab_credit_card_info.xml b/chrome/android/features/keyboard_accessory/internal/java/res/layout/keyboard_accessory_sheet_tab_credit_card_info.xml
index 6d87e0c..ee86ce4 100644
--- a/chrome/android/features/keyboard_accessory/internal/java/res/layout/keyboard_accessory_sheet_tab_credit_card_info.xml
+++ b/chrome/android/features/keyboard_accessory/internal/java/res/layout/keyboard_accessory_sheet_tab_credit_card_info.xml
@@ -79,6 +79,12 @@
                 android:layout_height="wrap_content"
                 style="@style/InputChip" />
 
+            <org.chromium.ui.widget.ChipView
+                android:id="@+id/cvc"
+                android:gravity="center_vertical|start"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                style="@style/InputChip" />
         </LinearLayout>
 
         <ImageView
diff --git a/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/CreditCardAccessoryInfoView.java b/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/CreditCardAccessoryInfoView.java
index b7c84ae..e40bf6e7c 100644
--- a/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/CreditCardAccessoryInfoView.java
+++ b/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/CreditCardAccessoryInfoView.java
@@ -28,6 +28,7 @@
     private ChipView mExpMonth;
     private ChipView mExpYear;
     private ChipView mCardholder;
+    private ChipView mCvc;
 
     /**
      * Constructor for inflating from XML.
@@ -46,6 +47,7 @@
         mExpMonth = findViewById(R.id.exp_month);
         mExpYear = findViewById(R.id.exp_year);
         mCardholder = findViewById(R.id.cardholder);
+        mCvc = findViewById(R.id.cvc);
     }
 
     public void setIcon(@Nullable Drawable drawable) {
@@ -76,4 +78,8 @@
     public LinearLayout getExpiryGroup() {
         return mExpiryGroup;
     }
+
+    public ChipView getCvc() {
+        return mCvc;
+    }
 }
diff --git a/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/CreditCardAccessorySheetViewBinder.java b/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/CreditCardAccessorySheetViewBinder.java
index 53d16f8..81829847 100644
--- a/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/CreditCardAccessorySheetViewBinder.java
+++ b/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/CreditCardAccessorySheetViewBinder.java
@@ -51,6 +51,7 @@
             bindChipView(view.getExpMonth(), info.getFields().get(1));
             bindChipView(view.getExpYear(), info.getFields().get(2));
             bindChipView(view.getCardholder(), info.getFields().get(3));
+            bindChipView(view.getCvc(), info.getFields().get(4));
 
             view.getExpiryGroup().setVisibility(view.getExpYear().getVisibility() == View.VISIBLE
                                     || view.getExpMonth().getVisibility() == View.VISIBLE
diff --git a/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/sheet_component/AccessorySheetRenderTest.java b/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/sheet_component/AccessorySheetRenderTest.java
index 91845693..f7323d58a 100644
--- a/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/sheet_component/AccessorySheetRenderTest.java
+++ b/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/sheet_component/AccessorySheetRenderTest.java
@@ -196,6 +196,8 @@
                 new UserInfoField("2021", "2021", "-1", false, result -> {}));
         sheet.getUserInfoList().get(0).addField(
                 new UserInfoField("Todd Tester", "Todd Tester", "0", false, result -> {}));
+        sheet.getUserInfoList().get(0).addField(
+                new UserInfoField("123", "123", "-1", false, result -> {}));
         sheet.getUserInfoList().add(new KeyboardAccessoryData.UserInfo("", false));
         sheet.getUserInfoList().get(1).addField(
                 new UserInfoField("**** 8012", "Card for Maya Park", "1", false, result -> {}));
@@ -205,6 +207,8 @@
                 new UserInfoField("", "", "-1", false, result -> {}));
         sheet.getUserInfoList().get(1).addField( // Unused card holder field.
                 new UserInfoField("", "", "1", false, result -> {}));
+        sheet.getUserInfoList().get(1).addField(
+                new UserInfoField("", "", "-1", false, result -> {}));
         sheet.getFooterCommands().add(
                 new KeyboardAccessoryData.FooterCommand("Manage payment methods", cb -> {}));
 
diff --git a/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/CreditCardAccessorySheetViewTest.java b/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/CreditCardAccessorySheetViewTest.java
index d751f28..ae260f95 100644
--- a/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/CreditCardAccessorySheetViewTest.java
+++ b/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/CreditCardAccessorySheetViewTest.java
@@ -116,7 +116,7 @@
 
         TestThreadUtils.runOnUiThreadBlocking(() -> {
             mModel.add(new AccessorySheetDataPiece(
-                    createInfo("4111111111111111", "04", "2034", "Kirby Puckett", clicked),
+                    createInfo("4111111111111111", "04", "2034", "Kirby Puckett", "123", clicked),
                     AccessorySheetDataPiece.Type.CREDIT_CARD_INFO));
             mModel.add(new AccessorySheetDataPiece(
                     new KeyboardAccessoryData.FooterCommand("Manage credit cards", null),
@@ -151,6 +151,7 @@
             infoWithUnclickableField.addField(new UserInfoField("", "", "month", false, null));
             infoWithUnclickableField.addField(new UserInfoField("", "", "year", false, null));
             infoWithUnclickableField.addField(new UserInfoField("", "", "name", false, null));
+            infoWithUnclickableField.addField(new UserInfoField("", "", "cvc", false, null));
             mModel.add(new AccessorySheetDataPiece(
                     infoWithUnclickableField, AccessorySheetDataPiece.Type.CREDIT_CARD_INFO));
             mModel.add(new AccessorySheetDataPiece(
@@ -174,7 +175,7 @@
         TestThreadUtils.runOnUiThreadBlocking(() -> {
             mModel.add(new AccessorySheetDataPiece(
                     // Cardholder name is empty
-                    createInfo("4111111111111111", "04", "2034", "", clicked),
+                    createInfo("4111111111111111", "04", "2034", "", "", clicked),
                     AccessorySheetDataPiece.Type.CREDIT_CARD_INFO));
             mModel.add(new AccessorySheetDataPiece(
                     new KeyboardAccessoryData.FooterCommand("Manage credit cards", null),
@@ -184,6 +185,7 @@
         CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mView.get().getChildCount(), is(2)));
 
         assertThat(findChipView(R.id.cardholder).isShown(), is(false));
+        assertThat(findChipView(R.id.cvc).isShown(), is(false));
     }
 
     @Test
@@ -209,14 +211,15 @@
         assertThat(warningText.getText(), is(kWarning));
     }
 
-    private UserInfo createInfo(
-            String number, String month, String year, String name, AtomicBoolean clickRecorder) {
+    private UserInfo createInfo(String number, String month, String year, String name, String cvc,
+            AtomicBoolean clickRecorder) {
         UserInfo info = new UserInfo("", false);
         info.addField(
                 new UserInfoField(number, number, "", false, item -> clickRecorder.set(true)));
         info.addField(new UserInfoField(month, month, "", false, item -> clickRecorder.set(true)));
         info.addField(new UserInfoField(year, year, "", false, item -> clickRecorder.set(true)));
         info.addField(new UserInfoField(name, name, "", false, item -> clickRecorder.set(true)));
+        info.addField(new UserInfoField(cvc, cvc, "", false, item -> clickRecorder.set(true)));
         return info;
     }
 
diff --git a/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java b/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java
index 2527b08..c96235fb 100644
--- a/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java
+++ b/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java
@@ -1482,21 +1482,21 @@
     // clang-format off
     @CommandLineFlags.Add({BASE_PARAMS + "/single/exclude_mv_tiles/false"
             + "/new_home_surface_from_home_button/hide_mv_tiles_and_tab_switcher"})
-    @DisabledTest(message = "http://crbug/1206081 - the Instant_Return version is flaky.")
     public void testNewSurfaceFromHomeButton(){
         // clang-format on
-        assumeTrue(mImmediateReturn);
-        StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
+        if (mImmediateReturn) {
+            StartSurfaceTestUtils.waitForOverviewVisible(
+                    mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
 
-        onViewWaiting(
-                allOf(withId(org.chromium.chrome.tab_ui.R.id.mv_tiles_container), isDisplayed()));
-        onViewWaiting(withId(org.chromium.chrome.tab_ui.R.id.carousel_tab_switcher_container));
-        onViewWaiting(withId(R.id.start_tab_switcher_button));
+            onViewWaiting(
+                    allOf(withId(org.chromium.chrome.tab_ui.R.id.mv_tiles_layout), isDisplayed()));
+            onViewWaiting(withId(org.chromium.chrome.tab_ui.R.id.carousel_tab_switcher_container));
+            onViewWaiting(withId(R.id.start_tab_switcher_button));
 
-        // Launch a tab. The home button should show on the normal tab.
-        StartSurfaceTestUtils.launchFirstMVTile(cta, /* currentTabCount = */ 1);
+            // Launch a tab. The home button should show on the normal tab.
+            StartSurfaceTestUtils.launchFirstMVTile(cta, /* currentTabCount = */ 1);
+        }
 
         // Go back to the home surface, MV tiles and carousel tab switcher should not show anymore.
         StartSurfaceTestUtils.pressHomePageButton(cta);
@@ -1519,18 +1519,19 @@
             + "/new_home_surface_from_home_button/hide_tab_switcher_only"})
     public void testNewSurfaceHideTabOnlyFromHomeButton() {
         // clang-format on
-        assumeTrue(mImmediateReturn);
-        StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
+        if (mImmediateReturn) {
+            StartSurfaceTestUtils.waitForOverviewVisible(
+                    mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
 
-        onViewWaiting(withId(org.chromium.chrome.tab_ui.R.id.mv_tiles_container));
-        onViewWaiting(withId(org.chromium.chrome.tab_ui.R.id.carousel_tab_switcher_container));
-        onViewWaiting(withId(R.id.start_tab_switcher_button));
+            onViewWaiting(withId(org.chromium.chrome.tab_ui.R.id.mv_tiles_layout));
+            onViewWaiting(withId(org.chromium.chrome.tab_ui.R.id.carousel_tab_switcher_container));
+            onViewWaiting(withId(R.id.start_tab_switcher_button));
 
-        // Launch a tab. The home button should show on the normal tab.
-        StartSurfaceTestUtils.launchFirstMVTile(cta, /* currentTabCount = */ 1);
-        onViewWaiting(withId(R.id.home_button)).check(matches(isDisplayed()));
+            // Launch a tab. The home button should show on the normal tab.
+            StartSurfaceTestUtils.launchFirstMVTile(cta, /* currentTabCount = */ 1);
+            onViewWaiting(withId(R.id.home_button)).check(matches(isDisplayed()));
+        }
 
         // Go back to the home surface, MV tiles and carousel tab switcher should not show anymore.
         StartSurfaceTestUtils.pressHomePageButton(cta);
@@ -1539,7 +1540,7 @@
         StartSurfaceTestUtils.waitForOverviewVisible(
                 mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
         onViewWaiting(withId(R.id.start_tab_switcher_button));
-        onView(withId(org.chromium.chrome.tab_ui.R.id.mv_tiles_container))
+        onView(withId(org.chromium.chrome.tab_ui.R.id.mv_tiles_layout))
                 .check(matches(withEffectiveVisibility(VISIBLE)));
         onView(withId(org.chromium.chrome.tab_ui.R.id.carousel_tab_switcher_container))
                 .check(matches(withEffectiveVisibility(GONE)));
@@ -1728,6 +1729,7 @@
     @Feature({"StartSurface"})
     @CommandLineFlags.Add({BASE_PARAMS + "/single"})
     public void testOpenTileInIncognitoTabWithContextMenu() throws ExecutionException {
+        Assume.assumeFalse("https://crbug.com/1210554", mUseInstantStart && mImmediateReturn);
         if (!mImmediateReturn) {
             StartSurfaceTestUtils.pressHomePageButton(mActivityTestRule.getActivity());
         }
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceMediator.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceMediator.java
index c6d60c2..3d27db66 100644
--- a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceMediator.java
+++ b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceMediator.java
@@ -144,11 +144,15 @@
     private class FeedSurfaceHeaderSelectedCallback implements OnSectionHeaderSelectedListener {
         @Override
         public void onSectionHeaderSelected(int index) {
+            PropertyListModel<PropertyModel, PropertyKey> headerList =
+                    mSectionHeaderModel.get(SectionHeaderListProperties.SECTION_HEADERS_KEY);
+            if (index >= headerList.size()) {
+                Log.e(TAG, "Attempted to select a Tab with an invalid index");
+                return;
+            }
             mSectionHeaderModel.set(SectionHeaderListProperties.CURRENT_TAB_INDEX_KEY, index);
             Runnable onSelectCallback =
-                    mSectionHeaderModel.get(SectionHeaderListProperties.SECTION_HEADERS_KEY)
-                            .get(index)
-                            .get(SectionHeaderProperties.ON_SELECT_CALLBACK_KEY);
+                    headerList.get(index).get(SectionHeaderProperties.ON_SELECT_CALLBACK_KEY);
             if (onSelectCallback != null) {
                 onSelectCallback.run();
             }
@@ -1024,6 +1028,7 @@
     private ScrollState getScrollStateForAutoScrollToTop() {
         ScrollState state = new ScrollState();
         state.position = 1;
+        state.lastPosition = 5;
         return state;
     }
 
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/shared/FeedFeatures.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/shared/FeedFeatures.java
index 65994f91..4b8519b9 100644
--- a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/shared/FeedFeatures.java
+++ b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/shared/FeedFeatures.java
@@ -42,7 +42,7 @@
     public static boolean isAutoScrollToTopEnabled() {
         CommandLine commandLine = CommandLine.getInstance();
         if (commandLine == null) return false;
-        return commandLine.hasSwitch("feed_auto_scroll_to_top");
+        return commandLine.hasSwitch("feed-screenshot-mode");
     }
 
     private static PrefService getPrefService() {
diff --git a/chrome/android/java/res/xml/search_widget_info.xml b/chrome/android/java/res/xml/search_widget_info.xml
index d80b478..442092c5 100644
--- a/chrome/android/java/res/xml/search_widget_info.xml
+++ b/chrome/android/java/res/xml/search_widget_info.xml
@@ -6,7 +6,7 @@
 <!-- This widget automatically updates once per day, and whenever Chrome starts up. -->
 <appwidget-provider
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:minWidth="250dp"
+    android:minWidth="240dp"
     android:minHeight="48dp"
     android:initialLayout="@layout/search_widget_template"
     android:previewImage="@drawable/widget_preview"
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SectionHeaderView.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SectionHeaderView.java
index daf2d1cd..a6bd041 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SectionHeaderView.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SectionHeaderView.java
@@ -152,6 +152,13 @@
         }
     }
 
+    /** Removes all tabs. */
+    void removeAllTabs() {
+        if (mTabLayout != null) {
+            mTabLayout.removeAllTabs();
+        }
+    }
+
     /**
      * Set the properties for the header tab at a particular index to text.
      *
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SectionHeaderViewBinder.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SectionHeaderViewBinder.java
index 0f1325a..cfa92e6 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SectionHeaderViewBinder.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SectionHeaderViewBinder.java
@@ -62,6 +62,11 @@
     @Override
     public void onItemsRemoved(PropertyListModel<PropertyModel, PropertyKey> model,
             SectionHeaderView view, int index, int count) {
+        if (model.size() == 0) {
+            // All headers were removed.
+            view.removeAllTabs();
+            return;
+        }
         for (int i = index + count - 1; i >= index; i--) {
             view.removeTabAt(i);
         }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedRootUiCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedRootUiCoordinator.java
index 5257d82..ee025118 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedRootUiCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedRootUiCoordinator.java
@@ -63,6 +63,8 @@
 import org.chromium.chrome.browser.signin.SyncConsentActivityLauncherImpl;
 import org.chromium.chrome.browser.signin.ui.SigninPromoUtil;
 import org.chromium.chrome.browser.status_indicator.StatusIndicatorCoordinator;
+import org.chromium.chrome.browser.subscriptions.CommerceSubscriptionsService;
+import org.chromium.chrome.browser.subscriptions.CommerceSubscriptionsServiceFactory;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.TabAssociatedApp;
 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
@@ -119,6 +121,7 @@
     private HeightObserver mContinuousSearchObserver;
     private TabObscuringHandler.Observer mContinuousSearchTabObscuringHandlerObserver;
     private MerchantTrustSignalsCoordinator mMerchantTrustSignalsCoordinator;
+    private CommerceSubscriptionsService mCommerceSubscriptionsService;
 
     private int mStatusIndicatorHeight;
     private int mContinuousSearchHeight;
@@ -232,6 +235,11 @@
             mMerchantTrustSignalsCoordinator = null;
         }
 
+        if (mCommerceSubscriptionsService != null) {
+            mCommerceSubscriptionsService.destroy();
+            mCommerceSubscriptionsService = null;
+        }
+
         super.onDestroy();
     }
 
@@ -348,6 +356,7 @@
         initContinuousSearchCoordinator();
 
         initMerchantTrustSignals();
+        initCommerceSubscriptionsService();
     }
 
     private void initMerchantTrustSignals() {
@@ -484,6 +493,17 @@
         if (animate) browserControlsSizer.setAnimateBrowserControlsHeightChanges(false);
     }
 
+    private void initCommerceSubscriptionsService() {
+        if (!TabUiFeatureUtilities.ENABLE_PRICE_NOTIFICATION.getValue()) {
+            return;
+        }
+
+        CommerceSubscriptionsServiceFactory factory = new CommerceSubscriptionsServiceFactory();
+        mCommerceSubscriptionsService = factory.getForLastUsedProfile();
+        mCommerceSubscriptionsService.initDeferredStartupForActivity(
+                mActivity.getTabModelSelector(), mActivity.getLifecycleDispatcher());
+    }
+
     private void initStatusIndicatorCoordinator(LayoutManagerImpl layoutManager) {
         // TODO(crbug.com/1035584): Disable on tablets for now as we need to do one or two extra
         // things for tablets.
diff --git a/chrome/android/java_sources.gni b/chrome/android/java_sources.gni
index 7979c798..5d622d4 100644
--- a/chrome/android/java_sources.gni
+++ b/chrome/android/java_sources.gni
@@ -14,6 +14,8 @@
 import("//chrome/android/features/tab_ui/tab_management_java_sources.gni")
 import("//chrome/android/feed/feed_java_sources.gni")
 import("//chrome/browser/commerce/price_tracking/android/test_java_sources.gni")
+import(
+    "//chrome/browser/commerce/subscriptions/test/android/test_java_sources.gni")
 import("//chrome/browser/feed/android/web_feed_java_sources.gni")
 import("//chrome/browser/share/android/test_java_sources.gni")
 import("//chrome/common/features.gni")
@@ -46,6 +48,9 @@
 chrome_junit_test_java_sources += share_junit_test_java_sources
 chrome_junit_test_java_deps = share_junit_test_java_deps
 chrome_junit_test_java_deps += feed_test_deps
+chrome_junit_test_java_sources += commerce_subscriptions_junit_test_sources
+chrome_junit_test_java_deps += commerce_subscriptions_junit_test_deps
+chrome_test_java_sources += commerce_subscriptions_java_test_sources
 
 if (enable_arcore) {
   chrome_java_sources += [
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogTest.java
index 05e80a5..2180c5d 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogTest.java
@@ -6,13 +6,14 @@
 
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.Visibility.GONE;
 import static androidx.test.espresso.matcher.ViewMatchers.isChecked;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static androidx.test.espresso.matcher.ViewMatchers.isNotChecked;
+import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
-import static org.mockito.AdditionalMatchers.not;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.when;
 
@@ -129,6 +130,10 @@
         when(mPrefService.getInteger(Pref.PROMPT_FOR_DOWNLOAD_ANDROID)).thenReturn(promptStatus);
     }
 
+    private void setPromptForPolicy(boolean promptForPolicy) {
+        when(mPrefService.getBoolean(Pref.PROMPT_FOR_DOWNLOAD)).thenReturn(promptForPolicy);
+    }
+
     private void showDialog(
             long totalBytes, @DownloadLocationDialogType int dialogType, String suggestedPath) {
         TestThreadUtils.runOnUiThreadBlocking(() -> {
@@ -152,7 +157,7 @@
      */
     private void assertDontShowAgainCheckbox(Boolean checked) {
         if (checked == null) {
-            onView(withId(R.id.show_again_checkbox)).check(matches(not(isDisplayed())));
+            onView(withId(R.id.show_again_checkbox)).check(matches(withEffectiveVisibility(GONE)));
         } else if (checked) {
             onView(withId(R.id.show_again_checkbox)).check(matches(isChecked()));
         } else {
@@ -178,4 +183,16 @@
         assertSubtitle(DownloadUtils.getStringForBytes(getActivity(), TOTAL_BYTES));
         assertDontShowAgainCheckbox(false);
     }
+
+    @Test
+    @MediumTest
+    public void testForceShowEnterprisePolicy() {
+        when(mDownloadDialogBridgeJniMock.isLocationDialogManaged()).thenReturn(true);
+        setPromptForPolicy(true);
+        setDownloadPromptStatus(DownloadPromptStatus.SHOW_PREFERENCE);
+        showDialog(TOTAL_BYTES, DownloadLocationDialogType.DEFAULT, SUGGESTED_PATH);
+        assertTitle(R.string.download_location_dialog_title_confirm_download);
+        assertSubtitle(DownloadUtils.getStringForBytes(getActivity(), TOTAL_BYTES));
+        assertDontShowAgainCheckbox(null);
+    }
 }
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/feed/v2/FeedStreamTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/feed/v2/FeedStreamTest.java
index 9863831..92140415b6 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/feed/v2/FeedStreamTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/feed/v2/FeedStreamTest.java
@@ -37,6 +37,7 @@
 import org.mockito.MockitoAnnotations;
 import org.robolectric.Robolectric;
 import org.robolectric.annotation.Config;
+import org.robolectric.annotation.LooperMode;
 import org.robolectric.shadows.ShadowLog;
 
 import org.chromium.base.Callback;
@@ -75,6 +76,8 @@
 /** Unit tests for {@link FeedStream}. */
 @RunWith(BaseRobolectricTestRunner.class)
 @Config(manifest = Config.NONE, shadows = {ShadowPostTask.class, ShadowRecordHistogram.class})
+// TODO(crbug.com/1210371): Rewrite using paused loop. See crbug for details.
+@LooperMode(LooperMode.Mode.LEGACY)
 public class FeedStreamTest {
     private static final int LOAD_MORE_TRIGGER_LOOKAHEAD = 5;
     private static final int LOAD_MORE_TRIGGER_SCROLL_DISTANCE_DP = 100;
diff --git a/chrome/app/chromeos_strings.grdp b/chrome/app/chromeos_strings.grdp
index 88ba7c2..f5af4c8 100644
--- a/chrome/app/chromeos_strings.grdp
+++ b/chrome/app/chromeos_strings.grdp
@@ -354,6 +354,9 @@
   <message name="IDS_CELLULAR_SETUP_ESIM_PAGE_SCAN_QR_CODE_LOADING" desc="Label informing the user that the activation code is currently being verified.">
     Verifying activation code...
   </message>
+  <message name="IDS_CELLULAR_SETUP_ESIM_PAGE_VERIFYING_ACTIVATION_CODE" desc="Label informing the user that the activation code is currently being verified, and that it may take a couple of minutes before it is complete.">
+    Verifying activation code. This may take a few minutes.
+  </message>
   <message name="IDS_CELLULAR_SETUP_ESIM_PAGE_SCAN_QR_CODE_INVALID" desc="Label informing the user that the code used to install an eSIM profile is invalid.">
     Invalid code. Please try again.
   </message>
@@ -6036,4 +6039,9 @@
     </message>
   </if>
 
+   <!-- Strings for Projector-->
+  <message name="IDS_SELFIE_CAM_TITLE" desc="The title for the selfie camera dialog.">
+    Selfie Camera
+  </message>
+
 </grit-part>
diff --git a/chrome/app/chromeos_strings_grdp/IDS_CELLULAR_SETUP_ESIM_PAGE_VERIFYING_ACTIVATION_CODE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_CELLULAR_SETUP_ESIM_PAGE_VERIFYING_ACTIVATION_CODE.png.sha1
new file mode 100644
index 0000000..525e00b
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_CELLULAR_SETUP_ESIM_PAGE_VERIFYING_ACTIVATION_CODE.png.sha1
@@ -0,0 +1 @@
+b25b3277abcc39376632b9a832d89d3cbf826a53
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_SELFIE_CAM_TITLE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_SELFIE_CAM_TITLE.png.sha1
new file mode 100644
index 0000000..70c7e97
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_SELFIE_CAM_TITLE.png.sha1
@@ -0,0 +1 @@
+ca965f8892632fdb05f1abd84dbb29716c814e7d
\ No newline at end of file
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 497e9053..1f5d7c3a 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -2087,6 +2087,7 @@
     "//components/language/core/common",
     "//components/lens",
     "//components/leveldb_proto",
+    "//components/live_caption:constants",
     "//components/lookalikes/core",
     "//components/lookalikes/core:features",
     "//components/metrics:call_stack_profile_collector",
@@ -3399,10 +3400,10 @@
       "accessibility/caption_controller.h",
       "accessibility/caption_controller_factory.cc",
       "accessibility/caption_controller_factory.h",
-      "accessibility/caption_host_impl.cc",
-      "accessibility/caption_host_impl.h",
       "accessibility/invert_bubble_prefs.cc",
       "accessibility/invert_bubble_prefs.h",
+      "accessibility/live_caption_speech_recognition_host.cc",
+      "accessibility/live_caption_speech_recognition_host.h",
       "apps/app_service/app_icon_factory.cc",
       "apps/app_service/app_icon_factory.h",
       "apps/app_service/app_icon_source.cc",
@@ -3583,10 +3584,14 @@
       "enterprise/connectors/connectors_service.h",
       "enterprise/connectors/device_trust/attestation_service.cc",
       "enterprise/connectors/device_trust/attestation_service.h",
+      "enterprise/connectors/device_trust/crypto_utility.cc",
+      "enterprise/connectors/device_trust/crypto_utility.h",
       "enterprise/connectors/device_trust/device_trust_factory.cc",
       "enterprise/connectors/device_trust/device_trust_factory.h",
       "enterprise/connectors/device_trust/device_trust_service.cc",
       "enterprise/connectors/device_trust/device_trust_service.h",
+      "enterprise/connectors/device_trust/google_keys.cc",
+      "enterprise/connectors/device_trust/google_keys.h",
       "enterprise/connectors/device_trust/signal_reporter.cc",
       "enterprise/connectors/device_trust/signal_reporter.h",
       "enterprise/connectors/enterprise_connectors_policy_handler.cc",
@@ -4240,7 +4245,6 @@
       "//components/feedback/content:factory",
       "//components/image_fetcher/core",
       "//components/keep_alive_registry",
-      "//components/live_caption:constants",
       "//components/pref_registry",
       "//components/services/app_service:lib",
       "//components/services/app_service/public/cpp:app_file_handling",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 96578742..f802a123 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -6261,9 +6261,6 @@
 #endif  // !defined(OS_ANDROID)
 
 #if defined(OS_ANDROID)
-    {"page-info-version-2", flag_descriptions::kPageInfoV2Name,
-     flag_descriptions::kPageInfoV2Description, kOsAndroid,
-     FEATURE_VALUE_TYPE(page_info::kPageInfoV2)},
     {"page-info-discoverability",
      flag_descriptions::kPageInfoDiscoverabilityName,
      flag_descriptions::kPageInfoDiscoverabilityDescription, kOsAndroid,
@@ -6697,8 +6694,9 @@
      flag_descriptions::kSyncAutofillWalletOfferDataDescription, kOsAll,
      FEATURE_VALUE_TYPE(switches::kSyncAutofillWalletOfferData)},
 
-#if defined(OS_WIN) || defined(OS_MAC) || defined(OS_LINUX) || \
-    defined(OS_CHROMEOS)
+#if (defined(OS_WIN) || defined(OS_MAC) || defined(OS_LINUX) || \
+     defined(OS_CHROMEOS)) &&                                   \
+    BUILDFLAG(ENABLE_PRINTING)
     {"enable-oop-print-drivers", flag_descriptions::kEnableOopPrintDriversName,
      flag_descriptions::kEnableOopPrintDriversDescription, kOsDesktop,
      FEATURE_VALUE_TYPE(printing::features::kEnableOopPrintDrivers)},
diff --git a/chrome/browser/accessibility/caption_controller.cc b/chrome/browser/accessibility/caption_controller.cc
index 4398214d..5c9c839 100644
--- a/chrome/browser/accessibility/caption_controller.cc
+++ b/chrome/browser/accessibility/caption_controller.cc
@@ -9,8 +9,8 @@
 #include "base/bind.h"
 #include "base/command_line.h"
 #include "base/metrics/histogram_functions.h"
-#include "chrome/browser/accessibility/caption_host_impl.h"
 #include "chrome/browser/accessibility/caption_util.h"
+#include "chrome/browser/accessibility/live_caption_speech_recognition_host.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/ui/caption_bubble_controller.h"
 #include "chrome/common/pref_names.h"
@@ -213,23 +213,27 @@
 }
 
 bool CaptionController::DispatchTranscription(
-    CaptionHostImpl* caption_host_impl,
+    LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host,
     const media::mojom::SpeechRecognitionResultPtr& result) {
   if (!caption_bubble_controller_)
     return false;
-  return caption_bubble_controller_->OnTranscription(caption_host_impl, result);
+  return caption_bubble_controller_->OnTranscription(
+      live_caption_speech_recognition_host, result);
 }
 
-void CaptionController::OnError(CaptionHostImpl* caption_host_impl) {
+void CaptionController::OnError(
+    LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host) {
   if (!caption_bubble_controller_)
     return;
-  caption_bubble_controller_->OnError(caption_host_impl);
+  caption_bubble_controller_->OnError(live_caption_speech_recognition_host);
 }
 
-void CaptionController::OnAudioStreamEnd(CaptionHostImpl* caption_host_impl) {
+void CaptionController::OnAudioStreamEnd(
+    LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host) {
   if (!caption_bubble_controller_)
     return;
-  caption_bubble_controller_->OnAudioStreamEnd(caption_host_impl);
+  caption_bubble_controller_->OnAudioStreamEnd(
+      live_caption_speech_recognition_host);
 }
 
 void CaptionController::OnLanguageIdentificationEvent(
diff --git a/chrome/browser/accessibility/caption_controller.h b/chrome/browser/accessibility/caption_controller.h
index 6afbc50..e0cd6c6 100644
--- a/chrome/browser/accessibility/caption_controller.h
+++ b/chrome/browser/accessibility/caption_controller.h
@@ -28,7 +28,7 @@
 namespace captions {
 
 class CaptionBubbleController;
-class CaptionHostImpl;
+class LiveCaptionSpeechRecognitionHost;
 
 ///////////////////////////////////////////////////////////////////////////////
 // Caption Controller
@@ -56,7 +56,7 @@
   // transcription result was routed successfully. Transcriptions will halt if
   // this returns false.
   bool DispatchTranscription(
-      CaptionHostImpl* caption_host_impl,
+      LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host,
       const media::mojom::SpeechRecognitionResultPtr& result);
 
   void OnLanguageIdentificationEvent(
@@ -64,10 +64,12 @@
 
   // Alerts the CaptionBubbleController that there is an error in the speech
   // recognition service.
-  void OnError(CaptionHostImpl* caption_host_impl);
+  void OnError(
+      LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host);
 
   // Alerts the CaptionBubbleController that the audio stream has ended.
-  void OnAudioStreamEnd(CaptionHostImpl* caption_host_impl);
+  void OnAudioStreamEnd(
+      LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host);
 
  private:
   friend class CaptionControllerFactory;
diff --git a/chrome/browser/accessibility/caption_controller_browsertest.cc b/chrome/browser/accessibility/caption_controller_browsertest.cc
index b65a8bb..b434f95 100644
--- a/chrome/browser/accessibility/caption_controller_browsertest.cc
+++ b/chrome/browser/accessibility/caption_controller_browsertest.cc
@@ -11,7 +11,7 @@
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
 #include "chrome/browser/accessibility/caption_controller_factory.h"
-#include "chrome/browser/accessibility/caption_host_impl.h"
+#include "chrome/browser/accessibility/live_caption_speech_recognition_host.h"
 #include "chrome/browser/browser_features.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/lifetime/application_lifetime.h"
@@ -110,12 +110,13 @@
     return GetControllerForProfile(profile)->caption_bubble_controller_.get();
   }
 
-  CaptionHostImpl* GetCaptionHostImpl() {
-    if (!caption_host_impl_) {
-      caption_host_impl_ = std::make_unique<CaptionHostImpl>(
+  LiveCaptionSpeechRecognitionHost* GetLiveCaptionSpeechRecognitionHost() {
+    if (!live_caption_speech_recognition_host_) {
+      live_caption_speech_recognition_host_ = std::make_unique<
+          LiveCaptionSpeechRecognitionHost>(
           browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame());
     }
-    return caption_host_impl_.get();
+    return live_caption_speech_recognition_host_.get();
   }
 
   bool DispatchTranscription(std::string text) {
@@ -124,20 +125,22 @@
 
   bool DispatchTranscriptionToProfile(std::string text, Profile* profile) {
     return GetControllerForProfile(profile)->DispatchTranscription(
-        GetCaptionHostImpl(),
+        GetLiveCaptionSpeechRecognitionHost(),
         media::mojom::SpeechRecognitionResult::New(text, false /* is_final */));
   }
 
   void OnError() { OnErrorOnProfile(browser()->profile()); }
 
   void OnErrorOnProfile(Profile* profile) {
-    GetControllerForProfile(profile)->OnError(GetCaptionHostImpl());
+    GetControllerForProfile(profile)->OnError(
+        GetLiveCaptionSpeechRecognitionHost());
   }
 
   void OnAudioStreamEnd() { OnAudioStreamEndOnProfile(browser()->profile()); }
 
   void OnAudioStreamEndOnProfile(Profile* profile) {
-    GetControllerForProfile(profile)->OnAudioStreamEnd(GetCaptionHostImpl());
+    GetControllerForProfile(profile)->OnAudioStreamEnd(
+        GetLiveCaptionSpeechRecognitionHost());
   }
 
   bool HasBubbleController() {
@@ -177,7 +180,8 @@
  private:
   base::test::ScopedFeatureList scoped_feature_list_;
 
-  std::unique_ptr<CaptionHostImpl> caption_host_impl_;
+  std::unique_ptr<LiveCaptionSpeechRecognitionHost>
+      live_caption_speech_recognition_host_;
 };
 
 IN_PROC_BROWSER_TEST_F(CaptionControllerTest, ProfilePrefsAreRegistered) {
diff --git a/chrome/browser/accessibility/caption_host_impl.cc b/chrome/browser/accessibility/live_caption_speech_recognition_host.cc
similarity index 71%
rename from chrome/browser/accessibility/caption_host_impl.cc
rename to chrome/browser/accessibility/live_caption_speech_recognition_host.cc
index c050d1a..a488705 100644
--- a/chrome/browser/accessibility/caption_host_impl.cc
+++ b/chrome/browser/accessibility/live_caption_speech_recognition_host.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/accessibility/caption_host_impl.h"
+#include "chrome/browser/accessibility/live_caption_speech_recognition_host.h"
 
 #include <memory>
 #include <utility>
@@ -17,15 +17,17 @@
 namespace captions {
 
 // static
-void CaptionHostImpl::Create(
+void LiveCaptionSpeechRecognitionHost::Create(
     content::RenderFrameHost* frame_host,
     mojo::PendingReceiver<media::mojom::SpeechRecognitionRecognizerClient>
         receiver) {
-  mojo::MakeSelfOwnedReceiver(std::make_unique<CaptionHostImpl>(frame_host),
-                              std::move(receiver));
+  mojo::MakeSelfOwnedReceiver(
+      std::make_unique<LiveCaptionSpeechRecognitionHost>(frame_host),
+      std::move(receiver));
 }
 
-CaptionHostImpl::CaptionHostImpl(content::RenderFrameHost* frame_host)
+LiveCaptionSpeechRecognitionHost::LiveCaptionSpeechRecognitionHost(
+    content::RenderFrameHost* frame_host)
     : frame_host_(frame_host) {
   content::WebContents* web_contents = GetWebContents();
   if (!web_contents)
@@ -33,13 +35,13 @@
   Observe(web_contents);
 }
 
-CaptionHostImpl::~CaptionHostImpl() {
+LiveCaptionSpeechRecognitionHost::~LiveCaptionSpeechRecognitionHost() {
   CaptionController* caption_controller = GetCaptionController();
   if (caption_controller)
     caption_controller->OnAudioStreamEnd(this);
 }
 
-void CaptionHostImpl::OnSpeechRecognitionRecognitionEvent(
+void LiveCaptionSpeechRecognitionHost::OnSpeechRecognitionRecognitionEvent(
     media::mojom::SpeechRecognitionResultPtr result,
     OnSpeechRecognitionRecognitionEventCallback reply) {
   CaptionController* caption_controller = GetCaptionController();
@@ -50,7 +52,7 @@
   std::move(reply).Run(caption_controller->DispatchTranscription(this, result));
 }
 
-void CaptionHostImpl::OnLanguageIdentificationEvent(
+void LiveCaptionSpeechRecognitionHost::OnLanguageIdentificationEvent(
     media::mojom::LanguageIdentificationEventPtr event) {
   CaptionController* caption_controller = GetCaptionController();
   if (!caption_controller)
@@ -59,18 +61,19 @@
   caption_controller->OnLanguageIdentificationEvent(std::move(event));
 }
 
-void CaptionHostImpl::OnSpeechRecognitionError() {
+void LiveCaptionSpeechRecognitionHost::OnSpeechRecognitionError() {
   CaptionController* caption_controller = GetCaptionController();
   if (caption_controller)
     caption_controller->OnError(this);
 }
 
-void CaptionHostImpl::RenderFrameDeleted(content::RenderFrameHost* frame_host) {
+void LiveCaptionSpeechRecognitionHost::RenderFrameDeleted(
+    content::RenderFrameHost* frame_host) {
   if (frame_host == frame_host_)
     frame_host_ = nullptr;
 }
 
-content::WebContents* CaptionHostImpl::GetWebContents() {
+content::WebContents* LiveCaptionSpeechRecognitionHost::GetWebContents() {
   if (!frame_host_)
     return nullptr;
   content::WebContents* web_contents =
@@ -80,7 +83,7 @@
   return web_contents;
 }
 
-CaptionController* CaptionHostImpl::GetCaptionController() {
+CaptionController* LiveCaptionSpeechRecognitionHost::GetCaptionController() {
   content::WebContents* web_contents = GetWebContents();
   if (!web_contents)
     return nullptr;
diff --git a/chrome/browser/accessibility/caption_host_impl.h b/chrome/browser/accessibility/live_caption_speech_recognition_host.h
similarity index 65%
rename from chrome/browser/accessibility/caption_host_impl.h
rename to chrome/browser/accessibility/live_caption_speech_recognition_host.h
index a105a34..dcc3e06 100644
--- a/chrome/browser/accessibility/caption_host_impl.h
+++ b/chrome/browser/accessibility/live_caption_speech_recognition_host.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 CHROME_BROWSER_ACCESSIBILITY_CAPTION_HOST_IMPL_H_
-#define CHROME_BROWSER_ACCESSIBILITY_CAPTION_HOST_IMPL_H_
+#ifndef CHROME_BROWSER_ACCESSIBILITY_LIVE_CAPTION_SPEECH_RECOGNITION_HOST_H_
+#define CHROME_BROWSER_ACCESSIBILITY_LIVE_CAPTION_SPEECH_RECOGNITION_HOST_H_
 
 #include "content/public/browser/web_contents_observer.h"
 #include "media/mojo/mojom/speech_recognition_service.mojom.h"
@@ -18,19 +18,23 @@
 class CaptionController;
 
 ///////////////////////////////////////////////////////////////////////////////
-// Caption Host Impl
+//  Live Caption Speech Recognition Host
 //
 //  A class that implements the Mojo interface
-//  SpeechRecognitionRecognizerClient. There exists one CaptionHostImpl per
-//  render frame.
+//  SpeechRecognitionRecognizerClient. There exists one
+//  LiveCaptionSpeechRecognitionHost per render frame.
 //
-class CaptionHostImpl : public media::mojom::SpeechRecognitionRecognizerClient,
-                        public content::WebContentsObserver {
+class LiveCaptionSpeechRecognitionHost
+    : public media::mojom::SpeechRecognitionRecognizerClient,
+      public content::WebContentsObserver {
  public:
-  explicit CaptionHostImpl(content::RenderFrameHost* frame_host);
-  CaptionHostImpl(const CaptionHostImpl&) = delete;
-  CaptionHostImpl& operator=(const CaptionHostImpl&) = delete;
-  ~CaptionHostImpl() override;
+  explicit LiveCaptionSpeechRecognitionHost(
+      content::RenderFrameHost* frame_host);
+  LiveCaptionSpeechRecognitionHost(const LiveCaptionSpeechRecognitionHost&) =
+      delete;
+  LiveCaptionSpeechRecognitionHost& operator=(
+      const LiveCaptionSpeechRecognitionHost&) = delete;
+  ~LiveCaptionSpeechRecognitionHost() override;
 
   // static
   static void Create(
@@ -64,4 +68,4 @@
 
 }  // namespace captions
 
-#endif  // CHROME_BROWSER_ACCESSIBILITY_CAPTION_HOST_IMPL_H_
+#endif  // CHROME_BROWSER_ACCESSIBILITY_LIVE_CAPTION_SPEECH_RECOGNITION_HOST_H_
diff --git a/chrome/browser/accessibility/caption_host_impl_browsertest.cc b/chrome/browser/accessibility/live_caption_speech_recognition_host_browsertest.cc
similarity index 78%
rename from chrome/browser/accessibility/caption_host_impl_browsertest.cc
rename to chrome/browser/accessibility/live_caption_speech_recognition_host_browsertest.cc
index 293ce54..029deeb 100644
--- a/chrome/browser/accessibility/caption_host_impl_browsertest.cc
+++ b/chrome/browser/accessibility/live_caption_speech_recognition_host_browsertest.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/accessibility/caption_host_impl.h"
+#include "chrome/browser/accessibility/live_caption_speech_recognition_host.h"
 
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
@@ -36,12 +36,14 @@
 
 namespace captions {
 
-class CaptionHostImplTest : public InProcessBrowserTest {
+class LiveCaptionSpeechRecognitionHostTest : public InProcessBrowserTest {
  public:
-  CaptionHostImplTest() = default;
-  ~CaptionHostImplTest() override = default;
-  CaptionHostImplTest(const CaptionHostImplTest&) = delete;
-  CaptionHostImplTest& operator=(const CaptionHostImplTest&) = delete;
+  LiveCaptionSpeechRecognitionHostTest() = default;
+  ~LiveCaptionSpeechRecognitionHostTest() override = default;
+  LiveCaptionSpeechRecognitionHostTest(
+      const LiveCaptionSpeechRecognitionHostTest&) = delete;
+  LiveCaptionSpeechRecognitionHostTest& operator=(
+      const LiveCaptionSpeechRecognitionHostTest&) = delete;
 
   // InProcessBrowserTest overrides:
   void SetUp() override {
@@ -49,12 +51,13 @@
     InProcessBrowserTest::SetUp();
   }
 
-  void CreateCaptionHostImpl(content::RenderFrameHost* frame_host) {
+  void CreateLiveCaptionSpeechRecognitionHost(
+      content::RenderFrameHost* frame_host) {
     mojo::Remote<media::mojom::SpeechRecognitionRecognizerClient> remote;
     mojo::PendingReceiver<media::mojom::SpeechRecognitionRecognizerClient>
         receiver;
     remote.Bind(receiver.InitWithNewPipeAndPassRemote());
-    CaptionHostImpl::Create(frame_host, std::move(receiver));
+    LiveCaptionSpeechRecognitionHost::Create(frame_host, std::move(receiver));
     remotes_.emplace(frame_host, std::move(remote));
   }
 
@@ -63,7 +66,8 @@
                                            bool expected_success) {
     remotes_[frame_host]->OnSpeechRecognitionRecognitionEvent(
         media::mojom::SpeechRecognitionResult::New(text, /*final=*/true),
-        base::BindOnce(&CaptionHostImplTest::DispatchTranscriptionCallback,
+        base::BindOnce(&LiveCaptionSpeechRecognitionHostTest::
+                           DispatchTranscriptionCallback,
                        base::Unretained(this), expected_success));
   }
 
@@ -98,10 +102,11 @@
       remotes_;
 };
 
-IN_PROC_BROWSER_TEST_F(CaptionHostImplTest, DestroysWithoutCrashing) {
+IN_PROC_BROWSER_TEST_F(LiveCaptionSpeechRecognitionHostTest,
+                       DestroysWithoutCrashing) {
   content::RenderFrameHost* frame_host =
       browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();
-  CreateCaptionHostImpl(frame_host);
+  CreateLiveCaptionSpeechRecognitionHost(frame_host);
 
   SetLiveCaptionEnabled(true);
   OnSpeechRecognitionRecognitionEvent(
@@ -120,11 +125,11 @@
   base::RunLoop().RunUntilIdle();
 }
 
-IN_PROC_BROWSER_TEST_F(CaptionHostImplTest,
+IN_PROC_BROWSER_TEST_F(LiveCaptionSpeechRecognitionHostTest,
                        OnSpeechRecognitionRecognitionEvent) {
   content::RenderFrameHost* frame_host =
       browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();
-  CreateCaptionHostImpl(frame_host);
+  CreateLiveCaptionSpeechRecognitionHost(frame_host);
 
   SetLiveCaptionEnabled(true);
   OnSpeechRecognitionRecognitionEvent(frame_host,
@@ -140,20 +145,22 @@
   base::RunLoop().RunUntilIdle();
 }
 
-IN_PROC_BROWSER_TEST_F(CaptionHostImplTest, OnLanguageIdentificationEvent) {
+IN_PROC_BROWSER_TEST_F(LiveCaptionSpeechRecognitionHostTest,
+                       OnLanguageIdentificationEvent) {
   content::RenderFrameHost* frame_host =
       browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();
-  CreateCaptionHostImpl(frame_host);
+  CreateLiveCaptionSpeechRecognitionHost(frame_host);
 
   SetLiveCaptionEnabled(true);
   OnLanguageIdentificationEvent(
       frame_host, "en-US", media::mojom::ConfidenceLevel::kHighlyConfident);
 }
 
-IN_PROC_BROWSER_TEST_F(CaptionHostImplTest, OnSpeechRecognitionError) {
+IN_PROC_BROWSER_TEST_F(LiveCaptionSpeechRecognitionHostTest,
+                       OnSpeechRecognitionError) {
   content::RenderFrameHost* frame_host =
       browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();
-  CreateCaptionHostImpl(frame_host);
+  CreateLiveCaptionSpeechRecognitionHost(frame_host);
 
   SetLiveCaptionEnabled(true);
   OnSpeechRecognitionError(frame_host);
diff --git a/chrome/browser/apps/app_service/app_icon_factory.cc b/chrome/browser/apps/app_service/app_icon_factory.cc
index 08d3f21d..9ece50f 100644
--- a/chrome/browser/apps/app_service/app_icon_factory.cc
+++ b/chrome/browser/apps/app_service/app_icon_factory.cc
@@ -333,6 +333,50 @@
   return absl::nullopt;
 }
 
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+apps::mojom::IconValuePtr ApplyEffects(apps::IconEffects icon_effects,
+                                       int size_hint_in_dip,
+                                       apps::mojom::IconValuePtr iv,
+                                       gfx::ImageSkia mask_image) {
+  base::AssertLongCPUWorkAllowed();
+
+  extensions::ChromeAppIcon::ResizeFunction resize_function;
+  if (icon_effects & apps::IconEffects::kResizeAndPad) {
+    // TODO(crbug.com/826982): MD post-processing is not always applied: "See
+    // legacy code:
+    // https://cs.chromium.org/search/?q=ChromeAppIconLoader&type=cs In one
+    // cases MD design is used in another not."
+    resize_function =
+        base::BindRepeating(&app_list::MaybeResizeAndPadIconForMd);
+  }
+
+  if ((icon_effects & apps::IconEffects::kCrOsStandardMask) &&
+      !mask_image.isNull()) {
+    if (icon_effects & apps::IconEffects::kCrOsStandardBackground) {
+      iv->uncompressed = gfx::ImageSkiaOperations::CreateButtonBackground(
+          SK_ColorWHITE, iv->uncompressed, mask_image);
+    } else {
+      iv->uncompressed = gfx::ImageSkiaOperations::CreateMaskedImage(
+          iv->uncompressed, mask_image);
+    }
+  }
+
+  if (icon_effects & apps::IconEffects::kCrOsStandardIcon) {
+    iv->uncompressed = apps::CreateStandardIconImage(iv->uncompressed);
+  }
+
+  if (!resize_function.is_null()) {
+    resize_function.Run(gfx::Size(size_hint_in_dip, size_hint_in_dip),
+                        &iv->uncompressed);
+  }
+
+  if (!iv->uncompressed.isNull())
+    iv->uncompressed.MakeThreadSafe();
+
+  return iv;
+}
+#endif
+
 // This pipeline is meant to:
 // * Simplify loading icons, as things like effects and type are common
 //   to all loading.
@@ -393,6 +437,16 @@
           callback)
       : arc_activity_icons_callback_(std::move(callback)) {}
 
+  IconLoadingPipeline(int size_hint_in_dip,
+                      apps::mojom::Publisher::LoadIconCallback callback)
+      : size_hint_in_dip_(size_hint_in_dip), callback_(std::move(callback)) {}
+
+  void ApplyIconEffects(apps::IconEffects icon_effects,
+                        apps::mojom::IconValuePtr iv);
+
+  void ApplyBadges(apps::IconEffects icon_effects,
+                   apps::mojom::IconValuePtr iv);
+
   void LoadWebAppIcon(const std::string& web_app_id,
                       const GURL& launch_url,
                       const web_app::AppIconManager& icon_manager,
@@ -456,7 +510,9 @@
 
   void CompleteWithCompressed(std::vector<uint8_t> data);
 
-  void CompleteWithImageSkia(gfx::ImageSkia image);
+  void CompleteWithUncompressed(apps::mojom::IconValuePtr iv);
+
+  void CompleteWithIconValue(apps::mojom::IconValuePtr iv);
 
   void OnReadWebAppIcon(std::map<int, SkBitmap> icon_bitmaps);
 
@@ -495,6 +551,10 @@
 
   base::CancelableTaskTracker cancelable_task_tracker_;
 
+  // A sequenced task runner to create standard icons and not spamming the
+  // thread pool.
+  scoped_refptr<base::SequencedTaskRunner> standard_icon_task_runner_;
+
   gfx::ImageSkia foreground_image_;
   gfx::ImageSkia background_image_;
   bool foreground_is_set_ = false;
@@ -513,6 +573,65 @@
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 };
 
+void IconLoadingPipeline::ApplyIconEffects(apps::IconEffects icon_effects,
+                                           apps::mojom::IconValuePtr iv) {
+  if (!iv || iv->uncompressed.isNull())
+    return;
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+  if (!standard_icon_task_runner_) {
+    standard_icon_task_runner_ = base::ThreadPool::CreateSequencedTaskRunner(
+        {base::TaskPriority::USER_VISIBLE,
+         base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN});
+  }
+
+  gfx::ImageSkia mask_image;
+  if (icon_effects & apps::IconEffects::kCrOsStandardMask) {
+    mask_image = apps::LoadMaskImage(GetScaleToSize(iv->uncompressed));
+    mask_image.MakeThreadSafe();
+  }
+
+  iv->uncompressed.MakeThreadSafe();
+
+  base::PostTaskAndReplyWithResult(
+      standard_icon_task_runner_.get(), FROM_HERE,
+      base::BindOnce(&ApplyEffects, icon_effects, size_hint_in_dip_,
+                     std::move(iv), mask_image),
+      base::BindOnce(&IconLoadingPipeline::ApplyBadges,
+                     base::WrapRefCounted(this), icon_effects));
+#else
+  ApplyBadges(icon_effects, std::move(iv));
+#endif
+}
+
+void IconLoadingPipeline::ApplyBadges(apps::IconEffects icon_effects,
+                                      apps::mojom::IconValuePtr iv) {
+  const bool from_bookmark = icon_effects & apps::IconEffects::kRoundCorners;
+
+  bool app_launchable = true;
+  // Only one badge can be visible at a time.
+  // Priority in which badges are applied (from the highest): Blocked > Paused >
+  // Chrome. This means than when apps are disabled or paused app type
+  // distinction information (Chrome vs Android) is lost.
+  extensions::ChromeAppIcon::Badge badge_type =
+      extensions::ChromeAppIcon::Badge::kNone;
+  if (icon_effects & apps::IconEffects::kBlocked) {
+    badge_type = extensions::ChromeAppIcon::Badge::kBlocked;
+    app_launchable = false;
+  } else if (icon_effects & apps::IconEffects::kPaused) {
+    badge_type = extensions::ChromeAppIcon::Badge::kPaused;
+    app_launchable = false;
+  } else if (icon_effects & apps::IconEffects::kChromeBadge) {
+    badge_type = extensions::ChromeAppIcon::Badge::kChrome;
+  }
+
+  extensions::ChromeAppIcon::ApplyEffects(
+      size_hint_in_dip_, extensions::ChromeAppIcon::ResizeFunction(),
+      app_launchable, from_bookmark, badge_type, &iv->uncompressed);
+
+  std::move(callback_).Run(std::move(iv));
+}
+
 void IconLoadingPipeline::LoadWebAppIcon(
     const std::string& web_app_id,
     const GURL& launch_url,
@@ -885,28 +1004,24 @@
     MaybeLoadFallbackOrCompleteEmpty();
     return;
   }
-  gfx::ImageSkia processed_image = image;
+
+  apps::mojom::IconValuePtr iv = apps::mojom::IconValue::New();
+  iv->icon_type = icon_type_;
+  iv->uncompressed = image;
+  iv->is_placeholder_icon = is_placeholder_icon_;
 
   // Apply the icon effects on the uncompressed data. If the caller requests
   // an uncompressed icon, return the uncompressed result; otherwise, encode
   // the icon to a compressed icon, return the compressed result.
   if (icon_effects_) {
-    apps::ApplyIconEffects(icon_effects_, size_hint_in_dip_, &processed_image);
-  }
-
-  if (icon_type_ == apps::mojom::IconType::kUncompressed ||
-      icon_type_ == apps::mojom::IconType::kStandard) {
-    CompleteWithImageSkia(processed_image);
+    apps::ApplyIconEffects(
+        icon_effects_, size_hint_in_dip_, std::move(iv),
+        base::BindOnce(&IconLoadingPipeline::CompleteWithIconValue,
+                       base::WrapRefCounted(this)));
     return;
   }
 
-  processed_image.MakeThreadSafe();
-  base::ThreadPool::PostTaskAndReplyWithResult(
-      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
-      base::BindOnce(&apps::EncodeImageToPngBytes, processed_image,
-                     icon_scale_for_compressed_response_),
-      base::BindOnce(&IconLoadingPipeline::CompleteWithCompressed,
-                     base::WrapRefCounted(this)));
+  CompleteWithIconValue(std::move(iv));
 }
 
 void IconLoadingPipeline::CompleteWithCompressed(std::vector<uint8_t> data) {
@@ -922,20 +1037,33 @@
   std::move(callback_).Run(std::move(iv));
 }
 
-void IconLoadingPipeline::CompleteWithImageSkia(gfx::ImageSkia image) {
+void IconLoadingPipeline::CompleteWithUncompressed(
+    apps::mojom::IconValuePtr iv) {
   DCHECK_NE(icon_type_, apps::mojom::IconType::kCompressed);
   DCHECK_NE(icon_type_, apps::mojom::IconType::kUnknown);
-  if (image.isNull()) {
+  if (iv->uncompressed.isNull()) {
     MaybeLoadFallbackOrCompleteEmpty();
     return;
   }
-  apps::mojom::IconValuePtr iv = apps::mojom::IconValue::New();
-  iv->icon_type = icon_type_;
-  iv->uncompressed = std::move(image);
-  iv->is_placeholder_icon = is_placeholder_icon_;
   std::move(callback_).Run(std::move(iv));
 }
 
+void IconLoadingPipeline::CompleteWithIconValue(apps::mojom::IconValuePtr iv) {
+  if (icon_type_ == apps::mojom::IconType::kUncompressed ||
+      icon_type_ == apps::mojom::IconType::kStandard) {
+    CompleteWithUncompressed(std::move(iv));
+    return;
+  }
+
+  iv->uncompressed.MakeThreadSafe();
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
+      base::BindOnce(&apps::EncodeImageToPngBytes, iv->uncompressed,
+                     icon_scale_for_compressed_response_),
+      base::BindOnce(&IconLoadingPipeline::CompleteWithCompressed,
+                     base::WrapRefCounted(this)));
+}
+
 // Callback for reading uncompressed web app icons.
 void IconLoadingPipeline::OnReadWebAppIcon(
     std::map<int, SkBitmap> icon_bitmaps) {
@@ -1248,8 +1376,10 @@
   }
 
   image_skia.EnsureRepsForSupportedScales();
-  ApplyIconEffects(icon_effects, size_hint_in_dip, &image_skia);
-
+  if ((icon_effects & IconEffects::kCrOsStandardMask) &&
+      (icon_effects & IconEffects::kCrOsStandardBackground)) {
+    image_skia = apps::ApplyBackgroundAndMask(image_skia);
+  }
   return image_skia;
 }
 
@@ -1257,55 +1387,12 @@
 
 void ApplyIconEffects(IconEffects icon_effects,
                       int size_hint_in_dip,
-                      gfx::ImageSkia* image_skia) {
-  extensions::ChromeAppIcon::ResizeFunction resize_function;
-#if BUILDFLAG(IS_CHROMEOS_ASH)
-  if (icon_effects & IconEffects::kResizeAndPad) {
-    // TODO(crbug.com/826982): MD post-processing is not always applied: "See
-    // legacy code:
-    // https://cs.chromium.org/search/?q=ChromeAppIconLoader&type=cs In one
-    // cases MD design is used in another not."
-    resize_function =
-        base::BindRepeating(&app_list::MaybeResizeAndPadIconForMd);
-  }
-
-  if (icon_effects & IconEffects::kCrOsStandardMask) {
-    if (icon_effects & IconEffects::kCrOsStandardBackground) {
-      *image_skia = apps::ApplyBackgroundAndMask(*image_skia);
-    } else {
-      auto mask_image = LoadMaskImage(GetScaleToSize(*image_skia));
-      *image_skia =
-          gfx::ImageSkiaOperations::CreateMaskedImage(*image_skia, mask_image);
-    }
-  }
-
-  if (icon_effects & IconEffects::kCrOsStandardIcon) {
-    *image_skia = apps::CreateStandardIconImage(*image_skia);
-  }
-#endif
-
-  const bool from_bookmark = icon_effects & IconEffects::kRoundCorners;
-
-  bool app_launchable = true;
-  // Only one badge can be visible at a time.
-  // Priority in which badges are applied (from the highest): Blocked > Paused >
-  // Chrome. This means than when apps are disabled or paused app type
-  // distinction information (Chrome vs Android) is lost.
-  extensions::ChromeAppIcon::Badge badge_type =
-      extensions::ChromeAppIcon::Badge::kNone;
-  if (icon_effects & IconEffects::kBlocked) {
-    badge_type = extensions::ChromeAppIcon::Badge::kBlocked;
-    app_launchable = false;
-  } else if (icon_effects & IconEffects::kPaused) {
-    badge_type = extensions::ChromeAppIcon::Badge::kPaused;
-    app_launchable = false;
-  } else if (icon_effects & IconEffects::kChromeBadge) {
-    badge_type = extensions::ChromeAppIcon::Badge::kChrome;
-  }
-
-  extensions::ChromeAppIcon::ApplyEffects(size_hint_in_dip, resize_function,
-                                          app_launchable, from_bookmark,
-                                          badge_type, image_skia);
+                      apps::mojom::IconValuePtr iv,
+                      apps::mojom::Publisher::LoadIconCallback callback) {
+  scoped_refptr<IconLoadingPipeline> icon_loader =
+      base::MakeRefCounted<IconLoadingPipeline>(size_hint_in_dip,
+                                                std::move(callback));
+  icon_loader->ApplyIconEffects(icon_effects, std::move(iv));
 }
 
 void LoadIconFromExtension(apps::mojom::IconType icon_type,
diff --git a/chrome/browser/apps/app_service/app_icon_factory.h b/chrome/browser/apps/app_service/app_icon_factory.h
index 45f7487..8e6c385 100644
--- a/chrome/browser/apps/app_service/app_icon_factory.h
+++ b/chrome/browser/apps/app_service/app_icon_factory.h
@@ -119,11 +119,12 @@
     int size_hint_in_dip);
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
-// Modifies |image_skia| to apply icon post-processing effects like badging and
+// Modifies |iv| to apply icon post-processing effects like badging and
 // desaturation to gray.
 void ApplyIconEffects(IconEffects icon_effects,
                       int size_hint_in_dip,
-                      gfx::ImageSkia* image_skia);
+                      apps::mojom::IconValuePtr iv,
+                      apps::mojom::Publisher::LoadIconCallback callback);
 
 // Loads an icon from an extension.
 void LoadIconFromExtension(apps::mojom::IconType icon_type,
diff --git a/chrome/browser/apps/app_service/app_icon_factory_unittest.cc b/chrome/browser/apps/app_service/app_icon_factory_unittest.cc
index a1c4bc4..441aba2 100644
--- a/chrome/browser/apps/app_service/app_icon_factory_unittest.cc
+++ b/chrome/browser/apps/app_service/app_icon_factory_unittest.cc
@@ -1111,9 +1111,13 @@
   }
 
   gfx::ImageSkia converted_image = ConvertSquareBitmapsToImageSkia(
-      icon_bitmaps, /*icon_effects=*/apps::IconEffects::kCrOsStandardIcon,
+      icon_bitmaps,
+      /*icon_effects=*/apps::IconEffects::kCrOsStandardBackground |
+          apps::IconEffects::kCrOsStandardMask,
       /*size_hint_in_dip=*/32);
 
+  EnsureRepresentationsLoaded(converted_image);
+
   const std::vector<ui::ScaleFactor>& scale_factors =
       ui::GetSupportedScaleFactors();
   ASSERT_EQ(2U, scale_factors.size());
@@ -1122,14 +1126,14 @@
     const float scale = ui::GetScaleForScaleFactor(scale_factors[i]);
     ASSERT_TRUE(converted_image.HasRepresentation(scale));
 
-    // No colour in the upper left corner.
+    // No color in the upper left corner.
     EXPECT_FALSE(
         converted_image.GetRepresentation(scale).GetBitmap().getColor(0, 0));
 
+    // Has color in the center.
     const SquareSizePx center_px = sizes_px[i] / 2;
-    EXPECT_EQ(colors[i],
-              converted_image.GetRepresentation(scale).GetBitmap().getColor(
-                  center_px, center_px));
+    EXPECT_TRUE(converted_image.GetRepresentation(scale).GetBitmap().getColor(
+        center_px, center_px));
   }
 }
 
diff --git a/chrome/browser/apps/app_service/publishers/arc_apps.cc b/chrome/browser/apps/app_service/publishers/arc_apps.cc
index 42d837f..bcb2d1e 100644
--- a/chrome/browser/apps/app_service/publishers/arc_apps.cc
+++ b/chrome/browser/apps/app_service/publishers/arc_apps.cc
@@ -78,6 +78,21 @@
   std::move(callback).Run(std::move(iv));
 }
 
+void UpdateIconImage(apps::mojom::Publisher::LoadIconCallback callback,
+                     apps::mojom::IconValuePtr iv) {
+  if (base::FeatureList::IsEnabled(features::kAppServiceAdaptiveIcon) &&
+      iv->icon_type == apps::mojom::IconType::kCompressed) {
+    iv->uncompressed.MakeThreadSafe();
+    base::ThreadPool::PostTaskAndReplyWithResult(
+        FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
+        base::BindOnce(&apps::EncodeImageToPngBytes, iv->uncompressed,
+                       /*rep_icon_scale=*/1.0f),
+        base::BindOnce(&CompleteWithCompressed, std::move(callback)));
+    return;
+  }
+  std::move(callback).Run(std::move(iv));
+}
+
 void OnArcAppIconCompletelyLoaded(
     apps::mojom::IconType icon_type,
     int32_t size_hint_in_dip,
@@ -126,8 +141,10 @@
         iv->uncompressed = icon->image_skia();
       }
       if (icon_effects != apps::IconEffects::kNone) {
-        apps::ApplyIconEffects(icon_effects, size_hint_in_dip,
-                               &iv->uncompressed);
+        apps::ApplyIconEffects(
+            icon_effects, size_hint_in_dip, std::move(iv),
+            base::BindOnce(&UpdateIconImage, std::move(callback)));
+        return;
       }
       break;
     }
@@ -136,17 +153,7 @@
       break;
   }
 
-  if (base::FeatureList::IsEnabled(features::kAppServiceAdaptiveIcon) &&
-      icon_type == apps::mojom::IconType::kCompressed) {
-    iv->uncompressed.MakeThreadSafe();
-    base::ThreadPool::PostTaskAndReplyWithResult(
-        FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
-        base::BindOnce(&apps::EncodeImageToPngBytes, iv->uncompressed,
-                       /*rep_icon_scale=*/1.0f),
-        base::BindOnce(&CompleteWithCompressed, std::move(callback)));
-    return;
-  }
-  std::move(callback).Run(std::move(iv));
+  UpdateIconImage(std::move(callback), std::move(iv));
 }
 
 void UpdateAppPermissions(
diff --git a/chrome/browser/apps/app_service/publishers/remote_apps.cc b/chrome/browser/apps/app_service/publishers/remote_apps.cc
index 3a2060b5..ff8df41 100644
--- a/chrome/browser/apps/app_service/publishers/remote_apps.cc
+++ b/chrome/browser/apps/app_service/publishers/remote_apps.cc
@@ -96,17 +96,19 @@
     icon_image = delegate_->GetPlaceholderIcon(app_id, size_hint_in_dip);
   }
 
-  if (!icon_image.isNull()) {
-    icon->icon_type = icon_type;
-    icon->uncompressed = icon_image;
-    icon->is_placeholder_icon = is_placeholder_icon;
-    IconEffects icon_effects = (icon_type == mojom::IconType::kStandard)
-                                   ? IconEffects::kCrOsStandardIcon
-                                   : IconEffects::kResizeAndPad;
-    apps::ApplyIconEffects(icon_effects, size_hint_in_dip, &icon->uncompressed);
+  if (icon_image.isNull()) {
+    std::move(callback).Run(std::move(icon));
+    return;
   }
 
-  std::move(callback).Run(std::move(icon));
+  icon->icon_type = icon_type;
+  icon->uncompressed = icon_image;
+  icon->is_placeholder_icon = is_placeholder_icon;
+  IconEffects icon_effects = (icon_type == mojom::IconType::kStandard)
+                                 ? IconEffects::kCrOsStandardIcon
+                                 : IconEffects::kResizeAndPad;
+  apps::ApplyIconEffects(icon_effects, size_hint_in_dip, std::move(icon),
+                         std::move(callback));
 }
 
 void RemoteApps::Launch(const std::string& app_id,
diff --git a/chrome/browser/apps/app_shim/web_app_shim_manager_delegate_mac.cc b/chrome/browser/apps/app_shim/web_app_shim_manager_delegate_mac.cc
index 2f27bda8..fe0444e 100644
--- a/chrome/browser/apps/app_shim/web_app_shim_manager_delegate_mac.cc
+++ b/chrome/browser/apps/app_shim/web_app_shim_manager_delegate_mac.cc
@@ -9,6 +9,7 @@
 #include "chrome/browser/apps/app_service/app_service_proxy.h"
 #include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
 #include "chrome/browser/apps/app_service/browser_app_launcher.h"
+#include "chrome/browser/ui/browser_dialogs.h"
 #include "chrome/browser/web_applications/components/os_integration_manager.h"
 #include "chrome/browser/web_applications/components/web_app_shortcut_mac.h"
 #include "chrome/browser/web_applications/web_app_provider.h"
@@ -172,9 +173,29 @@
     return;
   }
 
-  apps::AppServiceProxyFactory::GetForProfile(profile)
-      ->BrowserAppLauncher()
-      ->LaunchAppWithParams(std::move(params));
+  if (params.protocol_handler_launch_url.has_value()) {
+    GURL protocol_url = params.protocol_handler_launch_url.value();
+
+    auto launch_callback = base::BindOnce(
+        [](apps::AppLaunchParams params, Profile* profile, bool accepted) {
+          if (accepted) {
+            apps::AppServiceProxyFactory::GetForProfile(profile)
+                ->BrowserAppLauncher()
+                ->LaunchAppWithParams(std::move(params));
+          }
+        },
+        std::move(params), profile);
+
+    // ShowWebAppProtocolHandlerIntentPicker keeps the `profile` alive through
+    // running of `launch_callback`.
+    chrome::ShowWebAppProtocolHandlerIntentPicker(
+        std::move(protocol_url), profile, app_id, std::move(launch_callback));
+
+  } else {
+    apps::AppServiceProxyFactory::GetForProfile(profile)
+        ->BrowserAppLauncher()
+        ->LaunchAppWithParams(std::move(params));
+  }
 }
 
 void WebAppShimManagerDelegate::LaunchShim(
diff --git a/chrome/browser/ash/accessibility/accessibility_event_rewriter_delegate_impl.cc b/chrome/browser/ash/accessibility/accessibility_event_rewriter_delegate_impl.cc
index 9a5c2a28..9ae149c 100644
--- a/chrome/browser/ash/accessibility/accessibility_event_rewriter_delegate_impl.cc
+++ b/chrome/browser/ash/accessibility/accessibility_event_rewriter_delegate_impl.cc
@@ -99,8 +99,8 @@
   extensions::EventRouter* event_router =
       extensions::EventRouter::Get(AccessibilityManager::Get()->profile());
 
-  auto event_args = std::make_unique<base::ListValue>();
-  event_args->AppendString(ToString(command));
+  std::vector<base::Value> event_args;
+  event_args.push_back(base::Value(ToString(command)));
 
   auto event = std::make_unique<extensions::Event>(
       extensions::events::ACCESSIBILITY_PRIVATE_ON_SWITCH_ACCESS_COMMAND,
@@ -116,13 +116,12 @@
   extensions::EventRouter* event_router =
       extensions::EventRouter::Get(AccessibilityManager::Get()->profile());
 
-  auto event_args = std::make_unique<base::ListValue>();
-  auto point_dict = std::make_unique<base::DictionaryValue>();
+  base::Value point_dict(base::Value::Type::DICTIONARY);
+  point_dict.SetDoubleKey("x", point.x());
+  point_dict.SetDoubleKey("y", point.y());
 
-  point_dict->SetDouble("x", point.x());
-  point_dict->SetDouble("y", point.y());
-
-  event_args->Append(std::move(point_dict));
+  std::vector<base::Value> event_args;
+  event_args.push_back(std::move(point_dict));
 
   auto event = std::make_unique<extensions::Event>(
       extensions::events::ACCESSIBILITY_PRIVATE_ON_POINT_SCAN_SET,
@@ -138,8 +137,8 @@
   extensions::EventRouter* event_router =
       extensions::EventRouter::Get(AccessibilityManager::Get()->profile());
 
-  auto event_args = std::make_unique<base::ListValue>();
-  event_args->AppendString(ToString(command));
+  std::vector<base::Value> event_args;
+  event_args.push_back(base::Value(ToString(command)));
 
   auto event = std::make_unique<extensions::Event>(
       extensions::events::ACCESSIBILITY_PRIVATE_ON_SWITCH_ACCESS_COMMAND,
diff --git a/chrome/browser/ash/accessibility/accessibility_manager.cc b/chrome/browser/ash/accessibility/accessibility_manager.cc
index 360efc49..00e0eee 100644
--- a/chrome/browser/ash/accessibility/accessibility_manager.cc
+++ b/chrome/browser/ash/accessibility/accessibility_manager.cc
@@ -575,11 +575,10 @@
   extensions::EventRouter* event_router =
       extensions::EventRouter::Get(profile_);
 
-  auto event_args = std::make_unique<base::ListValue>();
   auto event = std::make_unique<extensions::Event>(
       extensions::events::ACCESSIBILITY_PRIVATE_ON_TWO_FINGER_TOUCH_START,
       extensions::api::accessibility_private::OnTwoFingerTouchStart::kEventName,
-      std::move(event_args));
+      std::vector<base::Value>());
   event_router->BroadcastEvent(std::move(event));
 }
 
@@ -590,11 +589,10 @@
   extensions::EventRouter* event_router =
       extensions::EventRouter::Get(profile_);
 
-  auto event_args = std::make_unique<base::ListValue>();
   auto event = std::make_unique<extensions::Event>(
       extensions::events::ACCESSIBILITY_PRIVATE_ON_TWO_FINGER_TOUCH_STOP,
       extensions::api::accessibility_private::OnTwoFingerTouchStop::kEventName,
-      std::move(event_args));
+      std::vector<base::Value>());
   event_router->BroadcastEvent(std::move(event));
 }
 
@@ -615,11 +613,10 @@
   extensions::EventRouter* event_router =
       extensions::EventRouter::Get(profile_);
 
-  std::unique_ptr<base::ListValue> event_args =
-      std::make_unique<base::ListValue>();
-  event_args->AppendString(ui::ToString(gesture));
-  event_args->AppendInteger(location.x());
-  event_args->AppendInteger(location.y());
+  std::vector<base::Value> event_args;
+  event_args.push_back(base::Value(ui::ToString(gesture)));
+  event_args.push_back(base::Value(location.x()));
+  event_args.push_back(base::Value(location.y()));
   std::unique_ptr<extensions::Event> event(new extensions::Event(
       extensions::events::ACCESSIBILITY_PRIVATE_ON_ACCESSIBILITY_GESTURE,
       extensions::api::accessibility_private::OnAccessibilityGesture::
@@ -912,14 +909,12 @@
       extensions::EventRouter::Get(profile_);
 
   // Send an event to the Select-to-Speak extension requesting a state change.
-  std::unique_ptr<base::ListValue> event_args =
-      std::make_unique<base::ListValue>();
   std::unique_ptr<extensions::Event> event(new extensions::Event(
       extensions::events::
           ACCESSIBILITY_PRIVATE_ON_SELECT_TO_SPEAK_STATE_CHANGE_REQUESTED,
       extensions::api::accessibility_private::
           OnSelectToSpeakStateChangeRequested::kEventName,
-      std::move(event_args)));
+      std::vector<base::Value>()));
   event_router->DispatchEventWithLazyListener(
       extension_misc::kSelectToSpeakExtensionId, std::move(event));
 }
@@ -1464,12 +1459,10 @@
 
   const std::string& extension_id = extension_misc::kChromeVoxExtensionId;
 
-  std::unique_ptr<base::ListValue> event_args =
-      std::make_unique<base::ListValue>();
   std::unique_ptr<extensions::Event> event(new extensions::Event(
       extensions::events::ACCESSIBILITY_PRIVATE_ON_INTRODUCE_CHROME_VOX,
       extensions::api::accessibility_private::OnIntroduceChromeVox::kEventName,
-      std::move(event_args)));
+      std::vector<base::Value>()));
   event_router->DispatchEventWithLazyListener(extension_id, std::move(event));
 
   if (!chromevox_panel_) {
@@ -1769,10 +1762,10 @@
   extensions::EventRouter* event_router =
       extensions::EventRouter::Get(profile_);
 
-  auto event_args = std::make_unique<base::ListValue>();
-  event_args->AppendString(AccessibilityPrivateEnumForAction(action));
+  std::vector<base::Value> event_args;
+  event_args.push_back(base::Value(AccessibilityPrivateEnumForAction(action)));
   if (value != 0.0) {
-    event_args->Append(value);
+    event_args.push_back(base::Value(value));
   }
 
   auto event = std::make_unique<extensions::Event>(
diff --git a/chrome/browser/ash/accessibility/dictation_browsertest.cc b/chrome/browser/ash/accessibility/dictation_browsertest.cc
index eaddc740..f83bfc0 100644
--- a/chrome/browser/ash/accessibility/dictation_browsertest.cc
+++ b/chrome/browser/ash/accessibility/dictation_browsertest.cc
@@ -9,12 +9,12 @@
 #include "base/strings/utf_string_conversions.h"
 #include "base/timer/timer.h"
 #include "chrome/browser/ash/accessibility/accessibility_manager.h"
-#include "chrome/browser/ash/accessibility/soda_installer_impl_chromeos.h"
 #include "chrome/browser/speech/cros_speech_recognition_service_factory.h"
 #include "chrome/browser/speech/fake_speech_recognition_service.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "components/soda/soda_installer.h"
+#include "components/soda/soda_installer_impl_chromeos.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/fake_speech_recognition_manager.h"
 #include "media/mojo/mojom/speech_recognition_service.mojom.h"
diff --git a/chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge.cc b/chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge.cc
index 6e96a0c..f899a7f 100644
--- a/chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge.cc
+++ b/chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge.cc
@@ -33,6 +33,7 @@
 #include "components/exo/surface.h"
 #include "components/exo/wm_helper.h"
 #include "components/language/core/browser/pref_names.h"
+#include "components/live_caption/pref_names.h"
 #include "components/prefs/pref_change_registrar.h"
 #include "components/prefs/pref_service.h"
 #include "ui/accessibility/accessibility_features.h"
diff --git a/chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge_unittest.cc b/chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge_unittest.cc
index 1c46419..212802a 100644
--- a/chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge_unittest.cc
+++ b/chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge_unittest.cc
@@ -34,6 +34,7 @@
 #include "components/exo/shell_surface.h"
 #include "components/exo/shell_surface_util.h"
 #include "components/language/core/browser/pref_names.h"
+#include "components/live_caption/pref_names.h"
 #include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/testing_pref_service.h"
 #include "extensions/browser/event_router.h"
diff --git a/chrome/browser/ash/certificate_provider/certificate_provider_service_factory.cc b/chrome/browser/ash/certificate_provider/certificate_provider_service_factory.cc
index 11946cc..5e6130d0 100644
--- a/chrome/browser/ash/certificate_provider/certificate_provider_service_factory.cc
+++ b/chrome/browser/ash/certificate_provider/certificate_provider_service_factory.cc
@@ -86,8 +86,9 @@
     int request_id) {
   api_cp::CertificatesUpdateRequest certificates_update_request;
   certificates_update_request.certificates_request_id = request_id;
-  auto event_args = std::make_unique<base::ListValue>();
-  event_args->Append(certificates_update_request.ToValue());
+  std::vector<base::Value> event_args;
+  event_args.push_back(
+      base::Value::FromUniquePtrValue(certificates_update_request.ToValue()));
   return std::make_unique<extensions::Event>(
       extensions::events::CERTIFICATEPROVIDER_ON_CERTIFICATES_UPDATE_REQUESTED,
       api_cp::OnCertificatesUpdateRequested::kEventName, std::move(event_args));
@@ -96,8 +97,8 @@
 // Constructs the legacy "onCertificatesRequested" event.
 std::unique_ptr<extensions::Event> BuildOnCertificatesRequestedEvent(
     int request_id) {
-  auto event_args = std::make_unique<base::ListValue>();
-  event_args->Append(request_id);
+  std::vector<base::Value> event_args;
+  event_args.push_back(base::Value(request_id));
   return std::make_unique<extensions::Event>(
       extensions::events::CERTIFICATEPROVIDER_ON_CERTIFICATES_REQUESTED,
       api_cp::OnCertificatesRequested::kEventName, std::move(event_args));
@@ -145,8 +146,8 @@
       net::x509_util::CryptoBufferAsStringPiece(certificate.cert_buffer());
   request.certificate.assign(cert_der.begin(), cert_der.end());
 
-  auto event_args = std::make_unique<base::ListValue>();
-  event_args->Append(request.ToValue());
+  std::vector<base::Value> event_args;
+  event_args.push_back(base::Value::FromUniquePtrValue(request.ToValue()));
 
   return std::make_unique<extensions::Event>(
       extensions::events::CERTIFICATEPROVIDER_ON_SIGNATURE_REQUESTED,
@@ -196,9 +197,9 @@
   }
   request.digest.resize(digest_len);
 
-  std::unique_ptr<base::ListValue> event_args(new base::ListValue);
-  event_args->AppendInteger(request_id);
-  event_args->Append(request.ToValue());
+  std::vector<base::Value> event_args;
+  event_args.push_back(base::Value(request_id));
+  event_args.push_back(base::Value::FromUniquePtrValue(request.ToValue()));
 
   return std::make_unique<extensions::Event>(
       extensions::events::CERTIFICATEPROVIDER_ON_SIGN_DIGEST_REQUESTED,
diff --git a/chrome/browser/ash/crosapi/browser_loader.cc b/chrome/browser/ash/crosapi/browser_loader.cc
index 4260257..9136d71 100644
--- a/chrome/browser/ash/crosapi/browser_loader.cc
+++ b/chrome/browser/ash/crosapi/browser_loader.cc
@@ -15,6 +15,8 @@
 #include "base/logging.h"
 #include "base/task/task_traits.h"
 #include "base/task/thread_pool.h"
+#include "base/values.h"
+#include "base/version.h"
 #include "chrome/browser/ash/crosapi/browser_util.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/ui/ash/system_tray_client_impl.h"
@@ -46,6 +48,22 @@
 constexpr ComponentInfo kLacrosDogfoodStableInfo = {
     "lacros-dogfood-stable", "hnfmbeciphpghlfgpjfbcdifbknombnk"};
 
+// The rootfs lacros-chrome binary related files.
+constexpr char kLacrosChromeBinary[] = "chrome";
+constexpr char kLacrosImage[] = "lacros.squash";
+constexpr char kLacrosMetadata[] = "metadata.json";
+
+// The rootfs lacros-chrome binary related paths.
+// Must be kept in sync with lacros upstart conf files.
+constexpr char kRootfsLacrosMountPoint[] = "/run/lacros";
+constexpr char kRootfsLacrosPath[] = "/opt/google/lacros";
+
+// Lacros upstart jobs for mounting/unmounting the lacros-chrome image.
+// The conversion of upstart job names to dbus object paths is undocumented. See
+// function nih_dbus_path in libnih for the implementation.
+constexpr char kLacrosMounterUpstartJob[] = "lacros_2dmounter";
+constexpr char kLacrosUnmounterUpstartJob[] = "lacros_2dunmounter";
+
 ComponentInfo GetLacrosComponentInfo() {
   const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess();
   if (cmdline->HasSwitch(browser_util::kLacrosStabilitySwitch)) {
@@ -71,12 +89,18 @@
   return GetLacrosComponentInfo().crx_id;
 }
 
+// Returns whether lacros-chrome component is already installed.
+bool CheckInstalledMayBlock(
+    scoped_refptr<component_updater::CrOSComponentManager> manager) {
+  return manager->IsRegisteredMayBlock(GetLacrosComponentName());
+}
+
 // Returns whether lacros-fishfood component is already installed.
 // If it is, delete the user directory, too, because it will be
 // uninstalled.
 bool CheckInstalledAndMaybeRemoveUserDirectory(
     scoped_refptr<component_updater::CrOSComponentManager> manager) {
-  if (!manager->IsRegisteredMayBlock(GetLacrosComponentName()))
+  if (!CheckInstalledMayBlock(manager))
     return false;
 
   // Since we're already on a background thread, delete the user-data-dir
@@ -110,14 +134,20 @@
 
 BrowserLoader::BrowserLoader(
     scoped_refptr<component_updater::CrOSComponentManager> manager)
-    : BrowserLoader(std::make_unique<DelegateImpl>(), manager) {}
+    : BrowserLoader(std::make_unique<DelegateImpl>(),
+                    manager,
+                    g_browser_process->component_updater(),
+                    chromeos::UpstartClient::Get()) {}
 
 BrowserLoader::BrowserLoader(
     std::unique_ptr<Delegate> delegate,
-    scoped_refptr<component_updater::CrOSComponentManager> manager)
+    scoped_refptr<component_updater::CrOSComponentManager> manager,
+    component_updater::ComponentUpdateService* updater,
+    chromeos::UpstartClient* upstart_client)
     : delegate_(std::move(delegate)),
       component_manager_(manager),
-      component_update_service_(g_browser_process->component_updater()) {
+      component_update_service_(updater),
+      upstart_client_(upstart_client) {
   DCHECK(delegate_);
   DCHECK(component_manager_);
 }
@@ -148,14 +178,134 @@
     return;
   }
 
+  // TODO(b/188473251): Remove this check once rootfs lacros-chrome is in.
+  if (!base::PathExists(
+          base::FilePath(kRootfsLacrosPath).Append(kLacrosImage))) {
+    LOG(ERROR) << "Rootfs lacros image is missing. Going to load lacros "
+                  "component instead.";
+    LoadStatefulLacros(std::move(callback));
+    return;
+  }
+
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE, {base::MayBlock()},
+      base::BindOnce(&CheckInstalledMayBlock, component_manager_),
+      base::BindOnce(&BrowserLoader::OnLoadSelection,
+                     weak_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void BrowserLoader::OnLoadSelection(LoadCompletionCallback callback,
+                                    bool was_installed) {
+  // If there currently isn't a stateful lacros-chrome binary, proceed to use
+  // the rootfs lacros-chrome binary and start the installation of the
+  // stateful lacros-chrome binary in the background.
+  if (!was_installed) {
+    LoadRootfsLacros(std::move(callback));
+    LoadStatefulLacros({});
+    return;
+  }
+
+  // Otherwise proceed to compare the lacros-chrome binary versions.
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE, {base::MayBlock()},
+      base::BindOnce(&browser_util::GetRootfsLacrosVersionMayBlock,
+                     base::FilePath(kRootfsLacrosPath).Append(kLacrosMetadata)),
+      base::BindOnce(&BrowserLoader::OnLoadVersionSelection,
+                     weak_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void BrowserLoader::OnLoadVersionSelection(
+    LoadCompletionCallback callback,
+    base::Version rootfs_lacros_version) {
+  // Compare the rootfs vs stateful lacros-chrome binary versions.
+  // If the rootfs lacros-chrome is greater than or equal to the stateful
+  // lacros-chrome version, prioritize using the rootfs lacros-chrome and let
+  // stateful lacros-chrome update in the background.
+  if (rootfs_lacros_version.IsValid()) {
+    auto lacros_component_name = GetLacrosComponentName();
+    for (const auto& component_info :
+         component_update_service_->GetComponents()) {
+      if (component_info.id != lacros_component_name)
+        continue;
+      if (component_info.version <= rootfs_lacros_version) {
+        LOG(WARNING) << "Stateful lacros version ("
+                     << component_info.version.GetString()
+                     << ") is older or same as the rootfs lacros version ("
+                     << rootfs_lacros_version.GetString()
+                     << ", proceeding to use rootfs lacros.";
+        LoadRootfsLacros(std::move(callback));
+        LoadStatefulLacros({});
+        return;
+      }
+      // Break out to use stateful lacros-chrome.
+      LOG(WARNING)
+          << "Stateful lacros version is newer than the one in rootfs, "
+          << "procceding to use stateful lacros.";
+      break;
+    }
+  }
+  LoadStatefulLacros(std::move(callback));
+}
+
+void BrowserLoader::LoadStatefulLacros(LoadCompletionCallback callback) {
+  LOG(WARNING) << "Loading stateful lacros.";
+  // Unmount the rootfs lacros-chrome if we want to use stateful lacros-chrome.
+  // This will keep stateful lacros-chrome only mounted and not hold the rootfs
+  // lacros-chrome mount until a `Unload`.
+  if (callback && base::PathExists(base::FilePath(kRootfsLacrosMountPoint)
+                                       .Append(kLacrosChromeBinary))) {
+    // Ignore the unmount result.
+    upstart_client_->StartJob(kLacrosUnmounterUpstartJob, {},
+                              base::BindOnce([](bool) {}));
+  }
   component_manager_->Load(
       GetLacrosComponentName(),
       component_updater::CrOSComponentManager::MountPolicy::kMount,
       // If a compatible installation exists, use that and download any updates
       // in the background.
       component_updater::CrOSComponentManager::UpdatePolicy::kDontForce,
-      base::BindOnce(&BrowserLoader::OnLoadComplete, weak_factory_.GetWeakPtr(),
-                     std::move(callback)));
+      // If `callback` is null, means stateful lacros-chrome should be
+      // installed/updated but rootfs lacros-chrome will be used.
+      callback
+          ? base::BindOnce(&BrowserLoader::OnLoadComplete,
+                           weak_factory_.GetWeakPtr(), std::move(callback))
+          : base::BindOnce([](component_updater::CrOSComponentManager::Error,
+                              const base::FilePath& path) {}));
+}
+
+void BrowserLoader::LoadRootfsLacros(LoadCompletionCallback callback) {
+  LOG(WARNING) << "Loading rootfs lacros.";
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE, {base::MayBlock()},
+      base::BindOnce(
+          &base::PathExists,
+          base::FilePath(kRootfsLacrosMountPoint).Append(kLacrosChromeBinary)),
+      base::BindOnce(&BrowserLoader::OnLoadRootfsLacros,
+                     weak_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void BrowserLoader::OnLoadRootfsLacros(LoadCompletionCallback callback,
+                                       bool already_mounted) {
+  if (already_mounted) {
+    OnUpstartLacrosMounter(std::move(callback), true);
+    return;
+  }
+  upstart_client_->StartJob(
+      kLacrosMounterUpstartJob, {},
+      base::BindOnce(&BrowserLoader::OnUpstartLacrosMounter,
+                     weak_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void BrowserLoader::OnUpstartLacrosMounter(LoadCompletionCallback callback,
+                                           bool success) {
+  if (!success)
+    LOG(WARNING) << "Upstart failed to mount rootfs lacros.";
+  OnLoadComplete(
+      std::move(callback), component_updater::CrOSComponentManager::Error::NONE,
+      // If mounting wasn't successful, return a empty mount point to indicate
+      // failure. `OnLoadComplete` handles empty mount points and forwards the
+      // errors on the return callbacks.
+      success ? base::FilePath(kRootfsLacrosMountPoint) : base::FilePath());
 }
 
 void BrowserLoader::Unload() {
@@ -166,6 +316,10 @@
                      component_manager_),
       base::BindOnce(&BrowserLoader::OnCheckInstalled,
                      weak_factory_.GetWeakPtr()));
+  // Unmount the rootfs lacros-chrome if it was mounted.
+  // Ignore the unmount result.
+  upstart_client_->StartJob(kLacrosUnmounterUpstartJob, {},
+                            base::BindOnce([](bool) {}));
 }
 
 void BrowserLoader::OnEvent(Events event, const std::string& id) {
@@ -179,8 +333,9 @@
     LoadCompletionCallback callback,
     component_updater::CrOSComponentManager::Error error,
     const base::FilePath& path) {
-  // Bail out on error.
-  if (error != component_updater::CrOSComponentManager::Error::NONE) {
+  // Bail out on error or empty `path`.
+  if (error != component_updater::CrOSComponentManager::Error::NONE ||
+      path.empty()) {
     LOG(WARNING) << "Error loading lacros component image: "
                  << static_cast<int>(error);
     std::move(callback).Run(base::FilePath());
diff --git a/chrome/browser/ash/crosapi/browser_loader.h b/chrome/browser/ash/crosapi/browser_loader.h
index 92b5f6b8..71f3b01 100644
--- a/chrome/browser/ash/crosapi/browser_loader.h
+++ b/chrome/browser/ash/crosapi/browser_loader.h
@@ -7,9 +7,11 @@
 
 #include "base/callback.h"
 #include "base/files/file_path.h"
+#include "base/gtest_prod_util.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
 #include "chrome/browser/component_updater/cros_component_manager.h"
+#include "chromeos/dbus/upstart/upstart_client.h"
 #include "components/component_updater/component_updater_service.h"
 
 namespace crosapi {
@@ -32,14 +34,16 @@
       scoped_refptr<component_updater::CrOSComponentManager> manager);
   // Constructor for testing.
   BrowserLoader(std::unique_ptr<Delegate> delegate,
-                scoped_refptr<component_updater::CrOSComponentManager> manager);
+                scoped_refptr<component_updater::CrOSComponentManager> manager,
+                component_updater::ComponentUpdateService* updater,
+                chromeos::UpstartClient* upstart_client);
 
   BrowserLoader(const BrowserLoader&) = delete;
   BrowserLoader& operator=(const BrowserLoader&) = delete;
 
   ~BrowserLoader() override;
 
-  // Starts to load lacros-chrome binary.
+  // Starts to load lacros-chrome binary or the rootfs lacros-chrome binary.
   // |callback| is called on completion with the path to the lacros-chrome on
   // success, or an empty filepath on failure.
   using LoadCompletionCallback =
@@ -54,6 +58,31 @@
   void OnEvent(Events event, const std::string& id) override;
 
  private:
+  FRIEND_TEST_ALL_PREFIXES(BrowserLoaderTest,
+                           OnLoadSelectionQuicklyChooseRootfs);
+  FRIEND_TEST_ALL_PREFIXES(BrowserLoaderTest, OnLoadVersionSelectionStateful);
+  FRIEND_TEST_ALL_PREFIXES(BrowserLoaderTest, OnLoadVersionSelectionRootfs);
+  FRIEND_TEST_ALL_PREFIXES(BrowserLoaderTest,
+                           OnLoadVersionSelectionRootfsIsOlder);
+
+  // Loads/Installs the stateful lacros component.
+  void LoadStatefulLacros(LoadCompletionCallback callback);
+
+  // Loads the rootfs lacros component.
+  void LoadRootfsLacros(LoadCompletionCallback callback);
+  void OnLoadRootfsLacros(LoadCompletionCallback callback,
+                          bool already_mounted);
+
+  // Called to quickly load rootfs lacros if stateful lacros was missing and
+  // start the stateful lacros installation.
+  void OnLoadSelection(LoadCompletionCallback callback, bool was_installed);
+
+  // Called to determine which lacros to load based on version (rootfs vs
+  // stateful).
+  void LoadVersionSelection(LoadCompletionCallback callback);
+  void OnLoadVersionSelection(LoadCompletionCallback callback,
+                              base::Version rootfs_lacros_version);
+
   // Called on the completion of loading.
   void OnLoadComplete(LoadCompletionCallback callback,
                       component_updater::CrOSComponentManager::Error error,
@@ -66,6 +95,9 @@
   // Unloads the component. Called after system salt is available.
   void UnloadAfterCleanUp(const std::string& ignored_salt);
 
+  // Callback from upstart mounting lacros-chrome.
+  void OnUpstartLacrosMounter(LoadCompletionCallback callback, bool success);
+
   // Allows stubbing out some methods for testing.
   std::unique_ptr<Delegate> delegate_;
 
@@ -74,6 +106,10 @@
   // May be null in tests.
   component_updater::ComponentUpdateService* const component_update_service_;
 
+  // Pointer held to `UpstartClient` for testing purposes.
+  // Otherwise, the lifetime is the same as `chromeos::UpstartClient::Get()`.
+  chromeos::UpstartClient* const upstart_client_;
+
   base::WeakPtrFactory<BrowserLoader> weak_factory_{this};
 };
 
diff --git a/chrome/browser/ash/crosapi/browser_loader_unittest.cc b/chrome/browser/ash/crosapi/browser_loader_unittest.cc
index cbb1c23..1e11f758 100644
--- a/chrome/browser/ash/crosapi/browser_loader_unittest.cc
+++ b/chrome/browser/ash/crosapi/browser_loader_unittest.cc
@@ -4,6 +4,8 @@
 
 #include "chrome/browser/ash/crosapi/browser_loader.h"
 
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
 #include "base/run_loop.h"
 #include "base/test/bind.h"
 #include "base/test/scoped_feature_list.h"
@@ -11,10 +13,13 @@
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/component_updater/fake_cros_component_manager.h"
 #include "chrome/test/base/browser_process_platform_part_test_api_chromeos.h"
+#include "chromeos/dbus/upstart/fake_upstart_client.h"
+#include "components/component_updater/mock_component_updater_service.h"
 #include "components/update_client/update_client.h"
 #include "content/public/test/browser_task_environment.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+using testing::Return;
 using update_client::UpdateClient;
 
 namespace crosapi {
@@ -23,6 +28,9 @@
 // Copied from browser_loader.cc
 constexpr char kLacrosComponentName[] = "lacros-dogfood-dev";
 constexpr char kLacrosComponentId[] = "ldobopbhiamakmncndpkeelenhdmgfhk";
+constexpr char kLacrosMounterUpstartJob[] = "lacros_2dmounter";
+
+}  // namespace
 
 // Delegate for testing.
 class DelegateImpl : public BrowserLoader::Delegate {
@@ -41,64 +49,137 @@
 
 class BrowserLoaderTest : public testing::Test {
  public:
-  BrowserLoaderTest() { browser_util::SetLacrosEnabledForTest(true); }
+  BrowserLoaderTest() {
+    browser_util::SetLacrosEnabledForTest(true);
+
+    // Create dependencies for object under test.
+    component_manager_ =
+        base::MakeRefCounted<component_updater::FakeCrOSComponentManager>();
+    component_manager_->set_supported_components({kLacrosComponentName});
+    component_manager_->ResetComponentState(
+        kLacrosComponentName,
+        component_updater::FakeCrOSComponentManager::ComponentInfo(
+            component_updater::CrOSComponentManager::Error::NONE,
+            base::FilePath("/install/path"), base::FilePath("/mount/path")));
+    browser_part_ = std::make_unique<BrowserProcessPlatformPartTestApi>(
+        g_browser_process->platform_part());
+    browser_part_->InitializeCrosComponentManager(component_manager_);
+
+    // Create object under test.
+    auto delegate_ptr = std::make_unique<DelegateImpl>();
+    delegate_ = delegate_ptr.get();
+
+    browser_loader_ = std::make_unique<BrowserLoader>(
+        std::move(delegate_ptr), component_manager_,
+        &mock_component_update_service_, &fake_upstart_client_);
+  }
 
   ~BrowserLoaderTest() override {
+    browser_part_->ShutdownCrosComponentManager();
     browser_util::SetLacrosEnabledForTest(false);
   }
 
   // Public because this is test code.
   content::BrowserTaskEnvironment task_environment_;
+
+ protected:
+  DelegateImpl* delegate_;
+  component_updater::MockComponentUpdateService mock_component_update_service_;
+  scoped_refptr<component_updater::FakeCrOSComponentManager> component_manager_;
+  chromeos::FakeUpstartClient fake_upstart_client_;
+  std::unique_ptr<BrowserProcessPlatformPartTestApi> browser_part_;
+  std::unique_ptr<BrowserLoader> browser_loader_;
 };
 
 TEST_F(BrowserLoaderTest, ShowUpdateNotification) {
-  // Create dependencies for object under test.
-  scoped_refptr<component_updater::FakeCrOSComponentManager> component_manager =
-      base::MakeRefCounted<component_updater::FakeCrOSComponentManager>();
-  component_manager->set_supported_components({kLacrosComponentName});
-  component_manager->ResetComponentState(
-      kLacrosComponentName,
-      component_updater::FakeCrOSComponentManager::ComponentInfo(
-          component_updater::CrOSComponentManager::Error::NONE,
-          base::FilePath("/install/path"), base::FilePath("/mount/path")));
-  BrowserProcessPlatformPartTestApi browser_part(
-      g_browser_process->platform_part());
-  browser_part.InitializeCrosComponentManager(component_manager);
-
-  // Create object under test.
-  auto delegate_ptr = std::make_unique<DelegateImpl>();
-  DelegateImpl* delegate = delegate_ptr.get();
-  BrowserLoader browser_loader(std::move(delegate_ptr), component_manager);
-
   // Creating the loader does not trigger an update notification.
-  EXPECT_EQ(0, delegate->set_lacros_update_available_);
+  EXPECT_EQ(0, delegate_->set_lacros_update_available_);
 
   // The initial load of the component does not trigger an update notification.
   base::RunLoop run_loop;
-  browser_loader.Load(base::BindLambdaForTesting(
+  browser_loader_->Load(base::BindLambdaForTesting(
       [&](const base::FilePath&) { run_loop.Quit(); }));
   run_loop.Run();
-  EXPECT_EQ(0, delegate->set_lacros_update_available_);
+  EXPECT_EQ(0, delegate_->set_lacros_update_available_);
 
   // Update check does not trigger an update notification.
-  browser_loader.OnEvent(
+  browser_loader_->OnEvent(
       UpdateClient::Observer::Events::COMPONENT_CHECKING_FOR_UPDATES,
       kLacrosComponentId);
-  EXPECT_EQ(0, delegate->set_lacros_update_available_);
+  EXPECT_EQ(0, delegate_->set_lacros_update_available_);
 
   // Update download does not trigger an update notification.
-  browser_loader.OnEvent(
+  browser_loader_->OnEvent(
       UpdateClient::Observer::Events::COMPONENT_UPDATE_DOWNLOADING,
       kLacrosComponentId);
-  EXPECT_EQ(0, delegate->set_lacros_update_available_);
+  EXPECT_EQ(0, delegate_->set_lacros_update_available_);
 
   // Update completion trigger the notification.
-  browser_loader.OnEvent(UpdateClient::Observer::Events::COMPONENT_UPDATED,
-                         kLacrosComponentId);
-  EXPECT_EQ(1, delegate->set_lacros_update_available_);
-
-  browser_part.ShutdownCrosComponentManager();
+  browser_loader_->OnEvent(UpdateClient::Observer::Events::COMPONENT_UPDATED,
+                           kLacrosComponentId);
+  EXPECT_EQ(1, delegate_->set_lacros_update_available_);
 }
 
-}  // namespace
+TEST_F(BrowserLoaderTest, OnLoadSelectionQuicklyChooseRootfs) {
+  bool callback_called = false;
+  fake_upstart_client_.set_start_job_cb(base::BindRepeating(
+      [](bool* b, const std::string& job,
+         const std::vector<std::string>& upstart_env) {
+        EXPECT_EQ(job, kLacrosMounterUpstartJob);
+        *b = true;
+        return true;
+      },
+      &callback_called));
+  // Set `was_installed` to false, in order to quickly mount rootfs
+  // lacros-chrome.
+  browser_loader_->OnLoadSelection(base::BindOnce([](const base::FilePath&) {}),
+                                   false);
+  task_environment_.RunUntilIdle();
+  EXPECT_TRUE(callback_called);
+}
+
+TEST_F(BrowserLoaderTest, OnLoadVersionSelectionStateful) {
+  // Pass in an invalid `base::Version`.
+  browser_loader_->OnLoadVersionSelection({}, {});
+}
+
+TEST_F(BrowserLoaderTest, OnLoadVersionSelectionRootfs) {
+  EXPECT_CALL(mock_component_update_service_, GetComponents())
+      .WillOnce(Return(std::vector<component_updater::ComponentInfo>{
+          {kLacrosComponentName, "", {}, base::Version("1.0.0")}}));
+
+  bool callback_called = false;
+  fake_upstart_client_.set_start_job_cb(base::BindRepeating(
+      [](bool* b, const std::string& job,
+         const std::vector<std::string>& upstart_env) {
+        EXPECT_EQ(job, kLacrosMounterUpstartJob);
+        *b = true;
+        return true;
+      },
+      &callback_called));
+  // Pass in a rootfs lacros-chrome version that is newer.
+  browser_loader_->OnLoadVersionSelection(
+      base::BindOnce([](const base::FilePath&) {}), base::Version("2.0.0"));
+  task_environment_.RunUntilIdle();
+  EXPECT_TRUE(callback_called);
+}
+
+TEST_F(BrowserLoaderTest, OnLoadVersionSelectionRootfsIsOlder) {
+  EXPECT_CALL(mock_component_update_service_, GetComponents())
+      .WillOnce(Return(std::vector<component_updater::ComponentInfo>{
+          {kLacrosComponentName, "", {}, base::Version("3.0.0")}}));
+
+  bool callback_called = false;
+  fake_upstart_client_.set_start_job_cb(base::BindRepeating(
+      [](bool* b, const std::string& job,
+         const std::vector<std::string>& upstart_env) {
+        *b = true;
+        return true;
+      },
+      &callback_called));
+  // Pass in a rootfs lacros-chrome version that is older.
+  browser_loader_->OnLoadVersionSelection({}, base::Version("2.0.0"));
+  EXPECT_FALSE(callback_called);
+}
+
 }  // namespace crosapi
diff --git a/chrome/browser/ash/crosapi/browser_util.cc b/chrome/browser/ash/crosapi/browser_util.cc
index c2593c0..4d9a985 100644
--- a/chrome/browser/ash/crosapi/browser_util.cc
+++ b/chrome/browser/ash/crosapi/browser_util.cc
@@ -16,6 +16,7 @@
 #include "base/feature_list.h"
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
+#include "base/json/json_reader.h"
 #include "base/path_service.h"
 #include "base/process/process_handle.h"
 #include "base/stl_util.h"
@@ -66,6 +67,10 @@
 
 absl::optional<bool> g_lacros_primary_browser_for_test;
 
+// The rootfs lacros-chrome metadata keys.
+constexpr char kLacrosMetadataContentKey[] = "content";
+constexpr char kLacrosMetadataVersionKey[] = "version";
+
 // Some account types require features that aren't yet supported by lacros.
 // See https://crbug.com/1080693
 bool IsUserTypeAllowed(const User* user) {
@@ -594,5 +599,41 @@
   dict->SetString(user_id_hash, version.GetString());
 }
 
+base::Version GetRootfsLacrosVersionMayBlock(
+    const base::FilePath& version_file_path) {
+  if (!base::PathExists(version_file_path)) {
+    LOG(WARNING) << "The rootfs lacros-chrome metadata is missing.";
+    return {};
+  }
+
+  std::string metadata;
+  if (!base::ReadFileToString(version_file_path, &metadata)) {
+    PLOG(WARNING) << "Failed to read rootfs lacros-chrome metadata.";
+    return {};
+  }
+
+  absl::optional<base::Value> v = base::JSONReader::Read(metadata);
+  if (!v || !v->is_dict()) {
+    LOG(WARNING) << "Failed to parse rootfs lacros-chrome metadata.";
+    return {};
+  }
+
+  const base::Value* content = v->FindKey(kLacrosMetadataContentKey);
+  if (!content || !content->is_dict()) {
+    LOG(WARNING)
+        << "Failed to parse rootfs lacros-chrome metadata content key.";
+    return {};
+  }
+
+  const base::Value* version = content->FindKey(kLacrosMetadataVersionKey);
+  if (!version || !version->is_string()) {
+    LOG(WARNING)
+        << "Failed to parse rootfs lacros-chrome metadata version key.";
+    return {};
+  }
+
+  return base::Version{version->GetString()};
+}
+
 }  // namespace browser_util
 }  // namespace crosapi
diff --git a/chrome/browser/ash/crosapi/browser_util.h b/chrome/browser/ash/crosapi/browser_util.h
index f3ad60fb..0b8ded60 100644
--- a/chrome/browser/ash/crosapi/browser_util.h
+++ b/chrome/browser/ash/crosapi/browser_util.h
@@ -189,6 +189,11 @@
                    const std::string& user_id_hash,
                    const base::Version& version);
 
+// Gets the version of the rootfs lacros-chrome. By reading the metadata json
+// file in the correct format.
+base::Version GetRootfsLacrosVersionMayBlock(
+    const base::FilePath& version_file_path);
+
 }  // namespace browser_util
 }  // namespace crosapi
 
diff --git a/chrome/browser/ash/crosapi/browser_util_unittest.cc b/chrome/browser/ash/crosapi/browser_util_unittest.cc
index 5378494..b7f37ee7 100644
--- a/chrome/browser/ash/crosapi/browser_util_unittest.cc
+++ b/chrome/browser/ash/crosapi/browser_util_unittest.cc
@@ -5,6 +5,7 @@
 #include "chrome/browser/ash/crosapi/browser_util.h"
 
 #include "ash/constants/ash_features.h"
+#include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/json/json_reader.h"
 #include "base/test/scoped_feature_list.h"
@@ -493,4 +494,56 @@
   EXPECT_TRUE(dict->Equals(&expected));
 }
 
+TEST_F(BrowserUtilTest, GetRootfsLacrosVersionMayBlock) {
+  base::ScopedTempDir tmp_dir;
+  ASSERT_TRUE(tmp_dir.CreateUniqueTempDir());
+  const std::string kVersion = "91.0.4457";
+  const std::string kContent =
+      "{\"content\":{\"version\":\"" + kVersion + "\"}}";
+  auto path = tmp_dir.GetPath().Append("file");
+  ASSERT_TRUE(base::WriteFile(path, kContent));
+
+  EXPECT_EQ(browser_util::GetRootfsLacrosVersionMayBlock(path),
+            base::Version(kVersion));
+}
+
+TEST_F(BrowserUtilTest, GetRootfsLacrosVersionMayBlockMissingVersion) {
+  base::ScopedTempDir tmp_dir;
+  ASSERT_TRUE(tmp_dir.CreateUniqueTempDir());
+  const std::string kContent = "{\"content\":{}}";
+  auto path = tmp_dir.GetPath().Append("file");
+  ASSERT_TRUE(base::WriteFile(path, kContent));
+
+  EXPECT_FALSE(browser_util::GetRootfsLacrosVersionMayBlock(path).IsValid());
+}
+
+TEST_F(BrowserUtilTest, GetRootfsLacrosVersionMayBlockMissingContent) {
+  base::ScopedTempDir tmp_dir;
+  ASSERT_TRUE(tmp_dir.CreateUniqueTempDir());
+  const std::string kContent = "{}";
+  auto path = tmp_dir.GetPath().Append("file");
+  ASSERT_TRUE(base::WriteFile(path, kContent));
+
+  EXPECT_FALSE(browser_util::GetRootfsLacrosVersionMayBlock(path).IsValid());
+}
+
+TEST_F(BrowserUtilTest, GetRootfsLacrosVersionMayBlockMissingFile) {
+  base::ScopedTempDir tmp_dir;
+  ASSERT_TRUE(tmp_dir.CreateUniqueTempDir());
+  auto bad_path = tmp_dir.GetPath().Append("file");
+
+  EXPECT_FALSE(
+      browser_util::GetRootfsLacrosVersionMayBlock(bad_path).IsValid());
+}
+
+TEST_F(BrowserUtilTest, GetRootfsLacrosVersionMayBlockBadJson) {
+  base::ScopedTempDir tmp_dir;
+  ASSERT_TRUE(tmp_dir.CreateUniqueTempDir());
+  const std::string kContent = "!@#$";
+  auto path = tmp_dir.GetPath().Append("file");
+  ASSERT_TRUE(base::WriteFile(path, kContent));
+
+  EXPECT_FALSE(browser_util::GetRootfsLacrosVersionMayBlock(path).IsValid());
+}
+
 }  // namespace crosapi
diff --git a/chrome/browser/ash/file_system_provider/extension_provider.cc b/chrome/browser/ash/file_system_provider/extension_provider.cc
index 5e69307..b084724 100644
--- a/chrome/browser/ash/file_system_provider/extension_provider.cc
+++ b/chrome/browser/ash/file_system_provider/extension_provider.cc
@@ -110,7 +110,7 @@
       std::make_unique<extensions::Event>(
           extensions::events::FILE_SYSTEM_PROVIDER_ON_MOUNT_REQUESTED,
           extensions::api::file_system_provider::OnMountRequested::kEventName,
-          std::make_unique<base::ListValue>()));
+          std::vector<base::Value>()));
 
   return true;
 }
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 2ff2e2a..ac831919 100644
--- a/chrome/browser/ash/remote_apps/remote_apps_manager_browsertest.cc
+++ b/chrome/browser/ash/remote_apps/remote_apps_manager_browsertest.cc
@@ -35,6 +35,7 @@
 #include "chromeos/login/auth/user_context.h"
 #include "components/account_id/account_id.h"
 #include "components/policy/proto/chrome_device_policy.pb.h"
+#include "components/services/app_service/public/mojom/types.mojom.h"
 #include "components/user_manager/user.h"
 #include "components/user_manager/user_manager.h"
 #include "components/user_manager/user_type.h"
@@ -301,8 +302,24 @@
       base::FeatureList::IsEnabled(features::kAppServiceAdaptiveIcon)
           ? apps::IconEffects::kCrOsStandardIcon
           : apps::IconEffects::kResizeAndPad;
-  apps::ApplyIconEffects(icon_effects, 64, &icon);
-  CheckIconsEqual(icon, item->GetDefaultIcon());
+
+  base::RunLoop run_loop;
+  apps::mojom::IconValuePtr output_data = apps::mojom::IconValue::New();
+  apps::mojom::IconValuePtr iv = apps::mojom::IconValue::New();
+  iv->icon_type = apps::mojom::IconType::kStandard;
+  iv->uncompressed = icon;
+  iv->is_placeholder_icon = true;
+  apps::ApplyIconEffects(icon_effects, 64, std::move(iv),
+                         base::BindOnce(
+                             [](apps::mojom::IconValuePtr* result,
+                                base::OnceClosure load_app_icon_callback,
+                                apps::mojom::IconValuePtr icon) {
+                               *result = std::move(icon);
+                               std::move(load_app_icon_callback).Run();
+                             },
+                             &output_data, run_loop.QuitClosure()));
+  run_loop.Run();
+  CheckIconsEqual(output_data->uncompressed, item->GetDefaultIcon());
 }
 
 IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddAppError) {
diff --git a/chrome/browser/autofill/credit_card_accessory_controller_impl.cc b/chrome/browser/autofill/credit_card_accessory_controller_impl.cc
index ed03b04..7467e8a9 100644
--- a/chrome/browser/autofill/credit_card_accessory_controller_impl.cc
+++ b/chrome/browser/autofill/credit_card_accessory_controller_impl.cc
@@ -21,6 +21,7 @@
 #include "components/autofill/core/browser/autofill_browser_util.h"
 #include "components/autofill/core/browser/data_model/credit_card.h"
 #include "components/autofill/core/common/autofill_features.h"
+#include "components/autofill/core/common/autofill_payments_features.h"
 #include "components/strings/grit/components_strings.h"
 #include "ui/base/l10n/l10n_util.h"
 
@@ -41,6 +42,27 @@
                                        /*is_password=*/false, enabled));
 }
 
+void AddCardDetailsToUserInfo(const CreditCard& card,
+                              UserInfo* user_info,
+                              std::u16string cvc,
+                              bool enabled) {
+  if (card.HasValidExpirationDate()) {
+    AddSimpleField(card.Expiration2DigitMonthAsString(), user_info, enabled);
+    AddSimpleField(card.Expiration4DigitYearAsString(), user_info, enabled);
+  } else {
+    AddSimpleField(std::u16string(), user_info, enabled);
+    AddSimpleField(std::u16string(), user_info, enabled);
+  }
+
+  if (card.HasNameOnCard()) {
+    AddSimpleField(card.GetRawInfo(autofill::CREDIT_CARD_NAME_FULL), user_info,
+                   enabled);
+  } else {
+    AddSimpleField(std::u16string(), user_info, enabled);
+  }
+  AddSimpleField(cvc, user_info, enabled);
+}
+
 UserInfo TranslateCard(const CreditCard* data, bool enabled) {
   DCHECK(data);
 
@@ -50,21 +72,20 @@
   user_info.add_field(UserInfo::Field(obfuscated_number, obfuscated_number,
                                       data->guid(), /*is_password=*/false,
                                       enabled));
+  AddCardDetailsToUserInfo(*data, &user_info, std::u16string(), enabled);
 
-  if (data->HasValidExpirationDate()) {
-    AddSimpleField(data->Expiration2DigitMonthAsString(), &user_info, enabled);
-    AddSimpleField(data->Expiration4DigitYearAsString(), &user_info, enabled);
-  } else {
-    AddSimpleField(std::u16string(), &user_info, enabled);
-    AddSimpleField(std::u16string(), &user_info, enabled);
-  }
+  return user_info;
+}
 
-  if (data->HasNameOnCard()) {
-    AddSimpleField(data->GetRawInfo(autofill::CREDIT_CARD_NAME_FULL),
-                   &user_info, enabled);
-  } else {
-    AddSimpleField(std::u16string(), &user_info, enabled);
-  }
+UserInfo TranslateCachedCard(const CachedServerCardInfo* data, bool enabled) {
+  DCHECK(data);
+
+  const CreditCard& card = data->card;
+  UserInfo user_info(card.network());
+
+  AddSimpleField(card.GetRawInfo(autofill::CREDIT_CARD_NUMBER), &user_info,
+                 enabled);
+  AddCardDetailsToUserInfo(card, &user_info, data->cvc, enabled);
 
   return user_info;
 }
@@ -88,11 +109,26 @@
   bool allow_filling = valid_manager && ShouldAllowCreditCardFallbacks(
                                             GetManager()->client(),
                                             GetManager()->last_query_form());
-  std::transform(cards_cache_.begin(), cards_cache_.end(),
-                 std::back_inserter(info_to_add),
-                 [allow_filling](const CreditCard* data) {
-                   return TranslateCard(data, allow_filling);
-                 });
+
+  if (!cached_server_cards_.empty()) {
+    // Add the cached server cards first, so that they show up on the top of the
+    // manual filling view.
+    std::transform(cached_server_cards_.begin(), cached_server_cards_.end(),
+                   std::back_inserter(info_to_add),
+                   [allow_filling](const CachedServerCardInfo* data) {
+                     return TranslateCachedCard(data, allow_filling);
+                   });
+  }
+  // Only add cards that are not present in the cache. Otherwise, we might
+  // show duplicates.
+  for (auto* card : cards_cache_) {
+    if (cached_server_cards_.empty() ||
+        !GetManager()
+             ->credit_card_access_manager()
+             ->IsCardPresentInUnmaskedCache(card->server_id())) {
+      info_to_add.push_back(TranslateCard(card, allow_filling));
+    }
+  }
 
   const std::vector<FooterCommand> footer_commands = {FooterCommand(
       l10n_util::GetStringUTF16(
@@ -200,6 +236,7 @@
     FetchSuggestionsFromPersonalDataManager();
   } else {
     cards_cache_.clear();  // If cards cannot be filled, don't show them.
+    cached_server_cards_.clear();
   }
   absl::optional<AccessorySheetData> data = GetSheetData();
   if (source_observer_) {
@@ -288,6 +325,18 @@
     cards_cache_ = personal_data_manager_->GetCreditCardsToSuggest(
         /*include_server_cards=*/true);
   }
+  // TODO(crbug.com/1196021) Update the below logic to allow for showing only
+  // virtual cards if the kAutofillShowUnmaskedCachedCardInManualFillingView is
+  // disabled.
+  if (GetManager() && GetManager()->credit_card_access_manager() &&
+      base::FeatureList::IsEnabled(
+          autofill::features::
+              kAutofillShowUnmaskedCachedCardInManualFillingView)) {
+    cached_server_cards_ =
+        GetManager()->credit_card_access_manager()->GetCachedUnmaskedCards();
+  } else {
+    cached_server_cards_.clear();  // No data available.
+  }
 }
 
 base::WeakPtr<ManualFillingController>
diff --git a/chrome/browser/autofill/credit_card_accessory_controller_impl.h b/chrome/browser/autofill/credit_card_accessory_controller_impl.h
index 347b9cf8..359ee70 100644
--- a/chrome/browser/autofill/credit_card_accessory_controller_impl.h
+++ b/chrome/browser/autofill/credit_card_accessory_controller_impl.h
@@ -77,6 +77,11 @@
   autofill::BrowserAutofillManager* af_manager_for_testing_ = nullptr;
   autofill::AutofillDriver* af_driver_for_testing_ = nullptr;
 
+  // Cached cards that are already unmasked by the user. These are shown to the
+  // user in plaintext and won't require any authentication when filling is
+  // triggered.
+  std::vector<const CachedServerCardInfo*> cached_server_cards_;
+
   // The observer to notify if available suggestions change.
   FillingSourceObserver source_observer_;
 
diff --git a/chrome/browser/autofill/credit_card_accessory_controller_impl_unittest.cc b/chrome/browser/autofill/credit_card_accessory_controller_impl_unittest.cc
index e29b6408..7320c92 100644
--- a/chrome/browser/autofill/credit_card_accessory_controller_impl_unittest.cc
+++ b/chrome/browser/autofill/credit_card_accessory_controller_impl_unittest.cc
@@ -17,6 +17,7 @@
 #include "components/autofill/core/browser/test_autofill_client.h"
 #include "components/autofill/core/browser/test_autofill_driver.h"
 #include "components/autofill/core/browser/test_personal_data_manager.h"
+#include "components/autofill/core/common/autofill_payments_features.h"
 #include "components/autofill/core/common/form_data.h"
 #include "components/autofill/core/common/unique_ids.h"
 #include "components/strings/grit/components_strings.h"
@@ -121,6 +122,8 @@
                                                         &data_manager_)) {}
 
   void SetUp() override {
+    scoped_feature_list_.InitAndEnableFeature(
+        autofill::features::kAutofillShowUnmaskedCachedCardInManualFillingView);
     ChromeRenderViewHostTestHarness::SetUp();
     NavigateAndCommit(GURL(kExampleSite));
     SetFormOrigin(GURL(kExampleSite));
@@ -160,6 +163,25 @@
   TestBrowserAutofillManager af_manager_;
   base::MockCallback<AccessoryController::FillingSourceObserver>
       filling_source_observer_;
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+class CreditCardAccessoryControllerTestWithoutSupportingUnmaskedCards
+    : public CreditCardAccessoryControllerTest {
+ public:
+  void SetUp() override {
+    scoped_feature_list_.InitAndDisableFeature(
+        autofill::features::kAutofillShowUnmaskedCachedCardInManualFillingView);
+    ChromeRenderViewHostTestHarness::SetUp();
+    NavigateAndCommit(GURL(kExampleSite));
+    SetFormOrigin(GURL(kExampleSite));
+    FocusWebContentsOnMainFrame();
+
+    CreditCardAccessoryControllerImpl::CreateForWebContentsForTesting(
+        web_contents(), mock_mf_controller_.AsWeakPtr(), &data_manager_,
+        &af_manager_, &mock_af_driver_);
+    data_manager_.SetPrefService(profile()->GetPrefs());
+  }
 };
 
 TEST_F(CreditCardAccessoryControllerTest, RefreshSuggestions) {
@@ -185,6 +207,7 @@
           .AppendSimpleField(card.Expiration2DigitMonthAsString())
           .AppendSimpleField(card.Expiration4DigitYearAsString())
           .AppendSimpleField(card.GetRawInfo(autofill::CREDIT_CARD_NAME_FULL))
+          .AppendSimpleField(std::u16string())
           .Build());
 }
 
@@ -222,6 +245,9 @@
                              card.GetRawInfo(autofill::CREDIT_CARD_NAME_FULL),
                              /*is_obfuscated=*/false,
                              /*selectable=*/false)
+                .AppendField(std::u16string(), std::u16string(),
+                             /*is_obfuscated=*/false,
+                             /*selectable=*/false)
                 .Build());
 }
 
@@ -260,4 +286,121 @@
   controller()->OnFillingTriggered(field_id, field);
 }
 
+TEST_F(CreditCardAccessoryControllerTest,
+       RefreshSuggestionsUnmaskedCachedCard) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      autofill::features::kAutofillShowUnmaskedCachedCardInManualFillingView);
+  // Store a full server card in the credit_card_access_manager's
+  // unmasked_cards_cache.
+  autofill::CreditCard card = test::GetCreditCard();
+  card.set_record_type(CreditCard::FULL_SERVER_CARD);
+  data_manager_.AddCreditCard(card);
+  std::u16string cvc = u"123";
+  af_manager_.credit_card_access_manager()->CacheUnmaskedCardInfo(card, cvc);
+  autofill::AccessorySheetData result(autofill::AccessoryTabType::CREDIT_CARDS,
+                                      std::u16string());
+
+  EXPECT_CALL(mock_mf_controller_, RefreshSuggestions(_))
+      .WillOnce(SaveArg<0>(&result));
+  ASSERT_TRUE(controller());
+  controller()->RefreshSuggestions();
+
+  EXPECT_EQ(result, controller()->GetSheetData());
+  // Verify that the full card number and the cvc fields are added to the
+  // accessory sheet data.
+  EXPECT_EQ(
+      result,
+      CreditCardAccessorySheetDataBuilder()
+          .AddUserInfo(kVisaCard)
+          .AppendSimpleField(card.GetRawInfo(autofill::CREDIT_CARD_NUMBER))
+          .AppendSimpleField(card.Expiration2DigitMonthAsString())
+          .AppendSimpleField(card.Expiration4DigitYearAsString())
+          .AppendSimpleField(card.GetRawInfo(autofill::CREDIT_CARD_NAME_FULL))
+          .AppendSimpleField(cvc)
+          .Build());
+}
+
+TEST_F(CreditCardAccessoryControllerTest, UnmaskedCacheCardsReorderedToTheTop) {
+  // Add a masked card to PersonalDataManager.
+  autofill::CreditCard masked_card = test::GetMaskedServerCard();
+  data_manager_.AddCreditCard(masked_card);
+  // Add a full server card to PersonalDataManager and also cache it in hte
+  // CreditCardAccessManager.
+  autofill::CreditCard unmasked_card = test::GetCreditCard();
+  unmasked_card.set_record_type(CreditCard::FULL_SERVER_CARD);
+  data_manager_.AddCreditCard(unmasked_card);
+  std::u16string cvc = u"123";
+  af_manager_.credit_card_access_manager()->CacheUnmaskedCardInfo(unmasked_card,
+                                                                  cvc);
+  autofill::AccessorySheetData result(autofill::AccessoryTabType::CREDIT_CARDS,
+                                      std::u16string());
+
+  EXPECT_CALL(mock_mf_controller_, RefreshSuggestions(_))
+      .WillOnce(SaveArg<0>(&result));
+  ASSERT_TRUE(controller());
+  controller()->RefreshSuggestions();
+
+  EXPECT_EQ(result, controller()->GetSheetData());
+  // Verify that the unmasked card is at the top followed by the masked card.
+  EXPECT_EQ(
+      result,
+      CreditCardAccessorySheetDataBuilder()
+          .AddUserInfo(kVisaCard)
+          .AppendSimpleField(
+              unmasked_card.GetRawInfo(autofill::CREDIT_CARD_NUMBER))
+          .AppendSimpleField(unmasked_card.Expiration2DigitMonthAsString())
+          .AppendSimpleField(unmasked_card.Expiration4DigitYearAsString())
+          .AppendSimpleField(
+              unmasked_card.GetRawInfo(autofill::CREDIT_CARD_NAME_FULL))
+          .AppendSimpleField(cvc)
+          .AddUserInfo(kMasterCard)
+          .AppendField(masked_card.ObfuscatedLastFourDigits(),
+                       masked_card.ObfuscatedLastFourDigits(),
+                       masked_card.guid(),
+                       /*is_obfuscated=*/false,
+                       /*selectable=*/true)
+          .AppendSimpleField(masked_card.Expiration2DigitMonthAsString())
+          .AppendSimpleField(masked_card.Expiration4DigitYearAsString())
+          .AppendSimpleField(
+              masked_card.GetRawInfo(autofill::CREDIT_CARD_NAME_FULL))
+          .AppendSimpleField(std::u16string())
+          .Build());
+}
+
+TEST_F(CreditCardAccessoryControllerTestWithoutSupportingUnmaskedCards,
+       RefreshSuggestionsUnmaskedCachedCard) {
+  // Store a full server card in the credit_card_access_manager's
+  // unmasked_cards_cache.
+  autofill::CreditCard card = test::GetCreditCard();
+  card.set_record_type(CreditCard::FULL_SERVER_CARD);
+  data_manager_.AddCreditCard(card);
+  std::u16string cvc = u"123";
+  af_manager_.credit_card_access_manager()->CacheUnmaskedCardInfo(card, cvc);
+  autofill::AccessorySheetData result(autofill::AccessoryTabType::CREDIT_CARDS,
+                                      std::u16string());
+
+  EXPECT_CALL(mock_mf_controller_, RefreshSuggestions(_))
+      .WillOnce(SaveArg<0>(&result));
+  ASSERT_TRUE(controller());
+  controller()->RefreshSuggestions();
+
+  EXPECT_EQ(result, controller()->GetSheetData());
+  // Since the experiment is disabled, verify that the only the obfuscated last
+  // four and no cvc is added to the accessory sheet data.
+  EXPECT_EQ(
+      result,
+      CreditCardAccessorySheetDataBuilder()
+          .AddUserInfo(kVisaCard)
+          .AppendField(card.ObfuscatedLastFourDigits(),
+                       card.ObfuscatedLastFourDigits(), card.guid(),
+                       /*is_obfuscated=*/false,
+                       /*selectable=*/true)
+          .AppendSimpleField(card.Expiration2DigitMonthAsString())
+          .AppendSimpleField(card.Expiration4DigitYearAsString())
+          .AppendSimpleField(card.GetRawInfo(autofill::CREDIT_CARD_NAME_FULL))
+          .AppendSimpleField(std::u16string())
+          .Build());
+}
+
 }  // namespace autofill
diff --git a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
index 9d05f5f..4f322dce5 100644
--- a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
+++ b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
@@ -1443,7 +1443,8 @@
 TEST_F(ChromeBrowsingDataRemoverDelegateTest, RemovePersistentIsolatedOrigins) {
   PrefService* prefs = GetProfile()->GetPrefs();
 
-  // Add foo.com to the list of stored isolated origins.
+  // Add foo.com to the list of stored user-triggered isolated origins and
+  // bar.com to the list of stored web-triggered isolated origins.
   base::ListValue list;
   list.AppendString("http://foo.com");
   prefs->Set(site_isolation::prefs::kUserTriggeredIsolatedOrigins, list);
@@ -1451,6 +1452,12 @@
       prefs->GetList(site_isolation::prefs::kUserTriggeredIsolatedOrigins)
           ->GetList()
           .empty());
+  base::DictionaryValue dict;
+  dict.SetKey("https://bar.com", util::TimeToValue(base::Time::Now()));
+  prefs->Set(site_isolation::prefs::kWebTriggeredIsolatedOrigins, dict);
+  EXPECT_FALSE(
+      prefs->GetDictionary(site_isolation::prefs::kWebTriggeredIsolatedOrigins)
+          ->empty());
 
   // Clear history and ensure the stored isolated origins are cleared.
   BlockUntilBrowsingDataRemoved(base::Time(), base::Time::Max(),
@@ -1459,13 +1466,20 @@
       prefs->GetList(site_isolation::prefs::kUserTriggeredIsolatedOrigins)
           ->GetList()
           .empty());
+  EXPECT_TRUE(
+      prefs->GetDictionary(site_isolation::prefs::kWebTriggeredIsolatedOrigins)
+          ->empty());
 
-  // Re-add foo.com to stored isolated origins.
+  // Re-add foo.com and bar.com to stored isolated origins.
   prefs->Set(site_isolation::prefs::kUserTriggeredIsolatedOrigins, list);
   EXPECT_FALSE(
       prefs->GetList(site_isolation::prefs::kUserTriggeredIsolatedOrigins)
           ->GetList()
           .empty());
+  prefs->Set(site_isolation::prefs::kWebTriggeredIsolatedOrigins, dict);
+  EXPECT_FALSE(
+      prefs->GetDictionary(site_isolation::prefs::kWebTriggeredIsolatedOrigins)
+          ->empty());
 
   // Now clear cookies and other site data, and ensure foo.com is cleared.
   // Note that this uses a short time period to document that time ranges are
@@ -1476,13 +1490,20 @@
       prefs->GetList(site_isolation::prefs::kUserTriggeredIsolatedOrigins)
           ->GetList()
           .empty());
+  EXPECT_TRUE(
+      prefs->GetDictionary(site_isolation::prefs::kWebTriggeredIsolatedOrigins)
+          ->empty());
 
-  // Re-add foo.com.
+  // Re-add foo.com and bar.com.
   prefs->Set(site_isolation::prefs::kUserTriggeredIsolatedOrigins, list);
   EXPECT_FALSE(
       prefs->GetList(site_isolation::prefs::kUserTriggeredIsolatedOrigins)
           ->GetList()
           .empty());
+  prefs->Set(site_isolation::prefs::kWebTriggeredIsolatedOrigins, dict);
+  EXPECT_FALSE(
+      prefs->GetDictionary(site_isolation::prefs::kWebTriggeredIsolatedOrigins)
+          ->empty());
 
   // Clear the isolated origins data type.
   BlockUntilBrowsingDataRemoved(base::Time(), base::Time::Max(),
@@ -1491,13 +1512,20 @@
       prefs->GetList(site_isolation::prefs::kUserTriggeredIsolatedOrigins)
           ->GetList()
           .empty());
+  EXPECT_TRUE(
+      prefs->GetDictionary(site_isolation::prefs::kWebTriggeredIsolatedOrigins)
+          ->empty());
 
-  // Re-add foo.com.
+  // Re-add foo.com and bar.com.
   prefs->Set(site_isolation::prefs::kUserTriggeredIsolatedOrigins, list);
   EXPECT_FALSE(
       prefs->GetList(site_isolation::prefs::kUserTriggeredIsolatedOrigins)
           ->GetList()
           .empty());
+  prefs->Set(site_isolation::prefs::kWebTriggeredIsolatedOrigins, dict);
+  EXPECT_FALSE(
+      prefs->GetDictionary(site_isolation::prefs::kWebTriggeredIsolatedOrigins)
+          ->empty());
 
   // Clear both history and site data, and ensure the stored isolated origins
   // are cleared.
@@ -1508,6 +1536,9 @@
       prefs->GetList(site_isolation::prefs::kUserTriggeredIsolatedOrigins)
           ->GetList()
           .empty());
+  EXPECT_TRUE(
+      prefs->GetDictionary(site_isolation::prefs::kWebTriggeredIsolatedOrigins)
+          ->empty());
 }
 
 // Test that clearing history deletes favicons not associated with bookmarks.
diff --git a/chrome/browser/cart/cart_discount_link_fetcher.cc b/chrome/browser/cart/cart_discount_link_fetcher.cc
index 18dbfbd7..bc3a451 100644
--- a/chrome/browser/cart/cart_discount_link_fetcher.cc
+++ b/chrome/browser/cart/cart_discount_link_fetcher.cc
@@ -25,19 +25,19 @@
 const int64_t kTimeoutMs = 30000;
 }  // namespace
 
+CartDiscountLinkFetcher::~CartDiscountLinkFetcher() = default;
+
 void CartDiscountLinkFetcher::Fetch(
     std::unique_ptr<network::PendingSharedURLLoaderFactory> pending_factory,
     cart_db::ChromeCartContentProto cart_content_proto,
     CartDiscountLinkFetcherCallback callback) {
-  std::string default_url = cart_content_proto.merchant_cart_url();
 
   auto fetcher = CreateEndpointFetcher(std::move(pending_factory),
                                        std::move(cart_content_proto));
 
   auto* const fetcher_ptr = fetcher.get();
   fetcher_ptr->PerformRequest(
-      base::BindOnce(&OnLinkFetched, std::move(fetcher), std::move(callback),
-                     std::move(default_url)),
+      base::BindOnce(&OnLinkFetched, std::move(fetcher), std::move(callback)),
       nullptr);
 }
 
@@ -146,25 +146,21 @@
 void CartDiscountLinkFetcher::OnLinkFetched(
     std::unique_ptr<EndpointFetcher> endpoint_fetcher,
     CartDiscountLinkFetcherCallback callback,
-    std::string default_url,
     std::unique_ptr<EndpointResponse> responses) {
   VLOG(2) << "Response: " << responses->response;
   DCHECK(responses) << "responses should not be null";
   if (!responses) {
-    std::move(callback).Run(std::move(default_url));
+    std::move(callback).Run(GURL());
     return;
   }
 
   absl::optional<base::Value> value =
       base::JSONReader::Read(responses->response);
 
-  if (!value || !value->is_dict()) {
+  if (!value || !value->is_dict() || !value->FindKey("url")) {
     NOTREACHED() << "empty response or wrong format";
-    std::move(callback).Run(std::move(default_url));
+    std::move(callback).Run(GURL());
     return;
   }
-
-  std::string url = value->FindKey("url")->GetString();
-
-  std::move(callback).Run(std::move(url));
+  std::move(callback).Run(GURL(value->FindKey("url")->GetString()));
 }
diff --git a/chrome/browser/cart/cart_discount_link_fetcher.h b/chrome/browser/cart/cart_discount_link_fetcher.h
index 7dc20d1..6cd45c71 100644
--- a/chrome/browser/cart/cart_discount_link_fetcher.h
+++ b/chrome/browser/cart/cart_discount_link_fetcher.h
@@ -15,10 +15,12 @@
 // This is used to get an encrypted discount link.
 class CartDiscountLinkFetcher {
  public:
-  using CartDiscountLinkFetcherCallback = base::OnceCallback<void(std::string)>;
+  using CartDiscountLinkFetcherCallback = base::OnceCallback<void(const GURL&)>;
+
+  virtual ~CartDiscountLinkFetcher();
 
   // Fetches the encrypted link for the given |cart_content_proto|.
-  static void Fetch(
+  virtual void Fetch(
       std::unique_ptr<network::PendingSharedURLLoaderFactory> pending_factory,
       cart_db::ChromeCartContentProto cart_content_proto,
       CartDiscountLinkFetcherCallback callback);
@@ -34,7 +36,6 @@
       cart_db::ChromeCartContentProto cart_content_proto);
   static void OnLinkFetched(std::unique_ptr<EndpointFetcher> endpoint_fetcher,
                             CartDiscountLinkFetcherCallback callback,
-                            std::string default_url,
                             std::unique_ptr<EndpointResponse> responses);
 };
 
diff --git a/chrome/browser/cart/cart_service.cc b/chrome/browser/cart/cart_service.cc
index e5610ee9..c16dea46 100644
--- a/chrome/browser/cart/cart_service.cc
+++ b/chrome/browser/cart/cart_service.cc
@@ -16,11 +16,13 @@
 #include "chrome/grit/browser_resources.h"
 #include "chrome/grit/generated_resources.h"
 #include "components/prefs/pref_service.h"
+#include "components/prefs/scoped_user_pref_update.h"
 #include "components/search/ntp_features.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/storage_partition.h"
 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/re2/src/re2/re2.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/base/resource/resource_bundle.h"
@@ -92,7 +94,8 @@
           ServiceAccessType::EXPLICIT_ACCESS)),
       domain_name_mapping_(JSONToDictionary(IDR_CART_DOMAIN_NAME_MAPPING_JSON)),
       domain_cart_url_mapping_(
-          JSONToDictionary(IDR_CART_DOMAIN_CART_URL_MAPPING_JSON)) {
+          JSONToDictionary(IDR_CART_DOMAIN_CART_URL_MAPPING_JSON)),
+      discount_link_fetcher_(std::make_unique<CartDiscountLinkFetcher>()) {
   if (history_service_) {
     history_service_observation_.Observe(history_service_);
   }
@@ -117,6 +120,7 @@
   registry->RegisterIntegerPref(prefs::kCartModuleWelcomeSurfaceShownTimes, 0);
   registry->RegisterBooleanPref(prefs::kCartDiscountAcknowledged, false);
   registry->RegisterBooleanPref(prefs::kCartDiscountEnabled, false);
+  registry->RegisterDictionaryPref(prefs::kCartUsedDiscounts);
 }
 
 void CartService::Hide() {
@@ -293,8 +297,53 @@
 void CartService::GetDiscountURL(
     const GURL& cart_url,
     base::OnceCallback<void(const ::GURL&)> callback) {
-  // TODO(crbug.com/1204146): Add logic here to fetch discount URL from service.
-  std::move(callback).Run(cart_url);
+  if (!IsPartnerMerchant(cart_url) || !IsCartDiscountEnabled()) {
+    std::move(callback).Run(cart_url);
+    return;
+  }
+  LoadCart(eTLDPlusOne(cart_url),
+           base::BindOnce(&CartService::OnGetDiscountURL,
+                          weak_ptr_factory_.GetWeakPtr(), cart_url,
+                          std::move(callback)));
+}
+
+void CartService::OnGetDiscountURL(
+    const GURL& default_cart_url,
+    base::OnceCallback<void(const ::GURL&)> callback,
+    bool success,
+    std::vector<CartDB::KeyAndValue> proto_pairs) {
+  DCHECK_EQ(proto_pairs.size(), 1U);
+  if (proto_pairs.size() != 1U) {
+    std::move(callback).Run(default_cart_url);
+    return;
+  }
+  auto& cart_proto = proto_pairs[0].second;
+  if (cart_proto.discount_info().discount_info().empty()) {
+    std::move(callback).Run(default_cart_url);
+    return;
+  }
+  auto pending_factory = profile_->GetDefaultStoragePartition()
+                             ->GetURLLoaderFactoryForBrowserProcess()
+                             ->Clone();
+
+  discount_link_fetcher_->Fetch(
+      std::move(pending_factory), cart_proto,
+      base::BindOnce(&CartService::OnDiscountURLFetched,
+                     weak_ptr_factory_.GetWeakPtr(), default_cart_url,
+                     std::move(callback), cart_proto));
+}
+
+void CartService::OnDiscountURLFetched(
+    const GURL& default_cart_url,
+    base::OnceCallback<void(const ::GURL&)> callback,
+    const cart_db::ChromeCartContentProto& cart_proto,
+    const GURL& discount_url) {
+  std::move(callback).Run(discount_url.is_valid() ? discount_url
+                                                  : default_cart_url);
+  if (discount_url.is_valid()) {
+    CacheUsedDiscounts(cart_proto);
+    CleanUpDiscounts(cart_proto);
+  }
 }
 
 void CartService::LoadCartsWithFakeData(CartDB::LoadCallback callback) {
@@ -580,53 +629,34 @@
                                    weak_ptr_factory_.GetWeakPtr()));
 }
 
-void CartService::UpdateDiscounts(
-    const std::string& domain,
-    const double timestamp,
-    const std::vector<cart_db::DiscountInfoProto> discount_infos) {
-  auto update_discounts_callback = base::BindOnce(
-      &CartService::OnUpdateDiscount, weak_ptr_factory_.GetWeakPtr(), domain,
-      std::move(discount_infos), timestamp);
-
-  cart_db_->LoadCart(domain, std::move(update_discounts_callback));
-}
-
-void CartService::OnUpdateDiscount(
-    const std::string& domain,
-    const std::vector<cart_db::DiscountInfoProto> discount_infos,
-    const double timestamp,
-    bool success,
-    std::vector<CartDB::KeyAndValue> proto_pairs) {
-  if (!success || proto_pairs.size() == 0) {
+void CartService::UpdateDiscounts(const GURL& cart_url,
+                                  cart_db::ChromeCartContentProto new_proto) {
+  if (!cart_url.is_valid()) {
+    VLOG(1) << __func__
+            << "update discounts with invalid cart_url: " << cart_url;
     return;
   }
 
-  DCHECK_EQ(1U, proto_pairs.size());
-
-  auto cart_proto = proto_pairs[0].second;
-
-  cart_proto.mutable_discount_info()->set_last_fetched_timestamp(timestamp);
-
-  if (discount_infos.empty()) {
-    cart_proto.mutable_discount_info()->clear_discount_info();
-  } else {
-    for (cart_db::DiscountInfoProto discount_info : discount_infos) {
-      cart_db::DiscountInfoProto* added_discount =
-          cart_proto.mutable_discount_info()->add_discount_info();
-
-      added_discount->set_rule_id(discount_info.rule_id());
-      if (discount_info.has_amount_off()) {
-        added_discount->set_allocated_amount_off(
-            discount_info.release_amount_off());
-      } else {
-        added_discount->set_percent_off(discount_info.percent_off());
+  if (new_proto.has_discount_info() &&
+      !new_proto.discount_info().discount_info().empty()) {
+    // Filter used discounts.
+    std::vector<cart_db::DiscountInfoProto> discount_info_protos;
+    for (const cart_db::DiscountInfoProto& proto :
+         new_proto.discount_info().discount_info()) {
+      if (!IsDiscountUsed(proto.rule_id())) {
+        discount_info_protos.emplace_back(proto);
       }
-      added_discount->set_raw_merchant_offer_id(
-          discount_info.raw_merchant_offer_id());
+    }
+    if (discount_info_protos.empty()) {
+      new_proto.clear_discount_info();
+    } else {
+      *new_proto.mutable_discount_info()->mutable_discount_info() = {
+          discount_info_protos.begin(), discount_info_protos.end()};
     }
   }
 
-  cart_db_->AddCart(domain, std::move(cart_proto),
+  std::string domain = eTLDPlusOne(cart_url);
+  cart_db_->AddCart(domain, std::move(new_proto),
                     base::BindOnce(&CartService::OnOperationFinished,
                                    weak_ptr_factory_.GetWeakPtr()));
 }
@@ -647,3 +677,43 @@
   fetch_discount_worker_->Start(
       base::TimeDelta::FromMilliseconds(kDelayStartMs));
 }
+
+bool CartService::IsDiscountUsed(const std::string& rule_id) {
+  return profile_->GetPrefs()
+             ->GetDictionary(prefs::kCartUsedDiscounts)
+             ->FindBoolKey(rule_id) != absl::nullopt;
+}
+
+void CartService::CacheUsedDiscounts(
+    const cart_db::ChromeCartContentProto& proto) {
+  if (!proto.has_discount_info() ||
+      proto.discount_info().discount_info().empty()) {
+    NOTREACHED() << "Empty discounts";
+    return;
+  }
+  DictionaryPrefUpdate update(profile_->GetPrefs(), prefs::kCartUsedDiscounts);
+  for (auto discount_info : proto.discount_info().discount_info()) {
+    update->SetBoolKey(discount_info.rule_id(), true);
+  }
+}
+
+void CartService::CleanUpDiscounts(cart_db::ChromeCartContentProto proto) {
+  if (proto.merchant_cart_url().empty()) {
+    NOTREACHED() << "proto does not have merchant_cart_url";
+    return;
+  }
+  if (!proto.has_discount_info()) {
+    NOTREACHED() << "proto does not have discount_info";
+    return;
+  }
+
+  proto.clear_discount_info();
+  cart_db_->AddCart(eTLDPlusOne(GURL(proto.merchant_cart_url())), proto,
+                    base::BindOnce(&CartService::OnOperationFinished,
+                                   weak_ptr_factory_.GetWeakPtr()));
+}
+
+void CartService::SetCartDiscountLinkFetcherForTesting(
+    std::unique_ptr<CartDiscountLinkFetcher> discount_link_fetcher) {
+  discount_link_fetcher_ = std::move(discount_link_fetcher);
+}
diff --git a/chrome/browser/cart/cart_service.h b/chrome/browser/cart/cart_service.h
index b01c569a..5038cbc 100644
--- a/chrome/browser/cart/cart_service.h
+++ b/chrome/browser/cart/cart_service.h
@@ -10,6 +10,7 @@
 #include "base/values.h"
 #include "chrome/browser/cart/cart_db.h"
 #include "chrome/browser/cart/cart_db_content.pb.h"
+#include "chrome/browser/cart/cart_discount_link_fetcher.h"
 #include "chrome/browser/cart/cart_service_factory.h"
 #include "chrome/browser/profiles/profile.h"
 #include "components/history/core/browser/history_service.h"
@@ -54,11 +55,9 @@
   void DeleteCart(const std::string& domain);
   // Only load carts with fake data in the database.
   void LoadCartsWithFakeData(CartDB::LoadCallback callback);
-  // Gets called when discounts are available for the given domain.
-  void UpdateDiscounts(
-      const std::string& domain,
-      const double timestamp,
-      const std::vector<cart_db::DiscountInfoProto> discount_infos);
+  // Gets called when discounts are available for the given cart_url.
+  void UpdateDiscounts(const GURL& cart_url,
+                       cart_db::ChromeCartContentProto new_proto);
   // Gets called when a single cart in module is temporarily hidden.
   void HideCart(const GURL& cart_url, CartDB::OperationCallback callback);
   // Gets called when restoring the temporarily hidden single cart.
@@ -95,12 +94,15 @@
   // history::HistoryServiceObserver:
   void OnURLsDeleted(history::HistoryService* history_service,
                      const history::DeletionInfo& deletion_info) override;
+  // Returns whether a discount with |rule_id| is used or not.
+  bool IsDiscountUsed(const std::string& rule_id);
   // KeyedService:
   void Shutdown() override;
 
  private:
   friend class CartServiceFactory;
   friend class CartServiceTest;
+  friend class CartServiceDiscountTest;
   FRIEND_TEST_ALL_PREFIXES(CartHandlerNtpModuleFakeDataTest,
                            TestEnableFakeData);
 
@@ -141,20 +143,29 @@
                  bool success,
                  std::vector<CartDB::KeyAndValue> proto_pairs);
 
-  // A callback to handle updating discount for a cart.
-  void OnUpdateDiscount(
-      const std::string& domain,
-      const std::vector<cart_db::DiscountInfoProto> discount_infos,
-      const double timestamp,
-      bool success,
-      std::vector<CartDB::KeyAndValue> proto_pairs);
   // Gets called when users has enabled the rule-based discount feature.
   void StartGettingDiscount();
+  // A callback to fetch discount URL.
+  void OnGetDiscountURL(const GURL& default_cart_url,
+                        base::OnceCallback<void(const ::GURL&)> callback,
+                        bool success,
+                        std::vector<CartDB::KeyAndValue> proto_pairs);
+  // A callback to return discount URL when it is fetched.
+  void OnDiscountURLFetched(const GURL& default_cart_url,
+                            base::OnceCallback<void(const ::GURL&)> callback,
+                            const cart_db::ChromeCartContentProto& cart_proto,
+                            const GURL& discount_url);
 
   // A callback to decide if there are partner carts.
   void HasPartnerCarts(base::OnceCallback<void(bool)> callback,
                        bool success,
                        std::vector<CartDB::KeyAndValue> proto_pairs);
+  // Set discount_link_fetcher_ for testing purpose.
+  void SetCartDiscountLinkFetcherForTesting(
+      std::unique_ptr<CartDiscountLinkFetcher> discount_link_fetcher);
+
+  void CacheUsedDiscounts(const cart_db::ChromeCartContentProto& proto);
+  void CleanUpDiscounts(cart_db::ChromeCartContentProto proto);
 
   Profile* profile_;
   std::unique_ptr<CartDB> cart_db_;
@@ -164,6 +175,7 @@
   absl::optional<base::Value> domain_name_mapping_;
   absl::optional<base::Value> domain_cart_url_mapping_;
   std::unique_ptr<FetchDiscountWorker> fetch_discount_worker_;
+  std::unique_ptr<CartDiscountLinkFetcher> discount_link_fetcher_;
   base::WeakPtrFactory<CartService> weak_ptr_factory_{this};
 };
 
diff --git a/chrome/browser/cart/cart_service_unittest.cc b/chrome/browser/cart/cart_service_unittest.cc
index cd7ff290..b3c2387 100644
--- a/chrome/browser/cart/cart_service_unittest.cc
+++ b/chrome/browser/cart/cart_service_unittest.cc
@@ -238,6 +238,13 @@
     std::move(closure).Run();
   }
 
+  void GetEvaluationDiscountURL(base::OnceClosure closure,
+                                const GURL& expected,
+                                const GURL& found) {
+    EXPECT_EQ(expected, found);
+    std::move(closure).Run();
+  }
+
   std::string getDomainName(base::StringPiece domain) {
     std::string* res = service_->domain_name_mapping_->FindStringKey(domain);
     if (!res)
@@ -253,7 +260,18 @@
     return *res;
   }
 
-  void TearDown() override {}
+  void CacheUsedDiscounts(const cart_db::ChromeCartContentProto& proto) {
+    service_->CacheUsedDiscounts(proto);
+  }
+
+  void CleanUpDiscounts(const cart_db::ChromeCartContentProto& proto) {
+    service_->CleanUpDiscounts(proto);
+  }
+
+  void TearDown() override {
+    // Clean up the used discounts dictionary prefs.
+    profile_.GetPrefs()->ClearPref(prefs::kCartUsedDiscounts);
+  }
 
  protected:
   // This needs to be destroyed after task_environment, so that any tasks on
@@ -301,7 +319,7 @@
   cart_db::ChromeCartContentProto proto =
       BuildProto(kMockMerchantA, kMockMerchantURLA);
 
-  base::RunLoop run_loop[3];
+  base::RunLoop run_loop[4];
   cart_db->AddCart(
       kMockMerchantA, proto,
       base::BindOnce(&CartServiceTest::OperationEvaluation,
@@ -316,20 +334,28 @@
 
   const double timestamp = 1;
 
-  service_->UpdateDiscounts(kMockMerchantA, timestamp, kMockMerchantADiscounts);
-  task_environment_.RunUntilIdle();
+  cart_db::ChromeCartContentProto cart_with_discount_proto =
+      AddDiscountToProto(proto, timestamp, kMockMerchantADiscountRuleId,
+                         kMockMerchantADiscountsPercentOff,
+                         kMockMerchantADiscountsRawMerchantOfferId);
 
-  const ShoppingCarts expected = {
-      {kMockMerchantA,
-       AddDiscountToProto(proto, timestamp, kMockMerchantADiscountRuleId,
-                          kMockMerchantADiscountsPercentOff,
-                          kMockMerchantADiscountsRawMerchantOfferId)}};
+  service_->UpdateDiscounts(GURL(kMockMerchantURLA), cart_with_discount_proto);
+
+  const ShoppingCarts expected = {{kMockMerchantA, cart_with_discount_proto}};
 
   cart_db->LoadCart(kMockMerchantA,
                     base::BindOnce(&CartServiceTest::GetEvaluationDiscount,
                                    base::Unretained(this),
                                    run_loop[2].QuitClosure(), expected));
   run_loop[2].Run();
+
+  CacheUsedDiscounts(cart_with_discount_proto);
+  service_->UpdateDiscounts(GURL(kMockMerchantURLA), cart_with_discount_proto);
+  cart_db->LoadCart(
+      kMockMerchantA,
+      base::BindOnce(&CartServiceTest::GetEvaluationEmptyDiscount,
+                     base::Unretained(this), run_loop[3].QuitClosure()));
+  run_loop[3].Run();
 }
 
 // Test adding a cart with the same key and no product image won't overwrite
@@ -939,6 +965,50 @@
   EXPECT_EQ(GetCartURL(amazon_domain), amazon_cart.spec());
 }
 
+TEST_F(CartServiceTest, TestCacheUsedDiscounts) {
+  EXPECT_FALSE(service_->IsDiscountUsed(kMockMerchantADiscountRuleId));
+
+  cart_db::ChromeCartContentProto cart_with_discount_proto = AddDiscountToProto(
+      BuildProto(kMockMerchantA, kMockMerchantURLA), 1,
+      kMockMerchantADiscountRuleId, kMockMerchantADiscountsPercentOff,
+      kMockMerchantADiscountsRawMerchantOfferId);
+
+  CacheUsedDiscounts(cart_with_discount_proto);
+  EXPECT_TRUE(service_->IsDiscountUsed(kMockMerchantADiscountRuleId));
+}
+
+TEST_F(CartServiceTest, TestCleanUpDiscounts) {
+  cart_db::ChromeCartContentProto cart_with_discount_proto = AddDiscountToProto(
+      BuildProto(kMockMerchantA, kMockMerchantURLA), 1,
+      kMockMerchantADiscountRuleId, kMockMerchantADiscountsPercentOff,
+      kMockMerchantADiscountsRawMerchantOfferId);
+  const ShoppingCarts has_discount_cart = {
+      {kMockMerchantA, cart_with_discount_proto}};
+  CartDB* cart_db = service_->GetDB();
+
+  base::RunLoop run_loop[3];
+  cart_db->AddCart(
+      kMockMerchantA, cart_with_discount_proto,
+      base::BindOnce(&CartServiceTest::OperationEvaluation,
+                     base::Unretained(this), run_loop[0].QuitClosure(), true));
+  run_loop[0].Run();
+
+  cart_db->LoadCart(
+      kMockMerchantA,
+      base::BindOnce(&CartServiceTest::GetEvaluationDiscount,
+                     base::Unretained(this), run_loop[1].QuitClosure(),
+                     has_discount_cart));
+  run_loop[1].Run();
+
+  CleanUpDiscounts(cart_with_discount_proto);
+
+  cart_db->LoadCart(
+      kMockMerchantA,
+      base::BindOnce(&CartServiceTest::GetEvaluationEmptyDiscount,
+                     base::Unretained(this), run_loop[2].QuitClosure()));
+  run_loop[2].Run();
+}
+
 class CartServiceFakeDataTest : public CartServiceTest {
  public:
   // Features need to be initialized before CartServiceTest::SetUp runs, in
@@ -1071,6 +1141,29 @@
       profile_.GetPrefs()->GetBoolean(prefs::kCartDiscountAcknowledged));
 }
 
+class MockCartDiscountLinkFetcher : public CartDiscountLinkFetcher {
+ public:
+  MOCK_METHOD(
+      void,
+      Fetch,
+      (std::unique_ptr<network::PendingSharedURLLoaderFactory> pending_factory,
+       cart_db::ChromeCartContentProto cart_content_proto,
+       CartDiscountLinkFetcherCallback callback),
+      (override));
+
+  void SetDiscountURL(const GURL& discount_url) {
+    ON_CALL(*this, Fetch)
+        .WillByDefault(
+            [discount_url](
+                std::unique_ptr<network::PendingSharedURLLoaderFactory>
+                    pending_factory,
+                cart_db::ChromeCartContentProto cart_content_proto,
+                CartDiscountLinkFetcherCallback callback) {
+              return std::move(callback).Run(discount_url);
+            });
+  }
+};
+
 class CartServiceDiscountTest : public CartServiceTest {
  public:
   // Features need to be initialized before CartServiceTest::SetUp runs, in
@@ -1088,6 +1181,24 @@
     // Add a partner merchant cart.
     service_->AddCart(kMockMerchantA, absl::nullopt, kMockProtoA);
     task_environment_.RunUntilIdle();
+    // The feature is enabled for this test class.
+    profile_.GetPrefs()->SetBoolean(prefs::kCartDiscountEnabled, true);
+  }
+
+  void TearDown() override {
+    // Set the feature to default disabled state after test.
+    profile_.GetPrefs()->SetBoolean(prefs::kCartDiscountEnabled, false);
+  }
+
+  void SetCartDiscountURLForTesting(const GURL& discount_url,
+                                    bool expect_call) {
+    std::unique_ptr<MockCartDiscountLinkFetcher> mock_fetcher =
+        std::make_unique<MockCartDiscountLinkFetcher>();
+    mock_fetcher->SetDiscountURL(discount_url);
+    if (expect_call) {
+      EXPECT_CALL(*mock_fetcher, Fetch);
+    }
+    service_->SetCartDiscountLinkFetcherForTesting(std::move(mock_fetcher));
   }
 };
 
@@ -1151,9 +1262,128 @@
 
 // Tests updating whether rule-based discount is enabled in profile prefs.
 TEST_F(CartServiceDiscountTest, TestSetCartDiscountEnabled) {
-  ASSERT_FALSE(profile_.GetPrefs()->GetBoolean(prefs::kCartDiscountEnabled));
-  service_->SetCartDiscountEnabled(true);
   ASSERT_TRUE(profile_.GetPrefs()->GetBoolean(prefs::kCartDiscountEnabled));
   service_->SetCartDiscountEnabled(false);
   ASSERT_FALSE(profile_.GetPrefs()->GetBoolean(prefs::kCartDiscountEnabled));
+  service_->SetCartDiscountEnabled(true);
+  ASSERT_TRUE(profile_.GetPrefs()->GetBoolean(prefs::kCartDiscountEnabled));
+}
+
+// Tests no fetching for discount URL if the cart is not from a partner
+// merchant.
+TEST_F(CartServiceDiscountTest, TestNoFetchForNonPartner) {
+  base::RunLoop run_loop[2];
+  const double timestamp = 1;
+  SetCartDiscountURLForTesting(GURL("https://www.discount.com"), false);
+  cart_db::ChromeCartContentProto cart_proto = AddDiscountToProto(
+      BuildProto(kMockMerchantB, kMockMerchantURLB), timestamp,
+      kMockMerchantADiscountRuleId, kMockMerchantADiscountsPercentOff,
+      kMockMerchantADiscountsRawMerchantOfferId);
+  service_->GetDB()->AddCart(
+      kMockMerchantB, cart_proto,
+      base::BindOnce(&CartServiceTest::OperationEvaluation,
+                     base::Unretained(this), run_loop[0].QuitClosure(), true));
+  run_loop[0].Run();
+
+  GURL default_cart_url(kMockMerchantURLB);
+  service_->GetDiscountURL(
+      default_cart_url,
+      base::BindOnce(&CartServiceTest::GetEvaluationDiscountURL,
+                     base::Unretained(this), run_loop[1].QuitClosure(),
+                     default_cart_url));
+  run_loop[1].Run();
+}
+
+// Tests no fetching for discount URL if the cart doesn't have discount info.
+TEST_F(CartServiceDiscountTest, TestNoFetchWhenNoDiscount) {
+  base::RunLoop run_loop[2];
+  SetCartDiscountURLForTesting(GURL("https://www.discount.com"), false);
+  service_->LoadCart(
+      kMockMerchantA,
+      base::BindOnce(&CartServiceTest::GetEvaluationEmptyDiscount,
+                     base::Unretained(this), run_loop[0].QuitClosure()));
+  run_loop[0].Run();
+
+  GURL default_cart_url(kMockMerchantURLA);
+  service_->GetDiscountURL(
+      default_cart_url,
+      base::BindOnce(&CartServiceTest::GetEvaluationDiscountURL,
+                     base::Unretained(this), run_loop[1].QuitClosure(),
+                     default_cart_url));
+  run_loop[1].Run();
+}
+
+// Tests no fetching for discount URL if the feature is disabled.
+TEST_F(CartServiceDiscountTest, TestNoFetchWhenFeatureDisabled) {
+  base::RunLoop run_loop[2];
+  const double timestamp = 1;
+  GURL discount_url("https://www.discount.com");
+  SetCartDiscountURLForTesting(discount_url, false);
+  profile_.GetPrefs()->SetBoolean(prefs::kCartDiscountEnabled, false);
+  cart_db::ChromeCartContentProto cart_proto = AddDiscountToProto(
+      BuildProto(kMockMerchantA, kMockMerchantURLA), timestamp,
+      kMockMerchantADiscountRuleId, kMockMerchantADiscountsPercentOff,
+      kMockMerchantADiscountsRawMerchantOfferId);
+  service_->GetDB()->AddCart(
+      kMockMerchantA, cart_proto,
+      base::BindOnce(&CartServiceTest::OperationEvaluation,
+                     base::Unretained(this), run_loop[0].QuitClosure(), true));
+  run_loop[0].Run();
+
+  GURL default_cart_url(kMockMerchantURLA);
+  service_->GetDiscountURL(
+      default_cart_url,
+      base::BindOnce(&CartServiceTest::GetEvaluationDiscountURL,
+                     base::Unretained(this), run_loop[1].QuitClosure(),
+                     default_cart_url));
+  run_loop[1].Run();
+}
+
+// Tests CartService returning fetched discount URL.
+TEST_F(CartServiceDiscountTest, TestReturnDiscountURL) {
+  base::RunLoop run_loop[2];
+  const double timestamp = 1;
+  GURL discount_url("https://www.discount.com");
+  SetCartDiscountURLForTesting(discount_url, true);
+  cart_db::ChromeCartContentProto cart_proto = AddDiscountToProto(
+      BuildProto(kMockMerchantA, kMockMerchantURLA), timestamp,
+      kMockMerchantADiscountRuleId, kMockMerchantADiscountsPercentOff,
+      kMockMerchantADiscountsRawMerchantOfferId);
+  service_->GetDB()->AddCart(
+      kMockMerchantA, cart_proto,
+      base::BindOnce(&CartServiceTest::OperationEvaluation,
+                     base::Unretained(this), run_loop[0].QuitClosure(), true));
+  run_loop[0].Run();
+
+  service_->GetDiscountURL(
+      GURL(kMockMerchantURLA),
+      base::BindOnce(&CartServiceTest::GetEvaluationDiscountURL,
+                     base::Unretained(this), run_loop[1].QuitClosure(),
+                     discount_url));
+  run_loop[1].Run();
+}
+
+// Tests CartService returning original cart URL as a fallback if the fetch
+// response is invalid.
+TEST_F(CartServiceDiscountTest, TestFetchInvalidFallback) {
+  base::RunLoop run_loop[2];
+  const double timestamp = 1;
+  SetCartDiscountURLForTesting(GURL("error"), true);
+  cart_db::ChromeCartContentProto cart_proto = AddDiscountToProto(
+      BuildProto(kMockMerchantA, kMockMerchantURLA), timestamp,
+      kMockMerchantADiscountRuleId, kMockMerchantADiscountsPercentOff,
+      kMockMerchantADiscountsRawMerchantOfferId);
+  service_->GetDB()->AddCart(
+      kMockMerchantA, cart_proto,
+      base::BindOnce(&CartServiceTest::OperationEvaluation,
+                     base::Unretained(this), run_loop[0].QuitClosure(), true));
+  run_loop[0].Run();
+
+  GURL default_cart_url(kMockMerchantURLA);
+  service_->GetDiscountURL(
+      default_cart_url,
+      base::BindOnce(&CartServiceTest::GetEvaluationDiscountURL,
+                     base::Unretained(this), run_loop[1].QuitClosure(),
+                     default_cart_url));
+  run_loop[1].Run();
 }
diff --git a/chrome/browser/cart/commerce_hint_service.cc b/chrome/browser/cart/commerce_hint_service.cc
index 79654161..fb18e7b31 100644
--- a/chrome/browser/cart/commerce_hint_service.cc
+++ b/chrome/browser/cart/commerce_hint_service.cc
@@ -15,15 +15,23 @@
 #include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
 #include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
 #include "chrome/browser/profiles/profile.h"
+#include "components/search/ntp_features.h"
 #include "content/public/browser/frame_service_base.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_contents_user_data.h"
 #include "mojo/public/cpp/bindings/self_owned_receiver.h"
 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
+#include "third_party/re2/src/re2/re2.h"
 
 namespace cart {
 
 namespace {
+// TODO(crbug.com/1207197): Pull below methods to a utility class to share with
+// other classes.
+constexpr base::FeatureParam<std::string> kPartnerMerchantPattern{
+    &ntp_features::kNtpChromeCartModule, "partner-merchant-pattern",
+    // This regex does not match anything.
+    "\\b\\B"};
 
 // TODO(crbug/1164236): support multiple cart systems in the same domain.
 // Returns eTLB+1 domain.
@@ -32,6 +40,21 @@
       url, net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
 }
 
+const re2::RE2& GetPartnerMerchantPattern() {
+  re2::RE2::Options options;
+  options.set_case_sensitive(false);
+  static base::NoDestructor<re2::RE2> instance(kPartnerMerchantPattern.Get(),
+                                               options);
+  return *instance;
+}
+
+bool IsPartnerMerchant(const GURL& url) {
+  const std::string& url_string = url.spec();
+  return RE2::PartialMatch(
+      re2::StringPiece(url_string.data(), url_string.size()),
+      GetPartnerMerchantPattern());
+}
+
 void ConstructCartProto(cart_db::ChromeCartContentProto* proto,
                         const GURL& navigation_url,
                         std::vector<mojom::ProductPtr> products) {
@@ -170,6 +193,12 @@
     DVLOG(1) << "Reject cart URL with different eTLD+1 domain.";
     validated_cart = absl::nullopt;
   }
+  // When rule-based discount is enabled, do not accept cart page URLs from
+  // partner merchants as there could be things like discount tokens in them.
+  if (service_->IsCartDiscountEnabled() && IsPartnerMerchant(navigation_url) &&
+      product_id.empty()) {
+    validated_cart = absl::nullopt;
+  }
   cart_db::ChromeCartContentProto proto;
   std::vector<mojom::ProductPtr> products;
   if (!product_id.empty()) {
@@ -191,9 +220,15 @@
     std::vector<mojom::ProductPtr> products) {
   if (ShouldSkip(cart_url))
     return;
+  absl::optional<GURL> validated_cart = cart_url;
+  // When rule-based discount is enabled, do not accept cart page URLs from
+  // partner merchants as there could be things like discount tokens in them.
+  if (service_->IsCartDiscountEnabled() && IsPartnerMerchant(cart_url)) {
+    validated_cart = absl::nullopt;
+  }
   cart_db::ChromeCartContentProto proto;
   ConstructCartProto(&proto, cart_url, std::move(products));
-  service_->AddCart(proto.key(), cart_url, std::move(proto));
+  service_->AddCart(proto.key(), validated_cart, std::move(proto));
 }
 
 WEB_CONTENTS_USER_DATA_KEY_IMPL(CommerceHintService)
diff --git a/chrome/browser/cart/fetch_discount_worker.cc b/chrome/browser/cart/fetch_discount_worker.cc
index 36929ea5..13b44d9b 100644
--- a/chrome/browser/cart/fetch_discount_worker.cc
+++ b/chrome/browser/cart/fetch_discount_worker.cc
@@ -17,12 +17,6 @@
 // 30 minutes.
 const int64_t kDelayFetchMs = 1800000;
 const int kImmediateFetchMs = 0;
-
-std::string eTLDPlusOne(const GURL& url) {
-  return net::registry_controlled_domains::GetDomainAndRegistry(
-      url, net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
-}
-
 }  // namespace
 
 CartLoader::CartLoader(Profile* profile)
@@ -42,9 +36,8 @@
 void CartDiscountUpdater::update(
     const std::string& cart_url,
     const cart_db::ChromeCartContentProto new_proto) {
-  const GURL url(cart_url);
-  std::string domain = eTLDPlusOne(url);
-  cart_service_->AddCart(domain, url, std::move(new_proto));
+  GURL url(cart_url);
+  cart_service_->UpdateDiscounts(url, std::move(new_proto));
 }
 
 CartLoaderAndUpdaterFactory::CartLoaderAndUpdaterFactory(Profile* profile)
diff --git a/chrome/browser/chrome_browser_interface_binders.cc b/chrome/browser/chrome_browser_interface_binders.cc
index 4020633..e57f43f 100644
--- a/chrome/browser/chrome_browser_interface_binders.cc
+++ b/chrome/browser/chrome_browser_interface_binders.cc
@@ -104,7 +104,7 @@
 #include "third_party/blink/public/mojom/digital_goods/digital_goods.mojom.h"
 #include "third_party/blink/public/mojom/installedapp/installed_app_provider.mojom.h"
 #else
-#include "chrome/browser/accessibility/caption_host_impl.h"
+#include "chrome/browser/accessibility/live_caption_speech_recognition_host.h"
 #include "chrome/browser/badging/badge_manager.h"
 #include "chrome/browser/cart/chrome_cart.mojom.h"
 #include "chrome/browser/cart/commerce_hint_service.h"
@@ -492,7 +492,8 @@
   PrefService* profile_prefs = profile->GetPrefs();
   if (profile_prefs->GetBoolean(prefs::kLiveCaptionEnabled) &&
       media::IsLiveCaptionFeatureEnabled()) {
-    captions::CaptionHostImpl::Create(frame_host, std::move(receiver));
+    captions::LiveCaptionSpeechRecognitionHost::Create(frame_host,
+                                                       std::move(receiver));
   }
 }
 #endif
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index 4b75f87..37412fa 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -1319,6 +1319,8 @@
   registry->RegisterListPref(
       site_isolation::prefs::kUserTriggeredIsolatedOrigins);
   registry->RegisterDictionaryPref(
+      site_isolation::prefs::kWebTriggeredIsolatedOrigins);
+  registry->RegisterDictionaryPref(
       prefs::kDevToolsBackgroundServicesExpirationDict);
   registry->RegisterBooleanPref(prefs::kSignedHTTPExchangeEnabled, true);
 #if !defined(OS_ANDROID)
diff --git a/chrome/browser/chrome_navigation_browsertest.cc b/chrome/browser/chrome_navigation_browsertest.cc
index 096134247..e1f39b7 100644
--- a/chrome/browser/chrome_navigation_browsertest.cc
+++ b/chrome/browser/chrome_navigation_browsertest.cc
@@ -2422,3 +2422,212 @@
                   ->GetProcess()
                   ->IsProcessLockedToSiteForTesting());
 }
+
+// This test class turns on the mode where sites served with
+// Cross-Origin-Opener-Policy headers are site-isolated.  This complements
+// COOPIsolationTest in content_browsertests and focuses on persistence of COOP
+// sites in user prefs, which requires the //chrome layer.
+class SiteIsolationForCOOPBrowserTest : public ChromeNavigationBrowserTest {
+ public:
+  // Use an HTTP server, since the COOP header is only populated for HTTPS.
+  SiteIsolationForCOOPBrowserTest()
+      : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
+    // Enable COOP isolation with a max of 3 stored sites.
+    const std::vector<base::test::ScopedFeatureList::FeatureAndParams>
+        kEnabledFeatures = {
+            {::features::kSiteIsolationForCrossOriginOpenerPolicy,
+             {{"stored_sites_max_size", base::NumberToString(3)},
+              {"should_persist_across_restarts", "true"}}}};
+    // Disable full site isolation so we can observe effects of COOP isolation.
+    const std::vector<base::Feature> kDisabledFeatures = {
+        features::kSitePerProcess};
+    feature_list_.InitWithFeaturesAndParameters(kEnabledFeatures,
+                                                kDisabledFeatures);
+  }
+
+  // Returns the list of COOP sites currently stored in user prefs.
+  std::vector<std::string> GetSavedIsolatedSites(Profile* profile) {
+    PrefService* prefs = profile->GetPrefs();
+    auto* dict = prefs->GetDictionary(
+        site_isolation::prefs::kWebTriggeredIsolatedOrigins);
+    std::vector<std::string> sites;
+    for (const auto& site_time_pair : dict->DictItems())
+      sites.push_back(site_time_pair.first);
+    return sites;
+  }
+
+ protected:
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    ChromeNavigationBrowserTest::SetUpCommandLine(command_line);
+
+    // Allow HTTPS server to be used on sites other than localhost.
+    command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
+  }
+
+  void SetUp() override {
+    https_server_.AddDefaultHandlers(GetChromeTestDataDir());
+    ASSERT_TRUE(https_server_.InitializeAndListen());
+    ChromeNavigationBrowserTest::SetUp();
+  }
+
+  void SetUpOnMainThread() override {
+    https_server_.StartAcceptingConnections();
+    ChromeNavigationBrowserTest::SetUpOnMainThread();
+  }
+
+  net::EmbeddedTestServer* https_server() { return &https_server_; }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+  net::EmbeddedTestServer https_server_;
+};
+
+// Verifies that sites isolated due to COOP headers are persisted across
+// restarts.  Note that persistence requires both visiting the COOP site and
+// interacting with it via a user activation.  Part 1/2.
+IN_PROC_BROWSER_TEST_F(SiteIsolationForCOOPBrowserTest,
+                       PRE_PersistAcrossRestarts) {
+  EXPECT_THAT(GetSavedIsolatedSites(browser()->profile()), IsEmpty());
+
+  content::WebContents* contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  // Navigate to a couple of URLs with COOP and trigger user activation on each
+  // one to add them to the saved list in user prefs.
+  GURL coop_url = https_server()->GetURL(
+      "saved.com", "/set-header?Cross-Origin-Opener-Policy: same-origin");
+  GURL coop_url2 = https_server()->GetURL(
+      "saved2.com", "/set-header?Cross-Origin-Opener-Policy: same-origin");
+  ui_test_utils::NavigateToURL(browser(), coop_url);
+  // Simulate user activation.
+  EXPECT_TRUE(ExecJs(contents, "// no-op"));
+  EXPECT_TRUE(
+      contents->GetMainFrame()->GetSiteInstance()->RequiresDedicatedProcess());
+
+  ui_test_utils::NavigateToURL(browser(), coop_url2);
+  // Simulate user activation.
+  EXPECT_TRUE(ExecJs(contents, "// no-op"));
+  EXPECT_TRUE(
+      contents->GetMainFrame()->GetSiteInstance()->RequiresDedicatedProcess());
+
+  // Check that saved.com and saved2.com were saved to disk.
+  EXPECT_THAT(GetSavedIsolatedSites(browser()->profile()),
+              UnorderedElementsAre("https://saved.com", "https://saved2.com"));
+}
+
+// Verifies that sites isolated due to COOP headers with a user activation are
+// persisted across restarts.  Part 2/2.
+IN_PROC_BROWSER_TEST_F(SiteIsolationForCOOPBrowserTest, PersistAcrossRestarts) {
+  // Check that saved.com and saved2.com are still saved after a restart.
+  EXPECT_THAT(GetSavedIsolatedSites(browser()->profile()),
+              UnorderedElementsAre("https://saved.com", "https://saved2.com"));
+
+  // Check that these sites have been loaded as isolated on startup and utilize
+  // a dedicated process after restarting even without serving COOP headers.
+  GURL saved_url(https_server()->GetURL("saved.com", "/title1.html"));
+  GURL saved2_url(https_server()->GetURL("saved2.com", "/title2.html"));
+  ui_test_utils::NavigateToURL(browser(), saved_url);
+  content::WebContents* contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  EXPECT_TRUE(
+      contents->GetMainFrame()->GetSiteInstance()->RequiresDedicatedProcess());
+  ui_test_utils::NavigateToURL(browser(), saved2_url);
+  EXPECT_TRUE(
+      contents->GetMainFrame()->GetSiteInstance()->RequiresDedicatedProcess());
+
+  // Sanity check that an unrelated non-isolated foo.com URL does not require a
+  // dedicated process.
+  GURL foo_url(https_server()->GetURL("foo.com", "/title3.html"));
+  ui_test_utils::NavigateToURL(browser(), foo_url);
+  EXPECT_FALSE(
+      contents->GetMainFrame()->GetSiteInstance()->RequiresDedicatedProcess());
+}
+
+// Check that COOP sites are not persisted in Incognito; the isolation should
+// only persist for the duration of the Incognito session.
+IN_PROC_BROWSER_TEST_F(SiteIsolationForCOOPBrowserTest, Incognito) {
+  Browser* incognito = CreateIncognitoBrowser();
+
+  GURL coop_url = https_server()->GetURL(
+      "foo.com", "/set-header?Cross-Origin-Opener-Policy: same-origin");
+
+  ui_test_utils::NavigateToURL(incognito, coop_url);
+  content::WebContents* contents =
+      incognito->tab_strip_model()->GetActiveWebContents();
+  // Simulate user activation to isolate foo.com for the rest of the incognito
+  // session.
+  EXPECT_TRUE(ExecJs(contents, "// no-op"));
+  EXPECT_TRUE(
+      contents->GetMainFrame()->GetSiteInstance()->RequiresDedicatedProcess());
+
+  // Check that navigations to foo.com (even without COOP) are isolated in
+  // future BrowsingInstances in Incognito.
+  AddBlankTabAndShow(incognito);
+  GURL foo_url = https_server()->GetURL("foo.com", "/title1.html");
+  ui_test_utils::NavigateToURL(incognito, foo_url);
+  contents = incognito->tab_strip_model()->GetActiveWebContents();
+  EXPECT_TRUE(
+      contents->GetMainFrame()->GetSiteInstance()->RequiresDedicatedProcess());
+
+  // foo.com should not be isolated in the regular profile.
+  AddBlankTabAndShow(browser());
+  ui_test_utils::NavigateToURL(browser(), foo_url);
+  contents = browser()->tab_strip_model()->GetActiveWebContents();
+  EXPECT_FALSE(
+      contents->GetMainFrame()->GetSiteInstance()->RequiresDedicatedProcess());
+
+  // Neither profile should've saved foo.com to COOP isolated sites prefs.
+  EXPECT_THAT(GetSavedIsolatedSites(browser()->profile()), IsEmpty());
+  EXPECT_THAT(GetSavedIsolatedSites(incognito->profile()), IsEmpty());
+}
+
+// Verify that when a COOP-isolated site is visited again, the timestamp in its
+// stored pref entry is updated correctly and taken into consideration when
+// trimming the list of stored COOP sites to its maximum size.
+IN_PROC_BROWSER_TEST_F(SiteIsolationForCOOPBrowserTest,
+                       TimestampUpdateOnSecondVisit) {
+  EXPECT_THAT(GetSavedIsolatedSites(browser()->profile()), IsEmpty());
+
+  content::WebContents* contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  const std::string kCoopPath =
+      "/set-header?Cross-Origin-Opener-Policy: same-origin";
+  GURL coop1 = https_server()->GetURL("coop1.com", kCoopPath);
+  GURL coop2 = https_server()->GetURL("coop2.com", kCoopPath);
+  GURL coop3 = https_server()->GetURL("coop3.com", kCoopPath);
+  GURL coop4 = https_server()->GetURL("coop4.com", kCoopPath);
+
+  // Navigate to three COOP sites and trigger user actuvation on each one to
+  // add them all to the list of persistently isolated COOP sites.
+  ui_test_utils::NavigateToURL(browser(), coop1);
+  EXPECT_TRUE(ExecJs(contents, "// no-op"));  // Simulate user activation.
+  ui_test_utils::NavigateToURL(browser(), coop2);
+  EXPECT_TRUE(ExecJs(contents, "// no-op"));  // Simulate user activation.
+  ui_test_utils::NavigateToURL(browser(), coop3);
+  EXPECT_TRUE(ExecJs(contents, "// no-op"));  // Simulate user activation.
+
+  // At this point, the first three sites should be saved to prefs.
+  EXPECT_THAT(GetSavedIsolatedSites(browser()->profile()),
+              UnorderedElementsAre("https://coop1.com", "https://coop2.com",
+                                   "https://coop3.com"));
+
+  // Visit coop1.com again.  This should update its timestamp to be more recent
+  // than coop2.com and coop3.com.  The set of saved sites shouldn't change.
+  AddBlankTabAndShow(browser());
+  contents = browser()->tab_strip_model()->GetActiveWebContents();
+  ui_test_utils::NavigateToURL(browser(), coop1);
+  EXPECT_TRUE(ExecJs(contents, "// no-op"));  // Simulate user activation.
+  EXPECT_THAT(GetSavedIsolatedSites(browser()->profile()),
+              UnorderedElementsAre("https://coop1.com", "https://coop2.com",
+                                   "https://coop3.com"));
+
+  // Now, visit coop4.com.  Since the maximum number of saved COOP sites is 3
+  // in this test, the oldest site should be evicted.  That evicted site should
+  // be coop2.com, since coop1.com's timestamp was just updated.
+  ui_test_utils::NavigateToURL(browser(), coop4);
+  EXPECT_TRUE(ExecJs(contents, "// no-op"));  // Simulate user activation.
+  EXPECT_THAT(GetSavedIsolatedSites(browser()->profile()),
+              UnorderedElementsAre("https://coop1.com", "https://coop3.com",
+                                   "https://coop4.com"));
+}
diff --git a/chrome/browser/chromeos/BUILD.gn b/chrome/browser/chromeos/BUILD.gn
index ac0359b..e5a4ce48 100644
--- a/chrome/browser/chromeos/BUILD.gn
+++ b/chrome/browser/chromeos/BUILD.gn
@@ -498,8 +498,6 @@
     "../ash/accessibility/magnifier_type.h",
     "../ash/accessibility/select_to_speak_event_handler_delegate_impl.cc",
     "../ash/accessibility/select_to_speak_event_handler_delegate_impl.h",
-    "../ash/accessibility/soda_installer_impl_chromeos.cc",
-    "../ash/accessibility/soda_installer_impl_chromeos.h",
     "../ash/account_manager/account_manager_edu_coexistence_controller.cc",
     "../ash/account_manager/account_manager_edu_coexistence_controller.h",
     "../ash/account_manager/account_manager_facade_factory_ash.cc",
diff --git a/chrome/browser/chromeos/extensions/autotest_private/autotest_private_api.cc b/chrome/browser/chromeos/extensions/autotest_private/autotest_private_api.cc
index 18c415748..3bcb133a 100644
--- a/chrome/browser/chromeos/extensions/autotest_private/autotest_private_api.cc
+++ b/chrome/browser/chromeos/extensions/autotest_private/autotest_private_api.cc
@@ -173,6 +173,7 @@
 #include "ui/display/screen.h"
 #include "ui/events/event_constants.h"
 #include "ui/events/types/event_type.h"
+#include "ui/gfx/geometry/point_conversions.h"
 #include "ui/message_center/message_center.h"
 #include "ui/message_center/notification_list.h"
 #include "ui/message_center/public/cpp/notification.h"
@@ -953,13 +954,13 @@
   ~EventGenerator() = default;
 
   void ScheduleMouseEvent(ui::EventType type,
-                          gfx::PointF location_in_host,
+                          gfx::PointF location_in_screen,
                           int flags) {
     if (flags == 0 &&
         (type == ui::ET_MOUSE_PRESSED || type == ui::ET_MOUSE_RELEASED)) {
       LOG(ERROR) << "No flags specified for mouse button changes";
     }
-    tasks_.push_back(Task(type, location_in_host, flags));
+    tasks_.push_back(Task(type, location_in_screen, flags));
   }
 
   void Run() {
@@ -977,12 +978,12 @@
     };
 
     const ui::EventType type;
-    const gfx::PointF location_in_host;
+    const gfx::PointF location_in_screen;
     const int flags;
     Status status = kNotScheduled;
 
-    Task(ui::EventType type, gfx::PointF location_in_host, int flags)
-        : type(type), location_in_host(location_in_host), flags(flags) {}
+    Task(ui::EventType type, gfx::PointF location_in_screen, int flags)
+        : type(type), location_in_screen(location_in_screen), flags(flags) {}
   };
 
   void SendEvent() {
@@ -1017,14 +1018,32 @@
         }
         break;
       }
-      case ui::ET_MOUSE_MOVED:
-      case ui::ET_MOUSE_DRAGGED:
+      case ui::ET_MOUSE_MOVED: {
+        display::Display display =
+            display::Screen::GetScreen()->GetDisplayNearestPoint(
+                gfx::ToFlooredPoint((task->location_in_screen)));
+        auto* root_window = ash::Shell::GetRootWindowForDisplayId(display.id());
+        if (!root_window->GetBoundsInScreen().Contains(
+                gfx::ToFlooredPoint(task->location_in_screen))) {
+          // Not in any of the display. Does nothing and schedules a new task.
+          OnFinishedProcessingEvent();
+          return;
+        }
+        gfx::PointF location_in_host(task->location_in_screen);
+        wm::ConvertPointFromScreen(root_window, &location_in_host);
+        ConvertPointToHost(root_window, &location_in_host);
+        if (root_window->GetHost() != host_) {
+          // Switching to the new display.
+          host_ = root_window->GetHost();
+          host_->MoveCursorToLocationInPixels(
+              gfx::ToFlooredPoint(location_in_host));
+        }
         // The location should be offset by the origin of the root-window since
         // ui::SystemInputInjector expects so.
         input_injector_->MoveCursorTo(
-            task->location_in_host +
-            host_->GetBoundsInPixels().OffsetFromOrigin());
+            location_in_host + host_->GetBoundsInPixels().OffsetFromOrigin());
         break;
+      }
       default:
         NOTREACHED();
     }
@@ -4469,38 +4488,30 @@
   if (!root_window)
     return RespondNow(Error("Failed to find the root window"));
 
-  const gfx::PointF location_in_root(params->location.x, params->location.y);
-  gfx::PointF location_in_screen = location_in_root;
-  wm::ConvertPointToScreen(root_window, &location_in_screen);
+  gfx::Point location_in_screen(params->location.x, params->location.y);
   auto* env = aura::Env::GetInstance();
   const gfx::Point last_mouse_location(env->last_mouse_location());
-  if (last_mouse_location == gfx::ToFlooredPoint(location_in_screen))
+  if (last_mouse_location == location_in_screen)
     return RespondNow(NoArguments());
 
-  gfx::PointF location_in_host = location_in_root;
-  ConvertPointToHost(root_window, &location_in_host);
-
   event_generator_ = std::make_unique<EventGenerator>(
       root_window->GetHost(),
       base::BindOnce(&AutotestPrivateMouseMoveFunction::Respond, this,
                      NoArguments()));
-  gfx::PointF start_in_host(last_mouse_location.x(), last_mouse_location.y());
-  wm::ConvertPointFromScreen(root_window, &start_in_host);
-  ConvertPointToHost(root_window, &start_in_host);
 
   int64_t steps = std::max(
       base::ClampFloor<int64_t>(params->duration_in_ms /
                                 event_generator_->interval().InMillisecondsF()),
       static_cast<int64_t>(1));
   int flags = env->mouse_button_flags();
-  ui::EventType type = (flags == 0) ? ui::ET_MOUSE_MOVED : ui::ET_MOUSE_DRAGGED;
   for (int64_t i = 1; i <= steps; ++i) {
     double progress = static_cast<double>(i) / static_cast<double>(steps);
-    gfx::PointF point(gfx::Tween::FloatValueBetween(progress, start_in_host.x(),
-                                                    location_in_host.x()),
-                      gfx::Tween::FloatValueBetween(progress, start_in_host.y(),
-                                                    location_in_host.y()));
-    event_generator_->ScheduleMouseEvent(type, point, flags);
+    gfx::PointF point(
+        gfx::Tween::FloatValueBetween(progress, last_mouse_location.x(),
+                                      location_in_screen.x()),
+        gfx::Tween::FloatValueBetween(progress, last_mouse_location.y(),
+                                      location_in_screen.y()));
+    event_generator_->ScheduleMouseEvent(ui::ET_MOUSE_MOVED, point, flags);
   }
   event_generator_->Run();
   return RespondLater();
@@ -5113,12 +5124,10 @@
   if (!event_router)
     return;
 
-  std::unique_ptr<base::ListValue> event_args =
-      std::make_unique<base::ListValue>();
   std::unique_ptr<Event> event(
       new Event(events::AUTOTESTPRIVATE_ON_CLIPBOARD_DATA_CHANGED,
                 api::autotest_private::OnClipboardDataChanged::kEventName,
-                std::move(event_args)));
+                std::vector<base::Value>()));
   event_router->BroadcastEvent(std::move(event));
 }
 
diff --git a/chrome/browser/chromeos/extensions/file_system_provider/file_system_provider_apitest.cc b/chrome/browser/chromeos/extensions/file_system_provider/file_system_provider_apitest.cc
index 647796a1..94133aff 100644
--- a/chrome/browser/chromeos/extensions/file_system_provider/file_system_provider_apitest.cc
+++ b/chrome/browser/chromeos/extensions/file_system_provider/file_system_provider_apitest.cc
@@ -151,184 +151,183 @@
 };
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, Mount) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "file_system_provider/mount", .launch_as_platform_app = true},
-      {.load_as_component = true}))
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/mount",
+                               {.launch_as_platform_app = true},
+                               {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, Unmount) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "file_system_provider/unmount", .launch_as_platform_app = true},
-      {.load_as_component = true}))
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/unmount",
+                               {.launch_as_platform_app = true},
+                               {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, GetAll) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "file_system_provider/get_all", .launch_as_platform_app = true},
-      {.load_as_component = true}))
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/get_all",
+                               {.launch_as_platform_app = true},
+                               {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, GetMetadata) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/get_metadata",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/get_metadata",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, ReadDirectory) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/read_directory",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/read_directory",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, ReadFile) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/read_file",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/read_file",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, BigFile) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "file_system_provider/big_file", .launch_as_platform_app = true},
-      {.load_as_component = true}))
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/big_file",
+                               {.launch_as_platform_app = true},
+                               {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, Evil) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "file_system_provider/evil", .launch_as_platform_app = true},
-      {.load_as_component = true}))
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/evil",
+                               {.launch_as_platform_app = true},
+                               {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, MimeType) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/mime_type",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/mime_type",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, CreateDirectory) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/create_directory",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/create_directory",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, DeleteEntry) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/delete_entry",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/delete_entry",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, CreateFile) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/create_file",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/create_file",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, CopyEntry) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/copy_entry",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/copy_entry",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, MoveEntry) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/move_entry",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/move_entry",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, Truncate) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "file_system_provider/truncate", .launch_as_platform_app = true},
-      {.load_as_component = true}))
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/truncate",
+                               {.launch_as_platform_app = true},
+                               {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, WriteFile) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/write_file",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/write_file",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, Extension) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/extension"},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/extension", {},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, Thumbnail) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/thumbnail",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/thumbnail",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, AddWatcher) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/add_watcher",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/add_watcher",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, RemoveWatcher) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/remove_watcher",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/remove_watcher",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, Notify) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "file_system_provider/notify", .launch_as_platform_app = true},
-      {.load_as_component = true}))
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/notify",
+                               {.launch_as_platform_app = true},
+                               {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, Configure) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/configure",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/configure",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, GetActions) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/get_actions",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/get_actions",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, ExecuteAction) {
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/execute_action",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/execute_action",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, Unresponsive_Extension) {
   AbortOnUnresponsivePerformer performer(browser()->profile());
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "file_system_provider/unresponsive_extension"},
-                       {.load_as_component = true}))
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/unresponsive_extension",
+                               {}, {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemProviderApiTest, Unresponsive_App) {
   AbortOnUnresponsivePerformer performer(browser()->profile());
-  ASSERT_TRUE(RunExtensionTest({.name = "file_system_provider/unresponsive_app",
-                                .launch_as_platform_app = true},
+  ASSERT_TRUE(RunExtensionTest("file_system_provider/unresponsive_app",
+                               {.launch_as_platform_app = true},
                                {.load_as_component = true}))
       << message_;
 }
diff --git a/chrome/browser/chromeos/extensions/media_player_event_router.cc b/chrome/browser/chromeos/extensions/media_player_event_router.cc
index 05d2dc4..071a6090 100644
--- a/chrome/browser/chromeos/extensions/media_player_event_router.cc
+++ b/chrome/browser/chromeos/extensions/media_player_event_router.cc
@@ -19,9 +19,8 @@
                     events::HistogramValue histogram_value,
                     const std::string& event_name) {
   if (context && EventRouter::Get(context)) {
-    std::unique_ptr<base::ListValue> args(new base::ListValue());
     std::unique_ptr<Event> event(
-        new Event(histogram_value, event_name, std::move(args)));
+        new Event(histogram_value, event_name, std::vector<base::Value>()));
     EventRouter::Get(context)->BroadcastEvent(std::move(event));
   }
 }
diff --git a/chrome/browser/chromeos/extensions/wallpaper_api.cc b/chrome/browser/chromeos/extensions/wallpaper_api.cc
index ab97650..9ffa3d6 100644
--- a/chrome/browser/chromeos/extensions/wallpaper_api.cc
+++ b/chrome/browser/chromeos/extensions/wallpaper_api.cc
@@ -204,11 +204,11 @@
     extensions::EventRouter* event_router =
         extensions::EventRouter::Get(profile);
 
-    base::Value event_args(Value::Type::LIST);
-    event_args.Append(Value(GenerateThumbnail(image, image.size())));
-    event_args.Append(Value(thumbnail_data));
-    event_args.Append(
-        extensions::api::wallpaper::ToString(params_->details.layout));
+    std::vector<base::Value> event_args;
+    event_args.push_back(base::Value(GenerateThumbnail(image, image.size())));
+    event_args.push_back(base::Value(thumbnail_data));
+    event_args.push_back(base::Value(
+        extensions::api::wallpaper::ToString(params_->details.layout)));
     // Setting wallpaper from right click menu in 'Files' app is a feature that
     // was implemented in crbug.com/578935. Since 'Files' app is a built-in v1
     // app in ChromeOS, we should treat it slightly differently with other third
@@ -216,14 +216,15 @@
     // and it should not appear in the wallpaper grid in the Wallpaper Picker.
     // But we should not display the 'wallpaper-set-by-mesage' since it might
     // introduce confusion as shown in crbug.com/599407.
-    event_args.Append((extension()->id() == file_manager::kFileManagerAppId)
-                          ? base::StringPiece()
-                          : extension()->name());
+    base::StringPiece ext_name;
+    if (extension()->id() != file_manager::kFileManagerAppId)
+      ext_name = extension()->name();
+    event_args.push_back(base::Value(ext_name));
     std::unique_ptr<extensions::Event> event(new extensions::Event(
         extensions::events::WALLPAPER_PRIVATE_ON_WALLPAPER_CHANGED_BY_3RD_PARTY,
         extensions::api::wallpaper_private::OnWallpaperChangedBy3rdParty::
             kEventName,
-        base::ListValue::From(std::make_unique<Value>(std::move(event_args)))));
+        std::move(event_args)));
     event_router->DispatchEventToExtension(extension_misc::kWallpaperManagerId,
                                            std::move(event));
   }
diff --git a/chrome/browser/chromeos/full_restore/arc_ghost_window_shell_surface.cc b/chrome/browser/chromeos/full_restore/arc_ghost_window_shell_surface.cc
index 5345dab7..6b35928c 100644
--- a/chrome/browser/chromeos/full_restore/arc_ghost_window_shell_surface.cc
+++ b/chrome/browser/chromeos/full_restore/arc_ghost_window_shell_surface.cc
@@ -36,7 +36,11 @@
   absl::optional<double> scale_factor = GetDisplayScaleFactor(display_id);
   DCHECK(scale_factor.has_value());
 
-  uint32_t theme_color = color.has_value() ? color.value() : SK_ColorWHITE;
+  // TODO(sstan): Fallback to system default color or other topic color, if
+  // the task hasn't valid theme color.
+  uint32_t theme_color = color.has_value() && IsValidThemeColor(color.value())
+                             ? color.value()
+                             : SK_ColorWHITE;
 
   // TODO(sstan): Handle the desk container from full_restore data.
   int container = ash::desks_util::GetActiveDeskContainerId();
diff --git a/chrome/browser/chromeos/full_restore/arc_window_utils.cc b/chrome/browser/chromeos/full_restore/arc_window_utils.cc
index a009ce1e..7490f57d 100644
--- a/chrome/browser/chromeos/full_restore/arc_window_utils.cc
+++ b/chrome/browser/chromeos/full_restore/arc_window_utils.cc
@@ -7,6 +7,7 @@
 #include "ash/public/cpp/ash_features.h"
 #include "components/arc/arc_util.h"
 #include "components/exo/wm_helper.h"
+#include "third_party/skia/include/core/SkColor.h"
 #include "ui/display/display.h"
 #include "ui/display/screen.h"
 
@@ -65,5 +66,9 @@
   return window_info;
 }
 
+bool IsValidThemeColor(uint32_t theme_color) {
+  return SkColorGetA(theme_color) == SK_AlphaOPAQUE;
+}
+
 }  // namespace full_restore
 }  // namespace chromeos
diff --git a/chrome/browser/chromeos/full_restore/arc_window_utils.h b/chrome/browser/chromeos/full_restore/arc_window_utils.h
index ee2e3d3..c5b06c2 100644
--- a/chrome/browser/chromeos/full_restore/arc_window_utils.h
+++ b/chrome/browser/chromeos/full_restore/arc_window_utils.h
@@ -27,6 +27,10 @@
 apps::mojom::WindowInfoPtr HandleArcWindowInfo(
     apps::mojom::WindowInfoPtr window_info);
 
+// Returns true if it is a valid theme color. In Android, any transparent color
+// cannot be a topic color.
+bool IsValidThemeColor(uint32_t theme_color);
+
 }  // namespace full_restore
 }  // namespace chromeos
 
diff --git a/chrome/browser/chromeos/printing/printer_setup_util.cc b/chrome/browser/chromeos/printing/printer_setup_util.cc
index a78c7fe..50ee4ac9 100644
--- a/chrome/browser/chromeos/printing/printer_setup_util.cc
+++ b/chrome/browser/chromeos/printing/printer_setup_util.cc
@@ -110,15 +110,39 @@
 
 void CapabilitiesFetchedFromService(
     const std::string& printer_id,
+    bool elevated_privileges,
     GetPrinterCapabilitiesCallback cb,
     mojom::PrinterSemanticCapsAndDefaultsResultPtr printer_caps) {
   if (printer_caps->is_result_code()) {
     LOG(WARNING) << "Failure fetching printer capabilities from service for "
                  << printer_id << " - error "
                  << printer_caps->get_result_code();
+
+    // If we failed because of access denied then we could retry at an elevated
+    // privilege (if not already elevated).
+    if (printer_caps->get_result_code() == mojom::ResultCode::kAccessDenied &&
+        !elevated_privileges) {
+      // Register that this printer requires elevated privileges.
+      PrintBackendServiceManager& service_mgr =
+          PrintBackendServiceManager::GetInstance();
+      service_mgr.SetPrinterDriverRequiresElevatedPrivilege(printer_id);
+
+      // Retry the operation which should now happen at a higher privilege
+      // level.
+      auto& service = service_mgr.GetService(
+          g_browser_process->GetApplicationLocale(), printer_id);
+      service->GetPrinterSemanticCapsAndDefaults(
+          printer_id,
+          base::BindOnce(&CapabilitiesFetchedFromService, printer_id,
+                         /*elevated_privileges=*/true, std::move(cb)));
+      return;
+    }
+
+    // Unable to fallback, call back without data.
     std::move(cb).Run(absl::nullopt);
     return;
   }
+
   VLOG(1) << "Successfully received printer capabilities from service for "
           << printer_id;
   std::move(cb).Run(printer_caps->get_printer_caps());
@@ -130,11 +154,16 @@
 
   if (base::FeatureList::IsEnabled(features::kEnableOopPrintDrivers)) {
     VLOG(1) << "Fetching printer capabilities via service";
-    auto& service = PrintBackendServiceManager::GetInstance().GetService(
+    PrintBackendServiceManager& service_mgr =
+        PrintBackendServiceManager::GetInstance();
+    auto& service = service_mgr.GetService(
         g_browser_process->GetApplicationLocale(), printer_id);
     service->GetPrinterSemanticCapsAndDefaults(
-        printer_id, base::BindOnce(&CapabilitiesFetchedFromService, printer_id,
-                                   std::move(cb)));
+        printer_id,
+        base::BindOnce(
+            &CapabilitiesFetchedFromService, printer_id,
+            service_mgr.PrinterDriverRequiresElevatedPrivilege(printer_id),
+            std::move(cb)));
   } else {
     VLOG(1) << "Fetching printer capabilities in-process";
     // USER_VISIBLE because the result is displayed in the print preview dialog.
diff --git a/chrome/browser/commerce/subscriptions/android/BUILD.gn b/chrome/browser/commerce/subscriptions/android/BUILD.gn
index 7c8128c..c844a1a8 100644
--- a/chrome/browser/commerce/subscriptions/android/BUILD.gn
+++ b/chrome/browser/commerce/subscriptions/android/BUILD.gn
@@ -4,35 +4,6 @@
 
 import("//build/config/android/rules.gni")
 
-android_library("java") {
-  sources = [
-    "java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscription.java",
-    "java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionJsonSerializer.java",
-    "java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceConfig.java",
-    "java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxy.java",
-    "java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsStorage.java",
-    "java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManager.java",
-    "java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManager.java",
-    "java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManagerImpl.java",
-  ]
-
-  deps = [
-    "//base:base_java",
-    "//chrome/android:base_module_java",
-    "//chrome/browser/android/lifecycle:java",
-    "//chrome/browser/endpoint_fetcher:java",
-    "//chrome/browser/flags:java",
-    "//chrome/browser/preferences:java",
-    "//chrome/browser/profiles/android:java",
-    "//chrome/browser/tab:java",
-    "//chrome/browser/tabmodel:java",
-    "//third_party/androidx:androidx_annotation_annotation_java",
-    "//url:gurl_java",
-  ]
-
-  annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
-}
-
 generate_jni("jni_headers") {
   sources = [
     "java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscription.java",
diff --git a/chrome/browser/commerce/subscriptions/android/DEPS b/chrome/browser/commerce/subscriptions/android/DEPS
index ff761be..3d12816 100644
--- a/chrome/browser/commerce/subscriptions/android/DEPS
+++ b/chrome/browser/commerce/subscriptions/android/DEPS
@@ -1,3 +1,4 @@
 include_rules = [
+ "+chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/PriceTrackingUtilities.java",
  "+chrome/android/java/src/org/chromium/chrome/browser",
 ]
\ No newline at end of file
diff --git a/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsService.java b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsService.java
new file mode 100644
index 0000000..e9afc28
--- /dev/null
+++ b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsService.java
@@ -0,0 +1,44 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.subscriptions;
+
+import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
+import org.chromium.chrome.browser.tabmodel.TabModelSelector;
+
+/**
+ * Commerce Subscriptions Service.
+ */
+public class CommerceSubscriptionsService {
+    private final SubscriptionsManagerImpl mSubscriptionManager;
+    private ImplicitPriceDropSubscriptionsManager mImplicitPriceDropSubscriptionsManager;
+
+    /** Creates a new instance. */
+    CommerceSubscriptionsService(SubscriptionsManagerImpl subscriptionsManager) {
+        mSubscriptionManager = subscriptionsManager;
+    }
+
+    /** Performs any deferred startup tasks required by {@link Subscriptions}. */
+    public void initDeferredStartupForActivity(TabModelSelector tabModelSelector,
+            ActivityLifecycleDispatcher activityLifecycleDispatcher) {
+        if (mImplicitPriceDropSubscriptionsManager == null) {
+            mImplicitPriceDropSubscriptionsManager = new ImplicitPriceDropSubscriptionsManager(
+                    tabModelSelector, activityLifecycleDispatcher, mSubscriptionManager);
+        }
+    }
+
+    /** Returns the subscriptionsManager. */
+    public SubscriptionsManagerImpl getSubscriptionsManager() {
+        return mSubscriptionManager;
+    }
+
+    /**
+     * Cleans up internal resources. Currently this method calls SubscriptionsManagerImpl#destroy.
+     */
+    public void destroy() {
+        if (mImplicitPriceDropSubscriptionsManager != null) {
+            mImplicitPriceDropSubscriptionsManager.destroy();
+        }
+    }
+}
diff --git a/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceFactory.java b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceFactory.java
new file mode 100644
index 0000000..dcadd88
--- /dev/null
+++ b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceFactory.java
@@ -0,0 +1,67 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.subscriptions;
+
+import androidx.annotation.VisibleForTesting;
+
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.profiles.ProfileManager;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {@link CommerceSubscriptionsService} cached by {@link Profile}.
+ */
+public class CommerceSubscriptionsServiceFactory {
+    @VisibleForTesting
+    protected static final Map<Profile, CommerceSubscriptionsService>
+            sProfileToSubscriptionsService = new HashMap<>();
+    private static ProfileManager.Observer sProfileManagerObserver;
+
+    /** Creates new instance. */
+    public CommerceSubscriptionsServiceFactory() {
+        if (sProfileManagerObserver == null) {
+            sProfileManagerObserver = new ProfileManager.Observer() {
+                @Override
+                public void onProfileAdded(Profile profile) {}
+
+                @Override
+                public void onProfileDestroyed(Profile destroyedProfile) {
+                    CommerceSubscriptionsService serviceToDestroy =
+                            sProfileToSubscriptionsService.get(destroyedProfile);
+                    if (serviceToDestroy != null) {
+                        serviceToDestroy.destroy();
+                        sProfileToSubscriptionsService.remove(destroyedProfile);
+                    }
+
+                    if (sProfileToSubscriptionsService.isEmpty()) {
+                        ProfileManager.removeObserver(sProfileManagerObserver);
+                        sProfileManagerObserver = null;
+                    }
+                }
+            };
+            ProfileManager.addObserver(sProfileManagerObserver);
+        }
+    }
+
+    /**
+     * Creates a new instance or reuses an existing one based on the current {@link Profile}.
+     *
+     * Note: Don't hold a reference to the returned value. Always use this method to access {@link
+     * CommerceSubscriptionsService} instead.
+     * @return {@link CommerceSubscriptionsService} instance for the current regular
+     *         profile.
+     */
+    public CommerceSubscriptionsService getForLastUsedProfile() {
+        Profile profile = Profile.getLastUsedRegularProfile();
+        CommerceSubscriptionsService service = sProfileToSubscriptionsService.get(profile);
+        if (service == null) {
+            service = new CommerceSubscriptionsService(new SubscriptionsManagerImpl(profile));
+            sProfileToSubscriptionsService.put(profile, service);
+        }
+        return service;
+    }
+}
diff --git a/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxy.java b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxy.java
index 136470d..4395558 100644
--- a/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxy.java
+++ b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxy.java
@@ -16,7 +16,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
-
 /**
  * Wrapper around CommerceSubscriptions Web APIs.
  */
@@ -41,6 +40,15 @@
     private static final String GET_SUBSCRIPTIONS_QUERY_PARAMS_TEMPLATE =
             "?requestParams.subscriptionType=%s";
     private static final int BACKEND_CANONICAL_CODE_SUCCESS = 0;
+    private final Profile mProfile;
+
+    /**
+     * Creates a new instance.
+     * @param profile the {@link Profile} to use when making the calls.
+     */
+    public CommerceSubscriptionsServiceProxy(Profile profile) {
+        mProfile = profile;
+    }
 
     /**
      * Makes an HTTPS call to the backend in order to create the provided subscriptions.
@@ -67,13 +75,12 @@
      */
     public void get(@CommerceSubscription.CommerceSubscriptionType String type,
             Callback<List<CommerceSubscription>> callback) {
-        // TODO(crbug.com/1195469) Accept Profile instance from SubscriptionsManager.
         EndpointFetcher.fetchUsingOAuth(
                 (response)
                         -> {
                     callback.onResult(createCommerceSubscriptions(response.getResponseString()));
                 },
-                Profile.getLastUsedRegularProfile(), OAUTH_NAME,
+                mProfile, OAUTH_NAME,
                 CommerceSubscriptionsServiceConfig.SUBSCRIPTIONS_SERVICE_BASE_URL.getValue()
                         + String.format(GET_SUBSCRIPTIONS_QUERY_PARAMS_TEMPLATE, type),
                 GET_HTTPS_METHOD, CONTENT_TYPE, OAUTH_SCOPE, EMPTY_POST_DATA,
@@ -81,14 +88,13 @@
     }
 
     private void manageSubscriptions(JSONObject requestPayload, Callback<Boolean> callback) {
-        // TODO(crbug.com/1195469) Accept Profile instance from SubscriptionsManager.
         EndpointFetcher.fetchUsingOAuth(
                 (response)
                         -> {
                     callback.onResult(
                             didManageSubscriptionCallSucceed(response.getResponseString()));
                 },
-                Profile.getLastUsedRegularProfile(), OAUTH_NAME,
+                mProfile, OAUTH_NAME,
                 CommerceSubscriptionsServiceConfig.SUBSCRIPTIONS_SERVICE_BASE_URL.getValue(),
                 POST_HTTPS_METHOD, CONTENT_TYPE, OAUTH_SCOPE, requestPayload.toString(),
                 HTTPS_REQUEST_TIMEOUT_MS);
diff --git a/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsStorage.java b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsStorage.java
index 60e99e37..cb8c470 100644
--- a/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsStorage.java
+++ b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsStorage.java
@@ -116,6 +116,11 @@
         mNativeCommerceSubscriptionDB = nativePtr;
     }
 
+    @VisibleForTesting
+    public void setNativeCommerceSubscriptionDBForTesting(long nativeCommerceSubscriptionDB) {
+        mNativeCommerceSubscriptionDB = nativeCommerceSubscriptionDB;
+    }
+
     /**
      * Generate the key for a {@link CommerceSubscription} used to store it in database.
      * @param subscription The {@link CommerceSubscription} whose key we want to generate.
diff --git a/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManager.java b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManager.java
index 29d3d1c..83d8389 100644
--- a/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManager.java
+++ b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManager.java
@@ -4,13 +4,15 @@
 
 package org.chromium.chrome.browser.subscriptions;
 
+import android.text.TextUtils;
+
 import androidx.annotation.VisibleForTesting;
 
-import org.chromium.chrome.browser.DeferredStartupHandler;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
 import org.chromium.chrome.browser.lifecycle.PauseResumeWithNativeObserver;
 import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
+import org.chromium.chrome.browser.price_tracking.PriceDropNotificationManager;
 import org.chromium.chrome.browser.subscriptions.CommerceSubscription.CommerceSubscriptionType;
 import org.chromium.chrome.browser.subscriptions.CommerceSubscription.SubscriptionManagementType;
 import org.chromium.chrome.browser.subscriptions.CommerceSubscription.TrackingIdType;
@@ -44,6 +46,7 @@
     private final PauseResumeWithNativeObserver mPauseResumeWithNativeObserver;
     private final SubscriptionsManagerImpl mSubscriptionManager;
     private final SharedPreferencesManager mSharedPreferencesManager;
+    private final PriceDropNotificationManager mPriceDropNotificationManager;
 
     public ImplicitPriceDropSubscriptionsManager(TabModelSelector tabModelSelector,
             ActivityLifecycleDispatcher activityLifecycleDispatcher,
@@ -62,7 +65,6 @@
             }
         };
         mTabModelSelector.getModel(false).addObserver(mTabModelObserver);
-        DeferredStartupHandler.getInstance().addDeferredTask(this::initializeSubscriptions);
         mActivityLifecycleDispatcher = activityLifecycleDispatcher;
         mPauseResumeWithNativeObserver = new PauseResumeWithNativeObserver() {
             @Override
@@ -75,6 +77,7 @@
         };
         mActivityLifecycleDispatcher.register(mPauseResumeWithNativeObserver);
         mSharedPreferencesManager = SharedPreferencesManager.getInstance();
+        mPriceDropNotificationManager = new PriceDropNotificationManager();
     }
 
     private boolean isUniqueTab(Tab tab) {
@@ -108,17 +111,21 @@
         }
         List<CommerceSubscription> subscriptions = new ArrayList<>();
         for (Tab tab : urlTabMapping.values()) {
+            if (!hasOfferId(tab)) {
+                continue;
+            }
             CommerceSubscription subscription =
                     new CommerceSubscription(CommerceSubscriptionType.PRICE_TRACK,
                             ShoppingPersistedTabData.from(tab).getMainOfferId(),
                             SubscriptionManagementType.CHROME_MANAGED, TrackingIdType.OFFER_ID);
             subscriptions.add(subscription);
         }
-        mSubscriptionManager.subscribe(subscriptions);
+        mSubscriptionManager.subscribe(subscriptions,
+                (didSucceed) -> { assert didSucceed : "Failed to create subscriptions."; });
     }
 
     private void unsubscribe(Tab tab) {
-        if (!isUniqueTab(tab)) {
+        if (!isUniqueTab(tab) || !hasOfferId(tab)) {
             return;
         }
 
@@ -126,11 +133,13 @@
                 new CommerceSubscription(CommerceSubscriptionType.PRICE_TRACK,
                         ShoppingPersistedTabData.from(tab).getMainOfferId(),
                         SubscriptionManagementType.CHROME_MANAGED, TrackingIdType.OFFER_ID);
-        mSubscriptionManager.unsubscribe(subscription);
+        mSubscriptionManager.unsubscribe(subscription,
+                (didSucceed) -> { assert didSucceed : "Failed to remove subscriptions."; });
     }
 
     private boolean hasOfferId(Tab tab) {
-        return !ShoppingPersistedTabData.from(tab).getMainOfferId().isEmpty();
+        return ShoppingPersistedTabData.from(tab) != null
+                && !TextUtils.isEmpty(ShoppingPersistedTabData.from(tab).getMainOfferId());
     }
 
     // TODO(crbug.com/1186450): Extract this method to a utility class. Also, make the one-day time
@@ -146,10 +155,11 @@
     }
 
     private boolean shouldInitializeSubscriptions() {
-        if (System.currentTimeMillis()
-                        - mSharedPreferencesManager.readLong(
-                                CHROME_MANAGED_SUBSCRIPTIONS_TIMESTAMP, -1)
-                < CHROME_MANAGED_SUBSCRIPTIONS_TIME_THRESHOLD_MS) {
+        if ((!mPriceDropNotificationManager.canPostNotification())
+                || (System.currentTimeMillis()
+                                - mSharedPreferencesManager.readLong(
+                                        CHROME_MANAGED_SUBSCRIPTIONS_TIMESTAMP, -1)
+                        < CHROME_MANAGED_SUBSCRIPTIONS_TIME_THRESHOLD_MS)) {
             return false;
         }
         mSharedPreferencesManager.writeLong(
diff --git a/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManager.java b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManager.java
index 5460ba3..de8b79bd 100644
--- a/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManager.java
+++ b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManager.java
@@ -15,25 +15,29 @@
     /**
      * Creates a new subscription on the server if needed.
      * @param subscription The {@link CommerceSubscription} to add.
+     * @param callback indicates whether or not the operation was successful.
      */
-    void subscribe(CommerceSubscription subscription);
+    void subscribe(CommerceSubscription subscription, Callback<Boolean> callback);
 
     /**
      * Creates new subscriptions in batch if needed.
      * @param subscriptions The list of {@link CommerceSubscription} to add.
+     * @param callback indicates whether or not the operation was successful.
      */
-    void subscribe(List<CommerceSubscription> subscriptions);
+    void subscribe(List<CommerceSubscription> subscriptions, Callback<Boolean> callback);
 
     /**
      * Destroys a subscription on the server if needed.
      * @param subscription The {@link CommerceSubscription} to destroy.
+     * @param callback indicates whether or not the operation was successful.
      */
-    void unsubscribe(CommerceSubscription subscription);
+    void unsubscribe(CommerceSubscription subscription, Callback<Boolean> callback);
 
     /**
      * Returns all subscriptions that match the provided type.
      * @param type The {@link CommerceSubscription.CommerceSubscriptionType} to query.
      * @param forceFetch Whether to fetch from server.
+     * @param callback returns the list of subscriptions.
      */
     void getSubscriptions(@CommerceSubscription.CommerceSubscriptionType String type,
             boolean forceFetch, Callback<List<CommerceSubscription>> callback);
diff --git a/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManagerImpl.java b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManagerImpl.java
index 7682d2e..d4928db 100644
--- a/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManagerImpl.java
+++ b/chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManagerImpl.java
@@ -9,6 +9,7 @@
 import org.chromium.base.Callback;
 import org.chromium.chrome.browser.profiles.Profile;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -18,67 +19,76 @@
  */
 public class SubscriptionsManagerImpl implements SubscriptionsManager {
     private final CommerceSubscriptionsStorage mStorage;
+    private final CommerceSubscriptionsServiceProxy mServiceProxy;
     private static List<CommerceSubscription> sRemoteSubscriptionsForTesting;
 
-    public SubscriptionsManagerImpl() {
-        mStorage = new CommerceSubscriptionsStorage(Profile.getLastUsedRegularProfile());
+    public SubscriptionsManagerImpl(Profile profile) {
+        mStorage = new CommerceSubscriptionsStorage(profile);
+        mServiceProxy = new CommerceSubscriptionsServiceProxy(profile);
     }
 
     /**
      * Creates a new subscription on the server-side and refreshes the local storage of
      * subscriptions.
      * @param subscription The {@link CommerceSubscription} to add.
+     * @param callback indicates whether or not the operation was successful.
      */
     @Override
-    public void subscribe(CommerceSubscription subscription) {
-        String type = subscription.getType();
-        if (type.equals(CommerceSubscription.CommerceSubscriptionType.PRICE_TRACK)) {
-            // TODO(crbug.com/1186450): Replace getSubscriptions with callback from subscription
-            // request.
-            getSubscriptions(type, true,
-                    remoteSubscriptions
-                    -> updateStorageWithSubscriptions(type, remoteSubscriptions));
+    public void subscribe(CommerceSubscription subscription, Callback<Boolean> callback) {
+        if (subscription == null || !isSubscriptionTypeSupported(subscription.getType())) {
+            callback.onResult(false);
+            return;
         }
+
+        mServiceProxy.create(new ArrayList<CommerceSubscription>() {
+            { add(subscription); };
+        }, (didSucceed) -> handleUpdateSubscriptionsResponse(didSucceed, subscription.getType()));
     }
 
     /**
      * Creates new subscriptions in batch if needed.
      * @param subscriptions The list of {@link CommerceSubscription} to add.
+     * @param callback indicates whether or not the operation was successful.
      */
     @Override
-    public void subscribe(List<CommerceSubscription> subscriptions) {
-        if (subscriptions.size() == 0) return;
+    public void subscribe(List<CommerceSubscription> subscriptions, Callback<Boolean> callback) {
+        if (subscriptions.size() == 0) {
+            callback.onResult(false);
+            return;
+        }
 
         String type = subscriptions.get(0).getType();
-        if (type.equals(CommerceSubscription.CommerceSubscriptionType.PRICE_TRACK)) {
-            // TODO(crbug.com/1186450): Replace getSubscriptions with callback from subscription
-            // request.
-            getSubscriptions(type, true,
-                    remoteSubscriptions
-                    -> updateStorageWithSubscriptions(type, remoteSubscriptions));
+        if (isSubscriptionTypeSupported(type)) {
+            mServiceProxy.create(subscriptions,
+                    (didSucceed) -> handleUpdateSubscriptionsResponse(didSucceed, type));
+        } else {
+            callback.onResult(false);
         }
     }
 
     /**
      * Destroys a subscription on the server-side and refreshes the local storage of subscriptions.
      * @param subscription The {@link CommerceSubscription} to destroy.
+     * @param callback indicates whether or not the operation was successful.
      */
     @Override
-    public void unsubscribe(CommerceSubscription subscription) {
+    public void unsubscribe(CommerceSubscription subscription, Callback<Boolean> callback) {
         String type = subscription.getType();
-        if (type.equals(CommerceSubscription.CommerceSubscriptionType.PRICE_TRACK)) {
-            // TODO(crbug.com/1186450): Replace getSubscriptions with callback from unsubscription
-            // request.
-            getSubscriptions(type, true,
-                    remoteSubscriptions
-                    -> updateStorageWithSubscriptions(type, remoteSubscriptions));
+        if (subscription == null || !isSubscriptionTypeSupported(type)) {
+            callback.onResult(false);
+            return;
         }
+
+        mServiceProxy.delete(new ArrayList<CommerceSubscription>() {
+            { add(subscription); };
+        }, (didSucceed) -> handleUpdateSubscriptionsResponse(didSucceed, type));
     }
 
     /**
      * Returns all subscriptions that match the provided type.
      * @param type The {@link CommerceSubscription.CommerceSubscriptionType} to query.
      * @param forceFetch Whether to fetch from server. If no, fetch from local storage.
+     * @param callback returns the list of subscriptions.
      */
     @Override
     public void getSubscriptions(@CommerceSubscription.CommerceSubscriptionType String type,
@@ -87,12 +97,19 @@
             callback.onResult(sRemoteSubscriptionsForTesting);
             return;
         }
-        if (!forceFetch) {
+        if (forceFetch) {
+            mServiceProxy.get(type, callback);
+        } else {
             mStorage.loadWithPrefix(String.valueOf(type),
                     localSubscriptions -> callback.onResult(localSubscriptions));
         }
     }
 
+    private boolean isSubscriptionTypeSupported(
+            @CommerceSubscription.CommerceSubscriptionType String type) {
+        return CommerceSubscription.CommerceSubscriptionType.PRICE_TRACK.equals(type);
+    }
+
     private void updateStorageWithSubscriptions(
             @CommerceSubscription.CommerceSubscriptionType String type,
             List<CommerceSubscription> remoteSubscriptions) {
@@ -110,6 +127,16 @@
         });
     }
 
+    private void handleUpdateSubscriptionsResponse(
+            Boolean didSucceed, @CommerceSubscription.CommerceSubscriptionType String type) {
+        assert didSucceed : "Failed to handle update subscriptions response";
+        if (didSucceed) {
+            getSubscriptions(type, true,
+                    remoteSubscriptions
+                    -> updateStorageWithSubscriptions(type, remoteSubscriptions));
+        }
+    }
+
     @VisibleForTesting
     public void setRemoteSubscriptionsForTesting(List<CommerceSubscription> subscriptions) {
         sRemoteSubscriptionsForTesting = subscriptions;
diff --git a/chrome/browser/commerce/subscriptions/android/java_sources.gni b/chrome/browser/commerce/subscriptions/android/java_sources.gni
new file mode 100644
index 0000000..952a720
--- /dev/null
+++ b/chrome/browser/commerce/subscriptions/android/java_sources.gni
@@ -0,0 +1,32 @@
+# 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.
+
+# TODO(crbug/1210158): This should be a separate build target when circular
+# dependencies are removed.
+commerce_subscriptions_java_sources = [
+  "//chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscription.java",
+  "//chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionJsonSerializer.java",
+  "//chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsService.java",
+  "//chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceConfig.java",
+  "//chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceFactory.java",
+  "//chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxy.java",
+  "//chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsStorage.java",
+  "//chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManager.java",
+  "//chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManager.java",
+  "//chrome/browser/commerce/subscriptions/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManagerImpl.java",
+]
+
+commerce_subscriptions_java_deps = [
+  "//base:base_java",
+  "//chrome/android:base_module_java",
+  "//chrome/browser/android/lifecycle:java",
+  "//chrome/browser/endpoint_fetcher:java",
+  "//chrome/browser/flags:java",
+  "//chrome/browser/preferences:java",
+  "//chrome/browser/profiles/android:java",
+  "//chrome/browser/tab:java",
+  "//chrome/browser/tabmodel:java",
+  "//third_party/androidx:androidx_annotation_annotation_java",
+  "//url:gurl_java",
+]
diff --git a/chrome/browser/commerce/subscriptions/test/android/BUILD.gn b/chrome/browser/commerce/subscriptions/test/android/BUILD.gn
deleted file mode 100644
index c5e46e6..0000000
--- a/chrome/browser/commerce/subscriptions/test/android/BUILD.gn
+++ /dev/null
@@ -1,67 +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.
-
-import("//build/config/android/config.gni")
-import("//build/config/android/rules.gni")
-
-java_library("junit") {
-  # Skip platform checks since Robolectric depends on requires_android targets.
-  bypass_platform_checks = true
-
-  testonly = true
-
-  sources = [
-    "java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionJsonSerializerUnitTest.java",
-    "java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManagerUnitTest.java",
-  ]
-
-  deps = [
-    "//base:base_java",
-    "//base:base_junit_test_support",
-    "//chrome/android:base_module_java",
-    "//chrome/android:chrome_java",
-    "//chrome/browser/android/lifecycle:java",
-    "//chrome/browser/commerce/subscriptions/android:java",
-    "//chrome/browser/flags:java",
-    "//chrome/browser/preferences:java",
-    "//chrome/browser/tab:java",
-    "//chrome/browser/tabmodel:java",
-    "//chrome/test/android:chrome_java_test_support",
-    "//third_party/android_deps:robolectric_all_java",
-    "//third_party/androidx:androidx_annotation_annotation_java",
-    "//third_party/androidx:androidx_test_runner_java",
-    "//third_party/hamcrest:hamcrest_core_java",
-    "//third_party/junit",
-    "//third_party/mockito:mockito_java",
-    "//url:gurl_java",
-    "//url:gurl_junit_test_support",
-  ]
-}
-
-android_library("javatests") {
-  testonly = true
-
-  sources = [
-    "java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxyUnitTest.java",
-    "java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsStorageTest.java",
-    "java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsLoadCallbackHelper.java",
-    "java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManagerImplTest.java",
-  ]
-
-  deps = [
-    "//base:base_java",
-    "//base:base_java_test_support",
-    "//chrome/browser/commerce/subscriptions/android:java",
-    "//chrome/browser/endpoint_fetcher:java",
-    "//chrome/browser/flags:java",
-    "//chrome/browser/profiles/android:java",
-    "//chrome/test/android:chrome_java_test_support",
-    "//content/public/test/android:content_java_test_support",
-    "//third_party/androidx:androidx_test_core_java",
-    "//third_party/androidx:androidx_test_runner_java",
-    "//third_party/hamcrest:hamcrest_java",
-    "//third_party/junit",
-    "//third_party/mockito:mockito_java",
-  ]
-}
diff --git a/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceFactoryUnitTest.java b/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceFactoryUnitTest.java
new file mode 100644
index 0000000..8a9b1eb8
--- /dev/null
+++ b/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceFactoryUnitTest.java
@@ -0,0 +1,105 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.subscriptions;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.base.test.util.JniMocker;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.profiles.ProfileManager;
+import org.chromium.chrome.test.util.browser.Features;
+import org.chromium.components.embedder_support.browser_context.BrowserContextHandle;
+
+/**
+ * Unit tests for {@link CommerceSubscriptionsServiceFactory}.
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class CommerceSubscriptionsServiceFactoryUnitTest {
+    @Rule
+    public TestRule mProcessor = new Features.JUnitProcessor();
+
+    @Rule
+    public JniMocker mMocker = new JniMocker();
+
+    @Mock
+    private Profile mProfileOne;
+
+    @Mock
+    private Profile mProfileTwo;
+
+    @Mock
+    private CommerceSubscriptionsStorage.Natives mCommerceSubscriptionsStorageJni;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        doReturn(false).when(mProfileOne).isOffTheRecord();
+        doReturn(false).when(mProfileTwo).isOffTheRecord();
+        mMocker.mock(CommerceSubscriptionsStorageJni.TEST_HOOKS, mCommerceSubscriptionsStorageJni);
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) {
+                CommerceSubscriptionsStorage storage =
+                        (CommerceSubscriptionsStorage) invocation.getArguments()[0];
+                storage.setNativeCommerceSubscriptionDBForTesting((long) 123);
+                return null;
+            }
+        })
+                .when(mCommerceSubscriptionsStorageJni)
+                .init(any(CommerceSubscriptionsStorage.class), any(BrowserContextHandle.class));
+    }
+
+    @Test
+    @SmallTest
+    public void testFactoryMethod() {
+        CommerceSubscriptionsServiceFactory factory = new CommerceSubscriptionsServiceFactory();
+
+        Profile.setLastUsedProfileForTesting(mProfileOne);
+        CommerceSubscriptionsService regularProfileOneService = factory.getForLastUsedProfile();
+        Assert.assertEquals(regularProfileOneService, factory.getForLastUsedProfile());
+
+        Profile.setLastUsedProfileForTesting(mProfileTwo);
+        CommerceSubscriptionsService regularProfileTwoService = factory.getForLastUsedProfile();
+        Assert.assertNotEquals(regularProfileOneService, regularProfileTwoService);
+        Assert.assertEquals(regularProfileTwoService, factory.getForLastUsedProfile());
+
+        Profile.setLastUsedProfileForTesting(mProfileOne);
+        Assert.assertEquals(regularProfileOneService, factory.getForLastUsedProfile());
+
+        Profile.setLastUsedProfileForTesting(mProfileTwo);
+        Assert.assertEquals(regularProfileTwoService, factory.getForLastUsedProfile());
+    }
+
+    @Test
+    @SmallTest
+    public void testServiceDestroyedWhenProfileIsDestroyed() {
+        CommerceSubscriptionsServiceFactory factory = new CommerceSubscriptionsServiceFactory();
+        Profile.setLastUsedProfileForTesting(mProfileOne);
+        CommerceSubscriptionsService service = factory.getForLastUsedProfile();
+        Assert.assertEquals(
+                1, CommerceSubscriptionsServiceFactory.sProfileToSubscriptionsService.size());
+        ProfileManager.onProfileDestroyed(mProfileOne);
+        Assert.assertTrue(
+                CommerceSubscriptionsServiceFactory.sProfileToSubscriptionsService.isEmpty());
+    }
+}
diff --git a/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxyUnitTest.java b/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxyUnitTest.java
index fa00f5f..dc2ac90 100644
--- a/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxyUnitTest.java
+++ b/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxyUnitTest.java
@@ -92,7 +92,7 @@
         mMocker.mock(EndpointFetcherJni.TEST_HOOKS, mEndpointFetcherJniMock);
         doReturn(false).when(mProfile).isOffTheRecord();
         Profile.setLastUsedProfileForTesting(mProfile);
-        mServiceProxy = new CommerceSubscriptionsServiceProxy();
+        mServiceProxy = new CommerceSubscriptionsServiceProxy(mProfile);
     }
 
     @After
diff --git a/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManagerUnitTest.java b/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManagerUnitTest.java
index bcb46d2..3bf9139 100644
--- a/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManagerUnitTest.java
+++ b/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManagerUnitTest.java
@@ -15,6 +15,8 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.os.Build;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -27,12 +29,13 @@
 import org.mockito.MockitoAnnotations;
 import org.robolectric.annotation.Config;
 
+import org.chromium.base.Callback;
 import org.chromium.base.UserDataHost;
 import org.chromium.base.test.BaseRobolectricTestRunner;
-import org.chromium.chrome.browser.DeferredStartupHandler;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
 import org.chromium.chrome.browser.lifecycle.PauseResumeWithNativeObserver;
 import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
+import org.chromium.chrome.browser.price_tracking.PriceDropNotificationManager;
 import org.chromium.chrome.browser.subscriptions.CommerceSubscription.CommerceSubscriptionType;
 import org.chromium.chrome.browser.subscriptions.CommerceSubscription.SubscriptionManagementType;
 import org.chromium.chrome.browser.subscriptions.CommerceSubscription.TrackingIdType;
@@ -42,7 +45,10 @@
 import org.chromium.chrome.browser.tabmodel.TabModel;
 import org.chromium.chrome.browser.tabmodel.TabModelObserver;
 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
+import org.chromium.chrome.browser.tasks.tab_management.PriceTrackingUtilities;
+import org.chromium.chrome.browser.tasks.tab_management.TabUiFeatureUtilities;
 import org.chromium.chrome.test.util.browser.Features;
+import org.chromium.components.browser_ui.notifications.MockNotificationManagerProxy;
 import org.chromium.url.GURL;
 
 import java.util.ArrayList;
@@ -77,8 +83,6 @@
     @Mock
     SubscriptionsManagerImpl mSubscriptionsManager;
     @Mock
-    DeferredStartupHandler mDeferredStartupHandler;
-    @Mock
     CriticalPersistedTabData mCriticalPersistedTabData1;
     @Mock
     CriticalPersistedTabData mCriticalPersistedTabData2;
@@ -99,6 +103,8 @@
     private CommerceSubscription mSubscription2;
     private ImplicitPriceDropSubscriptionsManager mImplicitSubscriptionsManager;
     private SharedPreferencesManager mSharedPreferencesManager;
+    private MockNotificationManagerProxy mMockNotificationManager;
+    private PriceDropNotificationManager mPriceDropNotificationManager;
 
     @Before
     public void setUp() {
@@ -128,13 +134,21 @@
         doNothing()
                 .when(mActivityLifecycleDispatcher)
                 .register(mPauseResumeWithNativeObserverCaptor.capture());
-        DeferredStartupHandler.setInstanceForTests(mDeferredStartupHandler);
         mSharedPreferencesManager = SharedPreferencesManager.getInstance();
         mSharedPreferencesManager.writeLong(
                 ImplicitPriceDropSubscriptionsManager.CHROME_MANAGED_SUBSCRIPTIONS_TIMESTAMP,
                 System.currentTimeMillis()
                         - ImplicitPriceDropSubscriptionsManager
                                   .CHROME_MANAGED_SUBSCRIPTIONS_TIME_THRESHOLD_MS);
+        PriceTrackingUtilities.setIsSignedInAndSyncEnabledForTesting(true);
+        TabUiFeatureUtilities.ENABLE_PRICE_NOTIFICATION.setForTesting(true);
+        mMockNotificationManager = new MockNotificationManagerProxy();
+        mMockNotificationManager.setNotificationsEnabled(true);
+        PriceDropNotificationManager.setNotificationManagerForTesting(mMockNotificationManager);
+        mPriceDropNotificationManager = new PriceDropNotificationManager();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            mPriceDropNotificationManager.createNotificationChannel();
+        }
 
         mImplicitSubscriptionsManager = new ImplicitPriceDropSubscriptionsManager(
                 mTabModelSelector, mActivityLifecycleDispatcher, mSubscriptionsManager);
@@ -142,13 +156,15 @@
 
     @After
     public void tearDown() {
-        DeferredStartupHandler.setInstanceForTests(null);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            mPriceDropNotificationManager.deleteChannelForTesting();
+        }
+        PriceDropNotificationManager.setNotificationManagerForTesting(null);
     }
 
     @Test
     public void testInitialSetup() {
         verify(mTabModel).addObserver(any(TabModelObserver.class));
-        verify(mDeferredStartupHandler).addDeferredTask(any(Runnable.class));
         verify(mActivityLifecycleDispatcher).register(any(PauseResumeWithNativeObserver.class));
     }
 
@@ -159,7 +175,18 @@
         mImplicitSubscriptionsManager.initializeSubscriptions();
 
         verify(mSubscriptionsManager)
-                .subscribe(eq(new ArrayList<>(Arrays.asList(mSubscription1, mSubscription2))));
+                .subscribe(eq(new ArrayList<>(Arrays.asList(mSubscription1, mSubscription2))),
+                        any(Callback.class));
+    }
+
+    @Test
+    public void testInitialSubscription_FeatureDisabled() {
+        doReturn(2).when(mTabModel).getCount();
+
+        TabUiFeatureUtilities.ENABLE_PRICE_NOTIFICATION.setForTesting(false);
+        mImplicitSubscriptionsManager.initializeSubscriptions();
+
+        verify(mSubscriptionsManager, times(0)).subscribe(any(List.class), any(Callback.class));
     }
 
     @Test
@@ -171,7 +198,8 @@
 
         mImplicitSubscriptionsManager.initializeSubscriptions();
 
-        verify(mSubscriptionsManager).subscribe(eq(new ArrayList<>(Arrays.asList(mSubscription2))));
+        verify(mSubscriptionsManager)
+                .subscribe(eq(new ArrayList<>(Arrays.asList(mSubscription2))), any(Callback.class));
     }
 
     @Test
@@ -180,7 +208,8 @@
 
         mImplicitSubscriptionsManager.initializeSubscriptions();
 
-        verify(mSubscriptionsManager).subscribe(eq(new ArrayList<>(Arrays.asList(mSubscription2))));
+        verify(mSubscriptionsManager)
+                .subscribe(eq(new ArrayList<>(Arrays.asList(mSubscription2))), any(Callback.class));
     }
 
     @Test
@@ -194,7 +223,8 @@
 
         mImplicitSubscriptionsManager.initializeSubscriptions();
 
-        verify(mSubscriptionsManager).subscribe(eq(new ArrayList<>(Arrays.asList(mSubscription2))));
+        verify(mSubscriptionsManager)
+                .subscribe(eq(new ArrayList<>(Arrays.asList(mSubscription2))), any(Callback.class));
     }
 
     @Test
@@ -203,7 +233,8 @@
 
         mImplicitSubscriptionsManager.initializeSubscriptions();
 
-        verify(mSubscriptionsManager).subscribe(eq(new ArrayList<>(Arrays.asList(mSubscription2))));
+        verify(mSubscriptionsManager)
+                .subscribe(eq(new ArrayList<>(Arrays.asList(mSubscription2))), any(Callback.class));
     }
 
     @Test
@@ -214,7 +245,7 @@
 
         mImplicitSubscriptionsManager.initializeSubscriptions();
 
-        verify(mSubscriptionsManager, times(0)).subscribe(any(List.class));
+        verify(mSubscriptionsManager, times(0)).subscribe(any(List.class), any(Callback.class));
     }
 
     @Test
@@ -222,14 +253,16 @@
         mPauseResumeWithNativeObserverCaptor.getValue().onResumeWithNative();
 
         verify(mSubscriptionsManager)
-                .subscribe(eq(new ArrayList<>(Arrays.asList(mSubscription1, mSubscription2))));
+                .subscribe(eq(new ArrayList<>(Arrays.asList(mSubscription1, mSubscription2))),
+                        any(Callback.class));
     }
 
     @Test
     public void testTabClosure() {
         mTabModelObserverCaptor.getValue().tabClosureCommitted(mTab1);
 
-        verify(mSubscriptionsManager, times(1)).unsubscribe(mSubscriptionCaptor.capture());
+        verify(mSubscriptionsManager, times(1))
+                .unsubscribe(mSubscriptionCaptor.capture(), any(Callback.class));
         assertThat(mSubscriptionCaptor.getAllValues().get(0).getTrackingId(),
                 equalTo(String.valueOf(OFFER1_ID)));
     }
@@ -238,7 +271,8 @@
     public void testTabRemove() {
         mTabModelObserverCaptor.getValue().tabRemoved(mTab1);
 
-        verify(mSubscriptionsManager, times(1)).unsubscribe(mSubscriptionCaptor.capture());
+        verify(mSubscriptionsManager, times(1))
+                .unsubscribe(mSubscriptionCaptor.capture(), any(Callback.class));
         assertThat(mSubscriptionCaptor.getAllValues().get(0).getTrackingId(),
                 equalTo(String.valueOf(OFFER1_ID)));
     }
@@ -252,7 +286,8 @@
 
         mTabModelObserverCaptor.getValue().tabClosureCommitted(mTab1);
 
-        verify(mSubscriptionsManager, never()).unsubscribe(mSubscriptionCaptor.capture());
+        verify(mSubscriptionsManager, never())
+                .unsubscribe(mSubscriptionCaptor.capture(), any(Callback.class));
     }
 
     @Test
diff --git a/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManagerImplTest.java b/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManagerImplTest.java
index 05a2c66..791268a 100644
--- a/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManagerImplTest.java
+++ b/chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManagerImplTest.java
@@ -16,12 +16,14 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 
 import org.chromium.base.ThreadUtils;
 import org.chromium.base.test.util.Batch;
 import org.chromium.base.test.util.CallbackHelper;
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.DisabledTest;
+import org.chromium.base.test.util.JniMocker;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
@@ -50,6 +52,15 @@
     public BlankCTATabInitialStateRule mBlankCTATabInitialStateRule =
             new BlankCTATabInitialStateRule(sActivityTestRule, false);
 
+    @Mock
+    private Profile mProfile;
+
+    @Rule
+    public JniMocker mMocker = new JniMocker();
+
+    @Mock
+    private CommerceSubscriptionsStorage.Natives mCommerceSubscriptionsStorageJni;
+
     private static final String OFFER_ID_1 = "offer_id_1";
     private static final String OFFER_ID_2 = "offer_id_2";
     private static final String OFFER_ID_3 = "offer_id_3";
@@ -64,9 +75,10 @@
 
     @Before
     public void setUp() throws Exception {
+        mMocker.mock(CommerceSubscriptionsStorageJni.TEST_HOOKS, mCommerceSubscriptionsStorageJni);
         TestThreadUtils.runOnUiThreadBlocking(() -> {
-            mStorage = new CommerceSubscriptionsStorage(Profile.getLastUsedRegularProfile());
-            mSubscriptionsManager = new SubscriptionsManagerImpl();
+            mStorage = new CommerceSubscriptionsStorage(mProfile);
+            mSubscriptionsManager = new SubscriptionsManagerImpl(mProfile);
         });
 
         mSubscription1 =
@@ -115,8 +127,11 @@
                     CommerceSubscriptionsStorage.getKey(subscription), subscription);
         }
         mSubscriptionsManager.setRemoteSubscriptionsForTesting(remoteSubscriptions);
+
         // Test local cache is updated after single subscription.
-        ThreadUtils.runOnUiThreadBlocking(() -> mSubscriptionsManager.subscribe(newSubscription));
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> mSubscriptionsManager.subscribe(newSubscription, (didSucceed) -> {}));
+
         loadSingleAndCheckResult(CommerceSubscriptionsStorage.getKey(mSubscription3), null);
         loadPrefixAndCheckResult(
                 CommerceSubscription.CommerceSubscriptionType.PRICE_TRACK, expectedSubscriptions);
@@ -143,7 +158,8 @@
         }
         mSubscriptionsManager.setRemoteSubscriptionsForTesting(remoteSubscriptions);
         // Test local cache is updated after subscribing a list of subscriptions.
-        ThreadUtils.runOnUiThreadBlocking(() -> mSubscriptionsManager.subscribe(newSubscriptions));
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> mSubscriptionsManager.subscribe(newSubscriptions, (didSucceed) -> {}));
         loadSingleAndCheckResult(CommerceSubscriptionsStorage.getKey(mSubscription3), null);
         loadPrefixAndCheckResult(
                 CommerceSubscription.CommerceSubscriptionType.PRICE_TRACK, expectedSubscriptions);
@@ -170,7 +186,7 @@
         mSubscriptionsManager.setRemoteSubscriptionsForTesting(remoteSubscriptions);
         // Test local cache is updated after unsubscription.
         ThreadUtils.runOnUiThreadBlocking(
-                () -> mSubscriptionsManager.unsubscribe(removedSubscription));
+                () -> mSubscriptionsManager.unsubscribe(removedSubscription, (didSucceed) -> {}));
         loadSingleAndCheckResult(CommerceSubscriptionsStorage.getKey(removedSubscription), null);
         loadPrefixAndCheckResult(
                 CommerceSubscription.CommerceSubscriptionType.PRICE_TRACK, expectedSubscriptions);
diff --git a/chrome/browser/commerce/subscriptions/test/android/test_java_sources.gni b/chrome/browser/commerce/subscriptions/test/android/test_java_sources.gni
new file mode 100644
index 0000000..a0598b979
--- /dev/null
+++ b/chrome/browser/commerce/subscriptions/test/android/test_java_sources.gni
@@ -0,0 +1,60 @@
+# Copyright 2021 The Chromium Authors.All rights reserved.
+# Use of this source code is governed by a BSD - style license that can be
+# found in the LICENSE file.
+
+# TODO(crbug/1210158): This should be a separate build target when circular
+# dependencies are removed.
+commerce_subscriptions_junit_test_sources = [
+  "//chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionJsonSerializerUnitTest.java",
+  "//chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceFactoryUnitTest.java",
+  "//chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/ImplicitPriceDropSubscriptionsManagerUnitTest.java",
+]
+
+commerce_subscriptions_junit_test_deps = [
+  "//base:base_java",
+  "//base:base_java_test_support",
+  "//base:base_junit_test_support",
+  "//chrome/android:base_module_java",
+  "//chrome/browser/android/lifecycle:java",
+  "//chrome/browser/flags:java",
+  "//chrome/browser/preferences:java",
+  "//chrome/browser/profiles/android:java",
+  "//chrome/browser/tab:java",
+  "//chrome/browser/tabmodel:java",
+  "//chrome/test/android:chrome_java_test_support",
+  "//components/browser_ui/notifications/android:test_support_java",
+  "//components/embedder_support/android:browser_context_java",
+  "//third_party/android_deps:robolectric_all_java",
+  "//third_party/androidx:androidx_annotation_annotation_java",
+  "//third_party/androidx:androidx_test_runner_java",
+  "//third_party/hamcrest:hamcrest_core_java",
+  "//third_party/junit",
+  "//third_party/mockito:mockito_java",
+  "//url:gurl_java",
+  "//url:gurl_junit_test_support",
+]
+
+commerce_subscriptions_java_test_sources = [
+  "//chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsServiceProxyUnitTest.java",
+  "//chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/CommerceSubscriptionsStorageTest.java",
+  "//chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsLoadCallbackHelper.java",
+  "//chrome/browser/commerce/subscriptions/test/android/java/src/org/chromium/chrome/browser/subscriptions/SubscriptionsManagerImplTest.java",
+]
+
+commerce_subscriptions_java_test_deps = [
+  "//base:base_java",
+  "//base:base_java_test_support",
+  "//chrome/android:base_module_java",
+  "//chrome/browser/android/lifecycle:java",
+  "//chrome/browser/endpoint_fetcher:java",
+  "//chrome/browser/flags:java",
+  "//chrome/browser/profiles/android:java",
+  "//chrome/browser/tabmodel:java",
+  "//chrome/test/android:chrome_java_test_support",
+  "//content/public/test/android:content_java_test_support",
+  "//third_party/androidx:androidx_test_core_java",
+  "//third_party/androidx:androidx_test_runner_java",
+  "//third_party/hamcrest:hamcrest_java",
+  "//third_party/junit",
+  "//third_party/mockito:mockito_java",
+]
diff --git a/chrome/browser/download/android/BUILD.gn b/chrome/browser/download/android/BUILD.gn
index 8b95deed..373c3a1 100644
--- a/chrome/browser/download/android/BUILD.gn
+++ b/chrome/browser/download/android/BUILD.gn
@@ -74,6 +74,7 @@
     "//chrome/browser/offline_pages/android:java",
     "//chrome/browser/preferences:java",
     "//chrome/browser/profiles/android:java",
+    "//chrome/browser/settings:java",
     "//chrome/browser/ui/messages/android:java",
     "//chrome/browser/util:java",
     "//components/browser_ui/modaldialog/android:java",
diff --git a/chrome/browser/download/android/download_dialog_bridge.cc b/chrome/browser/download/android/download_dialog_bridge.cc
index 5d2e2543f..6ee74b29 100644
--- a/chrome/browser/download/android/download_dialog_bridge.cc
+++ b/chrome/browser/download/android/download_dialog_bridge.cc
@@ -177,6 +177,13 @@
   return DownloadDialogBridge::ShouldShowDateTimePicker();
 }
 
+jboolean JNI_DownloadDialogBridge_IsLocationDialogManaged(JNIEnv* env) {
+  PrefService* pref_service =
+      ProfileManager::GetActiveUserProfile()->GetOriginalProfile()->GetPrefs();
+
+  return pref_service->IsManagedPreference(prefs::kPromptForDownload);
+}
+
 // static
 long DownloadDialogBridge::GetDownloadLaterMinFileSize() {
   return base::GetFieldTrialParamByFeatureAsInt(
diff --git a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DownloadDialogBridge.java b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DownloadDialogBridge.java
index 8117ae3f..a95c7a8 100644
--- a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DownloadDialogBridge.java
+++ b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DownloadDialogBridge.java
@@ -329,6 +329,21 @@
         getPrefService().setInteger(Pref.PROMPT_FOR_DOWNLOAD_ANDROID, status);
     }
 
+    /**
+     * @return The value for {@link Pref#PROMPT_FOR_DOWNLOAD}. This is currently only used by
+     * enterprise policy.
+     */
+    public static boolean getPromptForDownloadPolicy() {
+        return getPrefService().getBoolean(Pref.PROMPT_FOR_DOWNLOAD);
+    }
+
+    /**
+     * @return whether to prompt the download location dialog is controlled by enterprise policy.
+     */
+    public static boolean isLocationDialogManaged() {
+        return DownloadDialogBridgeJni.get().isLocationDialogManaged();
+    }
+
     public static boolean shouldShowDateTimePicker() {
         return DownloadDialogBridgeJni.get().shouldShowDateTimePicker();
     }
@@ -347,5 +362,6 @@
         boolean isDataReductionProxyEnabled();
         long getDownloadLaterMinFileSize();
         boolean shouldShowDateTimePicker();
+        boolean isLocationDialogManaged();
     }
 }
diff --git a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadDialogUtils.java b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadDialogUtils.java
index 4265876..30044009 100644
--- a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadDialogUtils.java
+++ b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadDialogUtils.java
@@ -32,6 +32,7 @@
     /**
      * Returns whether the download location suggestion dialog should be prompted.
      * @param dirs The available directories.
+     * @param defaultLocation The default download location.
      * @param totalBytes The download size.
      */
     public static boolean shouldSuggestDownloadLocation(
diff --git a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator.java b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator.java
index 859f8fa..88a62c66 100644
--- a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator.java
+++ b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator.java
@@ -46,6 +46,7 @@
     private @DownloadLocationDialogType int mDialogType;
     private String mSuggestedPath;
     private Context mContext;
+    private boolean mLocationDialogManaged;
 
     /**
      * Initializes the download location dialog.
@@ -75,6 +76,7 @@
         mTotalBytes = totalBytes;
         mDialogType = dialogType;
         mSuggestedPath = suggestedPath;
+        mLocationDialogManaged = DownloadDialogBridge.isLocationDialogManaged();
 
         DownloadDirectoryProvider.getInstance().getAllDirectoriesOptions(
                 (ArrayList<DirectoryOption> dirs) -> { onDirectoryOptionsRetrieved(dirs); });
@@ -129,7 +131,8 @@
         // If there is only one directory available, don't show the default dialog, and set the
         // download directory to default. Dialog will still show for other types of dialogs, like
         // name conflict or disk error.
-        if (dirs.size() == 1 && mDialogType == DownloadLocationDialogType.DEFAULT) {
+        if (dirs.size() == 1 && !mLocationDialogManaged
+                && mDialogType == DownloadLocationDialogType.DEFAULT) {
             final DirectoryOption dir = dirs.get(0);
             if (dir.type == DirectoryOption.DownloadLocationDirectoryType.DEFAULT) {
                 assert (!TextUtils.isEmpty(dir.location));
@@ -175,7 +178,8 @@
         builder.with(
                 DownloadLocationDialogProperties.FILE_NAME, new File(mSuggestedPath).getName());
         builder.with(DownloadLocationDialogProperties.SHOW_SUBTITLE, true);
-        builder.with(DownloadLocationDialogProperties.DONT_SHOW_AGAIN_CHECKBOX_SHOWN, true);
+        builder.with(DownloadLocationDialogProperties.DONT_SHOW_AGAIN_CHECKBOX_SHOWN,
+                !mLocationDialogManaged);
 
         switch (mDialogType) {
             case DownloadLocationDialogType.LOCATION_FULL:
@@ -203,8 +207,8 @@
                         mContext.getString(R.string.download_location_name_too_long));
                 break;
             case DownloadLocationDialogType.LOCATION_SUGGESTION:
-                builder.with(DownloadLocationDialogProperties.TITLE,
-                        mContext.getString(R.string.download_location_dialog_title));
+                assert !mLocationDialogManaged;
+                builder.with(DownloadLocationDialogProperties.TITLE, getDefaultTitle());
                 builder.with(DownloadLocationDialogProperties.SHOW_LOCATION_AVAILABLE_SPACE, true);
                 assert mTotalBytes > 0;
                 builder.with(DownloadLocationDialogProperties.FILE_SIZE,
@@ -212,8 +216,8 @@
                 builder.with(DownloadLocationDialogProperties.SHOW_SUBTITLE, false);
                 break;
             case DownloadLocationDialogType.DEFAULT:
-                builder.with(DownloadLocationDialogProperties.TITLE,
-                        mContext.getString(R.string.download_location_dialog_title));
+                builder.with(DownloadLocationDialogProperties.TITLE, getDefaultTitle());
+
                 if (mTotalBytes > 0) {
                     builder.with(DownloadLocationDialogProperties.SUBTITLE,
                             DownloadUtils.getStringForBytes(mContext, mTotalBytes));
@@ -226,6 +230,12 @@
         return builder.build();
     }
 
+    private String getDefaultTitle() {
+        return mContext.getString(mLocationDialogManaged
+                        ? R.string.download_location_dialog_title_confirm_download
+                        : R.string.download_location_dialog_title);
+    }
+
     /**
      * Pass along information from location dialog to native.
      *
@@ -254,10 +264,10 @@
 
         // Update preference to show prompt based on whether checkbox is checked only when the user
         // click the positive button.
-        if (dontShowAgain) {
-            DownloadDialogBridge.setPromptForDownloadAndroid(DownloadPromptStatus.DONT_SHOW);
-        } else {
-            DownloadDialogBridge.setPromptForDownloadAndroid(DownloadPromptStatus.SHOW_PREFERENCE);
+        if (!mLocationDialogManaged) {
+            DownloadDialogBridge.setPromptForDownloadAndroid(dontShowAgain
+                            ? DownloadPromptStatus.DONT_SHOW
+                            : DownloadPromptStatus.SHOW_PREFERENCE);
         }
     }
 
diff --git a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/settings/DownloadSettings.java b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/settings/DownloadSettings.java
index afca352..3396b841 100644
--- a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/settings/DownloadSettings.java
+++ b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/settings/DownloadSettings.java
@@ -19,6 +19,7 @@
 import org.chromium.chrome.browser.preferences.Pref;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.profiles.ProfileKey;
+import org.chromium.chrome.browser.settings.ChromeManagedPreferenceDelegate;
 import org.chromium.components.browser_ui.settings.ChromeSwitchPreference;
 import org.chromium.components.browser_ui.settings.SettingsUtils;
 import org.chromium.components.prefs.PrefService;
@@ -27,6 +28,7 @@
 /**
  * Fragment containing Download settings.
  */
+// TODO(xingliu): Add a test for this.
 public class DownloadSettings
         extends PreferenceFragmentCompat implements Preference.OnPreferenceChangeListener {
     static final String PREF_LOCATION_CHANGE = "location_change";
@@ -50,7 +52,8 @@
                 (ChromeSwitchPreference) findPreference(PREF_DOWNLOAD_LATER_PROMPT_ENABLED);
         mDownloadLaterPromptEnabledPref.setOnPreferenceChangeListener(this);
 
-        if (!ChromeFeatureList.isEnabled(ChromeFeatureList.DOWNLOAD_LATER)) {
+        boolean locationManaged = DownloadDialogBridge.isLocationDialogManaged();
+        if (locationManaged || !ChromeFeatureList.isEnabled(ChromeFeatureList.DOWNLOAD_LATER)) {
             getPreferenceScreen().removePreference(
                     findPreference(PREF_DOWNLOAD_LATER_PROMPT_ENABLED));
         }
@@ -58,6 +61,13 @@
         mLocationPromptEnabledPref =
                 (ChromeSwitchPreference) findPreference(PREF_LOCATION_PROMPT_ENABLED);
         mLocationPromptEnabledPref.setOnPreferenceChangeListener(this);
+        mLocationPromptEnabledPref.setManagedPreferenceDelegate(
+                new ChromeManagedPreferenceDelegate() {
+                    @Override
+                    public boolean isPreferenceControlledByPolicy(Preference preference) {
+                        return DownloadDialogBridge.isLocationDialogManaged();
+                    }
+                });
         mLocationChangePref = (DownloadLocationPreference) findPreference(PREF_LOCATION_CHANGE);
 
         if (PrefetchConfiguration.isPrefetchingFlagEnabled()) {
@@ -100,10 +110,17 @@
                     !(downloadLaterPromptStatus == DownloadLaterPromptStatus.DONT_SHOW));
         }
 
-        // Location prompt is marked enabled if the prompt status is not DONT_SHOW.
-        boolean isLocationPromptEnabled = DownloadDialogBridge.getPromptForDownloadAndroid()
-                != DownloadPromptStatus.DONT_SHOW;
-        mLocationPromptEnabledPref.setChecked(isLocationPromptEnabled);
+        if (DownloadDialogBridge.isLocationDialogManaged()) {
+            // Location prompt can be controlled by the enterprise policy.
+            mLocationPromptEnabledPref.setChecked(
+                    DownloadDialogBridge.getPromptForDownloadPolicy());
+        } else {
+            // Location prompt is marked enabled if the prompt status is not DONT_SHOW.
+            boolean isLocationPromptEnabled = DownloadDialogBridge.getPromptForDownloadAndroid()
+                    != DownloadPromptStatus.DONT_SHOW;
+            mLocationPromptEnabledPref.setChecked(isLocationPromptEnabled);
+            mLocationPromptEnabledPref.setEnabled(true);
+        }
 
         if (mPrefetchingEnabled != null) {
             mPrefetchingEnabled.setChecked(PrefetchConfiguration.isPrefetchingEnabledInSettings(
diff --git a/chrome/browser/download/download_prefs.cc b/chrome/browser/download/download_prefs.cc
index 72eb2b0..f5ce4b6 100644
--- a/chrome/browser/download/download_prefs.cc
+++ b/chrome/browser/download/download_prefs.cc
@@ -395,6 +395,10 @@
 
 // Return the Android prompt for download only.
 #if defined(OS_ANDROID)
+  // Use |prompt_for_download_| preference for enterprise policy.
+  if (prompt_for_download_.IsManaged())
+    return prompt_for_download_.GetValue();
+
   // As long as they haven't indicated in preferences they do not want the
   // dialog shown, show the dialog.
   return *prompt_for_download_android_ !=
@@ -406,6 +410,9 @@
 
 bool DownloadPrefs::PromptDownloadLater() const {
 #ifdef OS_ANDROID
+  if (prompt_for_download_.IsManaged())
+    return false;
+
   if (base::FeatureList::IsEnabled(download::features::kDownloadLater)) {
     return *prompt_for_download_later_ !=
            static_cast<int>(DownloadLaterPromptStatus::kDontShow);
diff --git a/chrome/browser/download/download_prefs_unittest.cc b/chrome/browser/download/download_prefs_unittest.cc
index b53026a9..afb61dc0 100644
--- a/chrome/browser/download/download_prefs_unittest.cc
+++ b/chrome/browser/download/download_prefs_unittest.cc
@@ -496,6 +496,33 @@
   EXPECT_TRUE(prefs.HasDownloadLaterPromptShown());
 }
 
+// Verfies the returned value of PromptForDownload() and PromptDownloadLater()
+// when prefs::kPromptForDownload is managed by enterprise policy,
+TEST(DownloadPrefsTest, ManagedPromptForDownload) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(download::features::kDownloadLater);
+
+  content::BrowserTaskEnvironment task_environment_;
+  TestingProfile profile;
+  profile.GetTestingPrefService()->SetManagedPref(
+      prefs::kPromptForDownload, std::make_unique<base::Value>(true));
+  DownloadPrefs prefs(&profile);
+
+  profile.GetPrefs()->SetInteger(
+      prefs::kDownloadLaterPromptStatus,
+      static_cast<int>(DownloadLaterPromptStatus::kShowPreference));
+  profile.GetPrefs()->SetInteger(
+      prefs::kPromptForDownloadAndroid,
+      static_cast<int>(DownloadPromptStatus::DONT_SHOW));
+  EXPECT_FALSE(prefs.PromptDownloadLater());
+  EXPECT_TRUE(prefs.PromptForDownload());
+
+  profile.GetTestingPrefService()->SetManagedPref(
+      prefs::kPromptForDownload, std::make_unique<base::Value>(false));
+  EXPECT_FALSE(prefs.PromptDownloadLater());
+  EXPECT_FALSE(prefs.PromptForDownload());
+}
+
 #endif  // OS_ANDROID
 
 }  // namespace
diff --git a/chrome/browser/enterprise/connectors/device_trust/BUILD.gn b/chrome/browser/enterprise/connectors/device_trust/BUILD.gn
index 12c5e4a..d7849d7a 100644
--- a/chrome/browser/enterprise/connectors/device_trust/BUILD.gn
+++ b/chrome/browser/enterprise/connectors/device_trust/BUILD.gn
@@ -8,7 +8,7 @@
   proto_in_dir =
       "//chrome/browser/enterprise/connectors/device_trust/attestation_protos"
   proto_out_dir = "chrome/browser/enterprise/connectors/device_trust/"
-  sources = [ "${proto_in_dir}/keystore.proto" ]
+  sources = [ "${proto_in_dir}/device_trust_keystore.proto" ]
   generate_python = false
 }
 
@@ -16,7 +16,7 @@
   proto_in_dir =
       "//chrome/browser/enterprise/connectors/device_trust/attestation_protos"
   proto_out_dir = "chrome/browser/enterprise/connectors/device_trust/"
-  sources = [ "${proto_in_dir}/attestation_ca.proto" ]
+  sources = [ "${proto_in_dir}/device_trust_attestation_ca.proto" ]
   generate_python = false
 }
 
@@ -24,7 +24,7 @@
   proto_in_dir =
       "//chrome/browser/enterprise/connectors/device_trust/attestation_protos"
   proto_out_dir = "chrome/browser/enterprise/connectors/device_trust/"
-  sources = [ "${proto_in_dir}/interface.proto" ]
+  sources = [ "${proto_in_dir}/device_trust_interface.proto" ]
   deps = [
     ":attestation_ca_proto",
     ":keystore_proto",
@@ -36,6 +36,6 @@
   proto_in_dir =
       "//chrome/browser/enterprise/connectors/device_trust/attestation_protos"
   proto_out_dir = "chrome/browser/enterprise/connectors/device_trust/"
-  sources = [ "${proto_in_dir}/google_key.proto" ]
+  sources = [ "${proto_in_dir}/device_trust_google_key.proto" ]
   generate_python = false
 }
diff --git a/chrome/browser/enterprise/connectors/device_trust/attestation_protos/attestation_ca.proto b/chrome/browser/enterprise/connectors/device_trust/attestation_protos/device_trust_attestation_ca.proto
similarity index 99%
rename from chrome/browser/enterprise/connectors/device_trust/attestation_protos/attestation_ca.proto
rename to chrome/browser/enterprise/connectors/device_trust/attestation_protos/device_trust_attestation_ca.proto
index e3fac270..bcf7478 100644
--- a/chrome/browser/enterprise/connectors/device_trust/attestation_protos/attestation_ca.proto
+++ b/chrome/browser/enterprise/connectors/device_trust/attestation_protos/device_trust_attestation_ca.proto
@@ -10,7 +10,7 @@
 
 option optimize_for = LITE_RUNTIME;
 
-package attestation;
+package enterprise_connectors;
 
 // Enumerates various certificate profiles supported by the Attestation CA.
 enum CertificateProfile {
diff --git a/chrome/browser/enterprise/connectors/device_trust/attestation_protos/google_key.proto b/chrome/browser/enterprise/connectors/device_trust/attestation_protos/device_trust_google_key.proto
similarity index 96%
rename from chrome/browser/enterprise/connectors/device_trust/attestation_protos/google_key.proto
rename to chrome/browser/enterprise/connectors/device_trust/attestation_protos/device_trust_google_key.proto
index 8fd316e..2b2f04e 100644
--- a/chrome/browser/enterprise/connectors/device_trust/attestation_protos/google_key.proto
+++ b/chrome/browser/enterprise/connectors/device_trust/attestation_protos/device_trust_google_key.proto
@@ -10,7 +10,7 @@
 
 option optimize_for = LITE_RUNTIME;
 
-package attestation;
+package enterprise_connectors;
 
 // The RSA public key of Google key used by attestation service. Only used
 // internally for attestation service, this message is specialized to contain a
diff --git a/chrome/browser/enterprise/connectors/device_trust/attestation_protos/interface.proto b/chrome/browser/enterprise/connectors/device_trust/attestation_protos/device_trust_interface.proto
similarity index 99%
rename from chrome/browser/enterprise/connectors/device_trust/attestation_protos/interface.proto
rename to chrome/browser/enterprise/connectors/device_trust/attestation_protos/device_trust_interface.proto
index 2209dae..fed1ba20 100644
--- a/chrome/browser/enterprise/connectors/device_trust/attestation_protos/interface.proto
+++ b/chrome/browser/enterprise/connectors/device_trust/attestation_protos/device_trust_interface.proto
@@ -10,10 +10,10 @@
 
 option optimize_for = LITE_RUNTIME;
 
-import "attestation_ca.proto";
-import "keystore.proto";
+import "device_trust_attestation_ca.proto";
+import "device_trust_keystore.proto";
 
-package attestation;
+package enterprise_connectors;
 
 enum AttestationStatus {
   STATUS_SUCCESS = 0;
diff --git a/chrome/browser/enterprise/connectors/device_trust/attestation_protos/keystore.proto b/chrome/browser/enterprise/connectors/device_trust/attestation_protos/device_trust_keystore.proto
similarity index 94%
rename from chrome/browser/enterprise/connectors/device_trust/attestation_protos/keystore.proto
rename to chrome/browser/enterprise/connectors/device_trust/attestation_protos/device_trust_keystore.proto
index b51c08b5..966a870 100644
--- a/chrome/browser/enterprise/connectors/device_trust/attestation_protos/keystore.proto
+++ b/chrome/browser/enterprise/connectors/device_trust/attestation_protos/device_trust_keystore.proto
@@ -10,7 +10,7 @@
 
 option optimize_for = LITE_RUNTIME;
 
-package attestation;
+package enterprise_connectors;
 
 // Describes key type.
 enum KeyType {
diff --git a/chrome/browser/enterprise/connectors/device_trust/attestation_service.cc b/chrome/browser/enterprise/connectors/device_trust/attestation_service.cc
index c852b57..d668ff5 100644
--- a/chrome/browser/enterprise/connectors/device_trust/attestation_service.cc
+++ b/chrome/browser/enterprise/connectors/device_trust/attestation_service.cc
@@ -5,14 +5,17 @@
 #include "chrome/browser/enterprise/connectors/device_trust/attestation_service.h"
 
 #include "base/base64.h"
+#include "base/bind.h"
 #include "base/json/json_reader.h"
-#include "base/json/json_string_value_serializer.h"
 #include "base/json/json_writer.h"
 #include "base/logging.h"
+#include "base/task/task_traits.h"
+#include "base/task/thread_pool.h"
 #include "base/values.h"
+#include "chrome/browser/enterprise/connectors/device_trust/crypto_utility.h"
 #include "chrome/browser/enterprise/connectors/device_trust/device_trust_key_pair.h"
 
-namespace attestation {
+namespace enterprise_connectors {
 
 AttestationService::AttestationService() {
 #if defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
@@ -23,9 +26,20 @@
 
 AttestationService::~AttestationService() = default;
 
+bool AttestationService::ChallengeComesFromVerifiedAccess(
+    const std::string& serialized_signed_data,
+    const std::string& public_key_modulus_hex) {
+  SignedData signed_challenge;
+  signed_challenge.ParseFromString(serialized_signed_data);
+  // Verify challenge signature.
+  return enterprise_connector::CryptoUtility::VerifySignatureUsingHexKey(
+      public_key_modulus_hex, signed_challenge.data(),
+      signed_challenge.signature());
+}
+
 std::string AttestationService::JsonChallengeToProtobufChallenge(
     const std::string& challenge) {
-  attestation::SignedData signed_challenge;
+  SignedData signed_challenge;
   // Get challenge and decode it.
   absl::optional<base::Value> data = base::JSONReader::Read(
       challenge, base::JSONParserOptions::JSON_ALLOW_TRAILING_COMMAS);
@@ -137,4 +151,55 @@
   return true;
 }
 
-}  // namespace attestation
+void AttestationService::BuildChallengeResponseForVAChallenge(
+    const std::string& challenge,
+    AttestationCallback callback) {
+  std::string serialized_signed_data =
+      JsonChallengeToProtobufChallenge(challenge);
+
+  AttestationCallback reply = base::BindOnce(
+      &AttestationService::PaserChallengeResponseAndRunCallback,
+      weak_factory_.GetWeakPtr(), challenge, std::move(callback));
+
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
+      base::BindOnce(
+          &AttestationService::VerifyChallengeAndMaybeCreateChallengeResponse,
+          base::Unretained(this), JsonChallengeToProtobufChallenge(challenge),
+          google_keys_.va_signing_key(VAType::DEFAULT_VA).modulus_in_hex()),
+      std::move(reply));
+}
+
+std::string AttestationService::VerifyChallengeAndMaybeCreateChallengeResponse(
+    const std::string& serialized_signed_data,
+    const std::string& public_key_modulus_hex) {
+  if (!ChallengeComesFromVerifiedAccess(serialized_signed_data,
+                                        public_key_modulus_hex)) {
+    LOG(ERROR) << "Challenge signature verification did not succeed.";
+    return std::string();
+  }
+  // If the verification that the challenge comes from Verified Access succeed,
+  // generate the challenge response.
+  SignEnterpriseChallengeRequest request;
+  SignEnterpriseChallengeReply result;
+  request.set_challenge(serialized_signed_data);
+  SignEnterpriseChallenge(request, &result);
+  return result.challenge_response();
+}
+
+void AttestationService::PaserChallengeResponseAndRunCallback(
+    const std::string& challenge,
+    AttestationCallback callback,
+    const std::string& challenge_response_proto) {
+  if (challenge_response_proto != std::string()) {
+    // Return to callback (throttle with the challenge response) with empty
+    // challenge response.
+    std::move(callback).Run(
+        ProtobufChallengeToJsonChallenge(challenge_response_proto));
+  } else {
+    // Make challenge response
+    std::move(callback).Run("");
+  }
+}
+
+}  // namespace enterprise_connectors
diff --git a/chrome/browser/enterprise/connectors/device_trust/attestation_service.h b/chrome/browser/enterprise/connectors/device_trust/attestation_service.h
index 206a5b9..5183d08 100644
--- a/chrome/browser/enterprise/connectors/device_trust/attestation_service.h
+++ b/chrome/browser/enterprise/connectors/device_trust/attestation_service.h
@@ -5,18 +5,16 @@
 #ifndef CHROME_BROWSER_ENTERPRISE_CONNECTORS_DEVICE_TRUST_ATTESTATION_SERVICE_H_
 #define CHROME_BROWSER_ENTERPRISE_CONNECTORS_DEVICE_TRUST_ATTESTATION_SERVICE_H_
 
+#include "base/bind.h"
 #include "build/build_config.h"
-#include "chrome/browser/enterprise/connectors/device_trust/attestation_ca.pb.h"
-#include "chrome/browser/enterprise/connectors/device_trust/interface.pb.h"
+#include "chrome/browser/enterprise/connectors/device_trust/device_trust_attestation_ca.pb.h"
+#include "chrome/browser/enterprise/connectors/device_trust/device_trust_interface.pb.h"
+#include "chrome/browser/enterprise/connectors/device_trust/google_keys.h"
 
 namespace enterprise_connectors {
 
 class DeviceTrustKeyPair;
 
-}
-
-namespace attestation {
-
 // This class is in charge of handling the key pair used for attestation. Also
 // provides the methods needed in the handshake between Chrome, an IdP and
 // Verified Access.
@@ -25,6 +23,8 @@
 // `SignEnterpriseChallengeReply`.
 class AttestationService {
  public:
+  using AttestationCallback = base::OnceCallback<void(const std::string&)>;
+
   AttestationService();
   ~AttestationService();
 
@@ -64,6 +64,21 @@
   std::string ProtobufChallengeToJsonChallenge(
       const std::string& challenge_response);
 
+  // Verify challenge comes from Verify Access.
+  bool ChallengeComesFromVerifiedAccess(
+      const std::string& serialized_signed_data,
+      const std::string& public_key_modulus_hex);
+
+  // Returns the challenge response proto.
+  std::string VerifyChallengeAndMaybeCreateChallengeResponse(
+      const std::string& serialized_signed_data,
+      const std::string& public_key_modulus_hex);
+
+  // If the challenge comes from Verified Access, generate the
+  // proper challenge response, otherwise reply with empty string.
+  void BuildChallengeResponseForVAChallenge(const std::string& challenge,
+                                            AttestationCallback callback);
+
  private:
   // Sign the challenge and return the challenge response in
   // `result.challenge_response`.
@@ -74,11 +89,19 @@
   // Sign `data` using `key_pair_` and store that value in `signature`.
   bool SignChallengeData(const std::string& data, std::string* response);
 
+  void PaserChallengeResponseAndRunCallback(
+      const std::string& challenge,
+      AttestationCallback callback,
+      const std::string& challenge_response_proto);
+
 #if defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
   std::unique_ptr<enterprise_connectors::DeviceTrustKeyPair> key_pair_;
 #endif  // defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
+
+  GoogleKeys google_keys_;
+  base::WeakPtrFactory<AttestationService> weak_factory_{this};
 };
 
-}  // namespace attestation
+}  // namespace enterprise_connectors
 
 #endif  // CHROME_BROWSER_ENTERPRISE_CONNECTORS_DEVICE_TRUST_ATTESTATION_SERVICE_H_
diff --git a/chrome/browser/enterprise/connectors/device_trust/attestation_service_unittest.cc b/chrome/browser/enterprise/connectors/device_trust/attestation_service_unittest.cc
index 2ec5e3b4..ccd06e1 100644
--- a/chrome/browser/enterprise/connectors/device_trust/attestation_service_unittest.cc
+++ b/chrome/browser/enterprise/connectors/device_trust/attestation_service_unittest.cc
@@ -6,7 +6,6 @@
 
 #include "base/base64.h"
 #include "base/json/json_reader.h"
-#include "base/logging.h"
 #include "base/values.h"
 #include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
@@ -34,7 +33,7 @@
 
 }  // namespace
 
-namespace attestation {
+namespace enterprise_connectors {
 
 class AttestationServiceTest : public testing::Test {
  public:
@@ -84,4 +83,4 @@
             std::string());
 }
 
-}  // namespace attestation
+}  // namespace enterprise_connectors
diff --git a/chrome/browser/enterprise/connectors/device_trust/crypto_utility.cc b/chrome/browser/enterprise/connectors/device_trust/crypto_utility.cc
new file mode 100644
index 0000000..bd56f4a
--- /dev/null
+++ b/chrome/browser/enterprise/connectors/device_trust/crypto_utility.cc
@@ -0,0 +1,81 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/enterprise/connectors/device_trust/crypto_utility.h"
+
+#include "base/containers/span.h"
+#include "base/logging.h"
+#include "crypto/signature_verifier.h"
+#include "third_party/boringssl/src/include/openssl/bn.h"
+#include "third_party/boringssl/src/include/openssl/bytestring.h"
+#include "third_party/boringssl/src/include/openssl/evp.h"
+#include "third_party/boringssl/src/include/openssl/mem.h"
+#include "third_party/boringssl/src/include/openssl/rsa.h"
+
+namespace enterprise_connector {
+
+namespace {
+
+const unsigned int kWellKnownExponent = 65537;
+
+bool CreatePublicKeyFromHex(const std::string& public_key_modulus_hex,
+                            std::vector<uint8_t>& public_key_info) {
+  bssl::UniquePtr<RSA> rsa(RSA_new());
+  bssl::UniquePtr<BIGNUM> n(BN_new());
+  bssl::UniquePtr<BIGNUM> e(BN_new());
+  if (!rsa || !e || !n) {
+    LOG(ERROR) << __func__ << ": Failed to allocate RSA or BIGNUMs.";
+    return false;
+  }
+  BIGNUM* pn = n.get();
+  if (!BN_set_word(e.get(), kWellKnownExponent) ||
+      !BN_hex2bn(&pn, public_key_modulus_hex.c_str())) {
+    LOG(ERROR) << __func__ << ": Failed to generate exponent or modulus.";
+    return false;
+  }
+  if (!RSA_set0_key(rsa.get(), n.release(), e.release(), nullptr)) {
+    LOG(ERROR) << __func__ << ": Failed to set exponent or modulus.";
+    return false;
+  }
+
+  bssl::UniquePtr<EVP_PKEY> public_key(EVP_PKEY_new());
+  EVP_PKEY_assign_RSA(public_key.get(), rsa.release());
+
+  uint8_t* der;
+  size_t der_len;
+  bssl::ScopedCBB cbb;
+  if (!CBB_init(cbb.get(), 0) ||
+      !EVP_marshal_public_key(cbb.get(), public_key.get()) ||
+      !CBB_finish(cbb.get(), &der, &der_len)) {
+    return false;
+  }
+  public_key_info.assign(der, der + der_len);
+  OPENSSL_free(der);
+
+  return true;
+}
+
+}  // namespace
+
+// static
+bool CryptoUtility::VerifySignatureUsingHexKey(
+    const std::string& public_key_modulus_hex,
+    const std::string& data,
+    const std::string& signature) {
+  std::vector<uint8_t> public_key_info;
+  if (!CreatePublicKeyFromHex(public_key_modulus_hex, public_key_info)) {
+    return false;
+  }
+
+  crypto::SignatureVerifier verifier;
+  if (!verifier.VerifyInit(crypto::SignatureVerifier::RSA_PKCS1_SHA256,
+                           base::as_bytes(base::make_span(signature)),
+                           base::as_bytes(base::make_span(public_key_info))))
+    return false;
+
+  verifier.VerifyUpdate(base::as_bytes(base::make_span(data)));
+  return verifier.VerifyFinal();
+}
+
+}  // namespace enterprise_connector
diff --git a/chrome/browser/enterprise/connectors/device_trust/crypto_utility.h b/chrome/browser/enterprise/connectors/device_trust/crypto_utility.h
new file mode 100644
index 0000000..6507184fa
--- /dev/null
+++ b/chrome/browser/enterprise/connectors/device_trust/crypto_utility.h
@@ -0,0 +1,26 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_ENTERPRISE_CONNECTORS_DEVICE_TRUST_CRYPTO_UTILITY_H_
+#define CHROME_BROWSER_ENTERPRISE_CONNECTORS_DEVICE_TRUST_CRYPTO_UTILITY_H_
+
+#include "chrome/browser/enterprise/connectors/device_trust/device_trust_attestation_ca.pb.h"
+
+namespace enterprise_connector {
+
+// A class which provides helpers for cryptography-related tasks.
+class CryptoUtility {
+ public:
+  // Verifies a PKCS #1 v1.5 SHA-256 |signature| over |data| with digest
+  // algorithm |digest_nid|. The |public_key_hex| contains a modulus in hex
+  // format.
+  static bool VerifySignatureUsingHexKey(
+      const std::string& public_key_modulus_hex,
+      const std::string& data,
+      const std::string& signature);
+};
+
+}  // namespace enterprise_connector
+
+#endif  // CHROME_BROWSER_ENTERPRISE_CONNECTORS_DEVICE_TRUST_CRYPTO_UTILITY_H_
diff --git a/chrome/browser/enterprise/connectors/device_trust/device_trust_service.cc b/chrome/browser/enterprise/connectors/device_trust/device_trust_service.cc
index d2c28d1..8cccc4d 100644
--- a/chrome/browser/enterprise/connectors/device_trust/device_trust_service.cc
+++ b/chrome/browser/enterprise/connectors/device_trust/device_trust_service.cc
@@ -11,27 +11,21 @@
 #include "base/values.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/enterprise/connectors/connectors_prefs.h"
-#include "chrome/browser/enterprise/connectors/device_trust/attestation_ca.pb.h"
-#include "chrome/browser/enterprise/connectors/device_trust/interface.pb.h"
+#include "chrome/browser/enterprise/connectors/device_trust/device_trust_attestation_ca.pb.h"
+#include "chrome/browser/enterprise/connectors/device_trust/device_trust_interface.pb.h"
 #include "chrome/browser/enterprise/connectors/device_trust/signal_reporter.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/common/pref_names.h"
 
 namespace enterprise_connectors {
 
-DeviceTrustService::DeviceTrustService() = default;
-
 DeviceTrustService::DeviceTrustService(Profile* profile)
     : prefs_(profile->GetPrefs()),
       first_report_sent_(false),
       signal_report_callback_(
           base::BindOnce(&DeviceTrustService::OnSignalReported,
                          base::Unretained(this))) {
-#if defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
-  key_pair_ = std::make_unique<DeviceTrustKeyPair>();
-#endif  // defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
-
-  attestation_service_ = std::make_unique<attestation::AttestationService>();
+  attestation_service_ = std::make_unique<AttestationService>();
   pref_observer_.Init(prefs_);
   pref_observer_.Add(kContextAwareAccessSignalsAllowlistPref,
                      base::BindRepeating(&DeviceTrustService::OnPolicyUpdated,
@@ -69,9 +63,6 @@
   }
 
   if (!first_report_sent_ && IsEnabled()) {  // Policy enabled for the 1st time.
-#if defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
-    key_pair_->Init();
-#endif  // defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
     reporter_->Init(MakePolicyCheck(),
                     base::BindOnce(&DeviceTrustService::OnReporterInitialized,
                                    weak_factory_.GetWeakPtr()));
@@ -95,7 +86,7 @@
   auto* credential = report.mutable_attestation_credential();
   credential->set_format(
       DeviceTrustReportEvent::Credential::EC_NID_X9_62_PRIME256V1_PUBLIC_DER);
-  credential->set_credential(key_pair_->ExportPEMPublicKey());
+  credential->set_credential(attestation_service_->ExportPEMPublicKey());
 #endif  // defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
 
   reporter_->SendReport(&report, std::move(signal_report_callback_));
@@ -128,21 +119,14 @@
 
 #if defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
 std::string DeviceTrustService::GetAttestationCredentialForTesting() const {
-  return key_pair_->ExportPEMPublicKey();
+  return attestation_service_->ExportPEMPublicKey();
 }
 #endif  // defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
 
-std::string DeviceTrustService::BuildChallengeResponse(
-    const std::string& challenge) {
-  ::attestation::SignEnterpriseChallengeRequest request;
-  ::attestation::SignEnterpriseChallengeReply result;
-  // Get the challenge from the SignedData json and create request.
-  request.set_challenge(
-      attestation_service_->JsonChallengeToProtobufChallenge(challenge));
-  attestation_service_->SignEnterpriseChallenge(request, &result);
-
-  return attestation_service_->ProtobufChallengeToJsonChallenge(
-      result.challenge_response());
+void DeviceTrustService::BuildChallengeResponse(const std::string& challenge,
+                                                AttestationCallback callback) {
+  attestation_service_->BuildChallengeResponseForVAChallenge(
+      challenge, std::move(callback));
 }
 
 }  // namespace enterprise_connectors
diff --git a/chrome/browser/enterprise/connectors/device_trust/device_trust_service.h b/chrome/browser/enterprise/connectors/device_trust/device_trust_service.h
index 40a0698..c0b81b8 100644
--- a/chrome/browser/enterprise/connectors/device_trust/device_trust_service.h
+++ b/chrome/browser/enterprise/connectors/device_trust/device_trust_service.h
@@ -10,15 +10,10 @@
 #include "components/keyed_service/core/keyed_service.h"
 #include "components/policy/core/browser/configuration_policy_handler.h"
 #include "components/prefs/pref_change_registrar.h"
-#include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/pref_service.h"
 
 #include <memory>
 
-#if defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
-#include "chrome/browser/enterprise/connectors/device_trust/device_trust_key_pair.h"
-#endif  // defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
-
 class KeyedService;
 class Profile;
 class PrefService;
@@ -30,8 +25,12 @@
 
 class DeviceTrustService : public KeyedService {
  public:
+  using AttestationCallback = base::OnceCallback<void(const std::string&)>;
+
+  DeviceTrustService() = delete;
   DeviceTrustService(const DeviceTrustService&) = delete;
   DeviceTrustService& operator=(const DeviceTrustService&) = delete;
+  ~DeviceTrustService() override;
 
   // Check if DeviceTrustService is enabled via prefs with non-empty allowlist.
   bool IsEnabled() const;
@@ -48,14 +47,13 @@
 
   // Starts flow that actually builds a response. This method is called
   // from a non_UI thread.
-  std::string BuildChallengeResponse(const std::string& challenge);
+  void BuildChallengeResponse(const std::string& challenge,
+                              AttestationCallback callback);
 
  private:
   friend class DeviceTrustFactory;
 
-  DeviceTrustService();
   explicit DeviceTrustService(Profile* profile);
-  ~DeviceTrustService() override;
 
   void Shutdown() override;
 
@@ -67,16 +65,12 @@
 
   PrefService* prefs_;
 
-#if defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
-  std::unique_ptr<DeviceTrustKeyPair> key_pair_;
-#endif  // defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC)
-
   PrefChangeRegistrar pref_observer_;
   bool first_report_sent_;
 
   std::unique_ptr<enterprise_connectors::DeviceTrustSignalReporter> reporter_;
   SignalReportCallback signal_report_callback_;
-  std::unique_ptr<attestation::AttestationService> attestation_service_;
+  std::unique_ptr<AttestationService> attestation_service_;
   base::WeakPtrFactory<DeviceTrustService> weak_factory_{this};
 };
 
diff --git a/chrome/browser/enterprise/connectors/device_trust/google_keys.cc b/chrome/browser/enterprise/connectors/device_trust/google_keys.cc
new file mode 100644
index 0000000..77e0229
--- /dev/null
+++ b/chrome/browser/enterprise/connectors/device_trust/google_keys.cc
@@ -0,0 +1,81 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/enterprise/connectors/device_trust/google_keys.h"
+
+#include <base/logging.h>
+
+namespace enterprise_connectors {
+
+namespace {
+// VA server instance for prod.
+// http://test-dvproxy-server.sandbox.google.com
+constexpr char kDefaultVASigningPublicKey[] =
+    "bf7fefa3a661437b26aed0801db64d7ba8b58875c351d3bdc9f653847d4a67b3"
+    "b67479327724d56aa0f71a3f57c2290fdc1ff05df80589715e381dfbbda2c4ac"
+    "114c30d0a73c5b7b2e22178d26d8b65860aa8dd65e1b3d61a07c81de87c1e7e4"
+    "590145624936a011ece10434c1d5d41f917c3dc4b41dd8392479130c4fd6eafc"
+    "3bb4e0dedcc8f6a9c28428bf8fbba8bd6438a325a9d3eabee1e89e838138ad99"
+    "69c292c6d9f6f52522333b84ddf9471ffe00f01bf2de5faa1621f967f49e158b"
+    "f2b305360f886826cc6fdbef11a12b2d6002d70d8d1e8f40e0901ff94c203cb2"
+    "01a36a0bd6e83955f14b494f4f2f17c0c826657b85c25ffb8a73599721fa17ab";
+constexpr char kDefaultVAEncryptionPublicKey[] =
+    "edba5e723da811e41636f792c7a77aef633fbf39b542aa537c93c93eaba7a3b1"
+    "0bc3e484388c13d625ef5573358ec9e7fbeb6baaaa87ca87d93fb61bf5760e29"
+    "6813c435763ed2c81f631e26e3ff1a670261cdc3c39a4640b6bbf4ead3d6587b"
+    "e43ef7f1f08e7596b628ec0b44c9b7ad71c9ee3a1258852c7a986c7614f0c4ec"
+    "f0ce147650a53b6aa9ae107374a2d6d4e7922065f2f6eb537a994372e1936c87"
+    "eb08318611d44daf6044f8527687dc7ce5319b51eae6ab12bee6bd16e59c499e"
+    "fa53d80232ae886c7ee9ad8bc1cbd6e4ac55cb8fa515671f7e7ad66e98769f52"
+    "c3c309f98bf08a3b8fbb0166e97906151b46402217e65c5d01ddac8514340e8b";
+
+// VA server instance for QA.
+// https://qa-dvproxy-server-gws.sandbox.google.com
+constexpr char kTestVASigningPublicKey[] =
+    "baab3e277518c65b1b98290bb55061df9a50b9f32a4b0ff61c7c61c51e966fcd"
+    "c891799a39ee0b7278f204a2b45a7e615080ff8f69f668e05adcf3486b319f80"
+    "f9da814d9b86b16a3e68b4ce514ab5591112838a68dc3bfdcc4043a5aa8de52c"
+    "ae936847a271971ecaa188172692c13f3b0321239c90559f3b7ba91e66d38ef4"
+    "db4c75104ac5f2f15e55a463c49753a88e56906b1725fd3f0c1372beb16d4904"
+    "752c74452b0c9f757ee12877a859dd0666cafaccbfc33fe67d98a89a2c12ef52"
+    "5e4b16ea8972577dbfc567c2625a3eee6bcaa6cb4939b941f57236d1d57243f8"
+    "c9766938269a8034d82fbd44044d2ee6a5c7275589afc3790b60280c0689900f";
+constexpr char kTestVAEncryptionPublicKey[] =
+    "c0c116e7ded8d7c1e577f9c8fb0d267c3c5c3e3b6800abb0309c248eaa5cd9bf"
+    "91945132e4bb0111711356a388b756788e20bc1ecc9261ea9bcae8369cfd050e"
+    "d8dc00b50fbe36d2c1c8a9b335f2e11096be76bebce8b5dcb0dc39ac0fd963b0"
+    "51474f794d4289cc0c52d0bab451b9e69a43ecd3a84330b0b2de4365c038ffce"
+    "ec0f1999d789615849c2f3c29d1d9ed42ccb7f330d5b56f40fb7cc6556190c3b"
+    "698c20d83fb341a442fd69701fe0bdc41bdcf8056ccbc8d9b4275e8e43ec6b63"
+    "c1ae70d52838dfa90a9cd9e7b6bd88ed3abf4fab444347104e30e635f4f296ac"
+    "4c91939103e317d0eca5f36c48102e967f176a19a42220f3cf14634b6773be07";
+
+}  // namespace
+
+GoogleKeys ::GoogleKeys() {
+  // No key_id for signing key.
+  va_signing_keys_[DEFAULT_VA].set_modulus_in_hex(kDefaultVASigningPublicKey);
+  va_signing_keys_[TEST_VA].set_modulus_in_hex(kTestVASigningPublicKey);
+
+  va_encryption_keys_[DEFAULT_VA].set_modulus_in_hex(
+      kDefaultVAEncryptionPublicKey);
+  va_encryption_keys_[TEST_VA].set_modulus_in_hex(kTestVAEncryptionPublicKey);
+}
+
+GoogleKeys::GoogleKeys(const DefaultGoogleRsaPublicKeySet& default_key_set)
+    : GoogleKeys() {
+  va_signing_keys_[DEFAULT_VA] = default_key_set.default_va_signing_key();
+  va_encryption_keys_[DEFAULT_VA] = default_key_set.default_va_encryption_key();
+}
+
+GoogleKeys::~GoogleKeys() = default;
+
+const GoogleRsaPublicKey& GoogleKeys::va_signing_key(VAType va_type) const {
+  return va_signing_keys_[va_type];
+}
+const GoogleRsaPublicKey& GoogleKeys::va_encryption_key(VAType va_type) const {
+  return va_encryption_keys_[va_type];
+}
+
+}  // namespace enterprise_connectors
diff --git a/chrome/browser/enterprise/connectors/device_trust/google_keys.h b/chrome/browser/enterprise/connectors/device_trust/google_keys.h
new file mode 100644
index 0000000..2b7babaf
--- /dev/null
+++ b/chrome/browser/enterprise/connectors/device_trust/google_keys.h
@@ -0,0 +1,40 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_ENTERPRISE_CONNECTORS_DEVICE_TRUST_GOOGLE_KEYS_H_
+#define CHROME_BROWSER_ENTERPRISE_CONNECTORS_DEVICE_TRUST_GOOGLE_KEYS_H_
+
+#include <array>
+#include <string>
+
+#include "chrome/browser/enterprise/connectors/device_trust/device_trust_google_key.pb.h"
+#include "chrome/browser/enterprise/connectors/device_trust/device_trust_interface.pb.h"
+
+namespace enterprise_connectors {
+
+// A class that manages the public keys along with their key IDs the attestation
+// service uses.
+class GoogleKeys {
+ public:
+  GoogleKeys();
+  explicit GoogleKeys(const DefaultGoogleRsaPublicKeySet& default_key_set);
+  ~GoogleKeys();
+
+  // Copyable and movable with the default behavior.
+  GoogleKeys(const GoogleKeys&);
+  GoogleKeys& operator=(const GoogleKeys&) = default;
+  GoogleKeys(GoogleKeys&&);
+  GoogleKeys& operator=(GoogleKeys&&) = default;
+
+  const GoogleRsaPublicKey& va_signing_key(VAType va_type) const;
+  const GoogleRsaPublicKey& va_encryption_key(VAType va_type) const;
+
+ private:
+  std::array<GoogleRsaPublicKey, VAType_ARRAYSIZE> va_signing_keys_;
+  std::array<GoogleRsaPublicKey, VAType_ARRAYSIZE> va_encryption_keys_;
+};
+
+}  // namespace enterprise_connectors
+
+#endif  // CHROME_BROWSER_ENTERPRISE_CONNECTORS_DEVICE_TRUST_GOOGLE_KEYS_H_
diff --git a/chrome/browser/enterprise/connectors/device_trust/navigation_throttle.cc b/chrome/browser/enterprise/connectors/device_trust/navigation_throttle.cc
index 09e4182..f485eedb 100644
--- a/chrome/browser/enterprise/connectors/device_trust/navigation_throttle.cc
+++ b/chrome/browser/enterprise/connectors/device_trust/navigation_throttle.cc
@@ -5,14 +5,12 @@
 #include "chrome/browser/enterprise/connectors/device_trust/navigation_throttle.h"
 
 #include "base/memory/ptr_util.h"
-#include "base/task/task_traits.h"
-#include "base/task/thread_pool.h"
 #include "base/values.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/enterprise/connectors/connectors_prefs.h"
 #include "chrome/browser/enterprise/connectors/device_trust/device_trust_factory.h"
+#include "chrome/browser/enterprise/connectors/device_trust/device_trust_interface.pb.h"
 #include "chrome/browser/enterprise/connectors/device_trust/device_trust_service.h"
-#include "chrome/browser/enterprise/connectors/device_trust/interface.pb.h"
 #include "chrome/browser/profiles/profile.h"
 #include "components/policy/core/browser/url_util.h"
 #include "components/prefs/pref_service.h"
@@ -135,40 +133,37 @@
     std::string challenge;
     if (headers->GetNormalizedHeader(kVerifiedAccessChallengeHeader,
                                      &challenge)) {
-      // Post a task to get the challenge response. It will defer the navigation
-      // and it will be resumed after it's built.
-      // `StartSignChallengeAndReplyWithResponse` won't run in the main thread,
-      // and then reply to `ReplyChallengeResponseAndResume` which makes use of
-      // `weak_ptr_factory_.GetWeakPtr()` so it won't run in case the object is
-      // destroyed.
-      AttestationCallback reply = base::BindOnce(
+      // Create callback for `ReplyChallengeResponseAndResume` which will
+      // be called after the challenge response is created. With this
+      // we can defer the navigation to unblock the main thread.
+      AttestationCallback resume_navigation_callback = base::BindOnce(
           &DeviceTrustNavigationThrottle::ReplyChallengeResponseAndResume,
           weak_ptr_factory_.GetWeakPtr());
 
-      base::ThreadPool::PostTaskAndReplyWithResult(
-          FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
-          base::BindOnce(&DeviceTrustNavigationThrottle::
-                             StartSignChallengeAndReplyWithResponse,
-                         base::Unretained(this), challenge),
-          std::move(reply));
+      // Call `DeviceTrustService::BuildChallengeResponse` which is one step on
+      // the chain that builds the challenge response. In this chain we post a
+      // task that won't run in the main thread.
+      device_trust_service_->BuildChallengeResponse(
+          challenge, std::move(resume_navigation_callback));
 
       return DEFER;
     }
+  } else {
+    LOG(ERROR) << "No challenge in the response.";
   }
   return PROCEED;
 }
 
-std::string
-DeviceTrustNavigationThrottle::StartSignChallengeAndReplyWithResponse(
-    const std::string& challenge) {
-  return device_trust_service_->BuildChallengeResponse(challenge);
-}
-
 void DeviceTrustNavigationThrottle::ReplyChallengeResponseAndResume(
-    std::string challenge_response) {
-  navigation_handle()->SetRequestHeader(kVerifiedAccessResponseHeader,
-                                        challenge_response);
-  Resume();
+    const std::string& challenge_response) {
+  if (challenge_response == std::string()) {
+    // Cancel the navigation if challenge signature is invalid.
+    CancelDeferredNavigation(content::NavigationThrottle::CANCEL_AND_IGNORE);
+  } else {
+    navigation_handle()->SetRequestHeader(kVerifiedAccessResponseHeader,
+                                          challenge_response);
+    Resume();
+  }
 }
 
 }  // namespace enterprise_connectors
diff --git a/chrome/browser/enterprise/connectors/device_trust/navigation_throttle.h b/chrome/browser/enterprise/connectors/device_trust/navigation_throttle.h
index ddb946a..5c6ba398 100644
--- a/chrome/browser/enterprise/connectors/device_trust/navigation_throttle.h
+++ b/chrome/browser/enterprise/connectors/device_trust/navigation_throttle.h
@@ -34,7 +34,7 @@
   static std::unique_ptr<DeviceTrustNavigationThrottle> MaybeCreateThrottleFor(
       content::NavigationHandle* navigation_handle);
 
-  using AttestationCallback = base::OnceCallback<void(std::string)>;
+  using AttestationCallback = base::OnceCallback<void(const std::string&)>;
 
   explicit DeviceTrustNavigationThrottle(
       content::NavigationHandle* navigation_handle);
@@ -61,13 +61,7 @@
   // Set `challege_response` into the header
   // `X-Verified-Access-Challenge-Response` of the redirection request to the
   // IdP and resume the navigation.
-  void ReplyChallengeResponseAndResume(std::string challenge_response);
-
-  // Call `DeviceTrustService::BuildChallengeResponse` which is the method that
-  // actually builds the challenge response, and return it as a string with the
-  // format described in `AttestationService::ProtobufChallengeToJsonChallenge`.
-  std::string StartSignChallengeAndReplyWithResponse(
-      const std::string& challenge);
+  void ReplyChallengeResponseAndResume(const std::string& challenge_response);
 
   // The URL matcher created from the ContextAwareAccessSignalsAllowlist policy.
   std::unique_ptr<url_matcher::URLMatcher> matcher_;
diff --git a/chrome/browser/enterprise/connectors/device_trust/navigation_throttle_unittest.cc b/chrome/browser/enterprise/connectors/device_trust/navigation_throttle_unittest.cc
index 2546dfd8..7befb7f2 100644
--- a/chrome/browser/enterprise/connectors/device_trust/navigation_throttle_unittest.cc
+++ b/chrome/browser/enterprise/connectors/device_trust/navigation_throttle_unittest.cc
@@ -14,6 +14,7 @@
 #include "content/public/browser/navigation_throttle.h"
 #include "content/public/test/browser_task_environment.h"
 #include "content/public/test/mock_navigation_handle.h"
+#include "net/http/http_response_headers.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 using content::NavigationThrottle;
@@ -23,6 +24,22 @@
 const base::Value kOrigins[]{base::Value("https://www.example.com"),
                              base::Value("example2.example.com")};
 
+constexpr char challenge[] =
+    "{"
+    "\"challenge\": {"
+    " \"data\": "
+    "\"ChZFbnRlcnByaXNlS2V5Q2hhbGxlbmdlEiB+CPt6kzZVCmxIPc4K5NdVGsTLYVcA0ekaCVq+"
+    "8KbZEBif4oXClC8=\","
+    "  \"signature\": "
+    "\"TiR/Qd/f+V/XFnYPIqeLO6/AXI+SKOnKGJmqhJd06MjnHhRnCK5u/BdFkq2H5U/"
+    "qqAx4DS6SfcLfJ+6NEdsemn/5UTmarOOxWA8Fh2zc2a2Zr1+MGDdgRkckIzA5iw99/"
+    "EV+xIUXyVaqJjSuD9iPSFJzlJUtlhbijf8JT1w8PuuxNOERuhqIrJvpZFpb+"
+    "u99YLuGrpw7y64Bh6AhsXryGjowqXYojYWAOYeHX4b2axkHDsThybI+v+"
+    "ECtVHi3l6Z2TOwr7fkyhoy1Kz9swd30rw6/VDB92jzrJTQoy3rQ2+aY8KxycU/"
+    "nuJn3H6583SsiaTbKgyHKmObbGdt0GVWLQ==\""
+    "}"
+    "}";
+
 }  // namespace
 
 namespace enterprise_connectors {
@@ -78,4 +95,27 @@
   EXPECT_EQ(NavigationThrottle::PROCEED, throttle->WillStartRequest().action());
 }
 
+scoped_refptr<net::HttpResponseHeaders> GetHeaderChallenge(
+    const std::string& challenge) {
+  std::string raw_response_headers =
+      "HTTP/1.1 200 OK\r\n"
+      "x-verified-access-challenge: " +
+      challenge + "\r\n";
+  return base::MakeRefCounted<net::HttpResponseHeaders>(
+      net::HttpUtil::AssembleRawHeaders(raw_response_headers));
+}
+
+TEST_F(DeviceTrustNavigationThrottleTest, BuildChallengeResponseFromHeader) {
+  EnableDeviceTrust();
+  GURL url("https://www.example.com/");
+  content::MockNavigationHandle test_handle(url, main_rfh());
+
+  test_handle.set_response_headers(GetHeaderChallenge(challenge));
+  auto throttle =
+      DeviceTrustNavigationThrottle::MaybeCreateThrottleFor(&test_handle);
+  ASSERT_TRUE(throttle);
+
+  EXPECT_EQ(NavigationThrottle::DEFER, throttle->WillStartRequest().action());
+}
+
 }  // namespace enterprise_connectors
diff --git a/chrome/browser/enterprise/connectors/file_system/box_api_call_flow.cc b/chrome/browser/enterprise/connectors/file_system/box_api_call_flow.cc
index 83760c6..06ee7fa 100644
--- a/chrome/browser/enterprise/connectors/file_system/box_api_call_flow.cc
+++ b/chrome/browser/enterprise/connectors/file_system/box_api_call_flow.cc
@@ -397,7 +397,6 @@
     : folder_id_(folder_id),
       target_file_name_(target_file_name),
       local_file_path_(local_file_path),
-      file_mime_type_(GetMimeType(target_file_name)),
       multipart_boundary_(net::GenerateMimeMultipartBoundary()),
       callback_(std::move(callback)) {}
 
@@ -406,12 +405,6 @@
 void BoxWholeFileUploadApiCallFlow::Start(
     scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
     const std::string& access_token) {
-  // Ensure that file extension was valid and file type was obtained.
-  if (file_mime_type_.empty()) {
-    DLOG(ERROR) << "Couldn't obtain proper file type for " << target_file_name_;
-    std::move(callback_).Run(false, 0, GURL());
-  }
-
   // Forward the arguments via PostReadFileTask() then OnFileRead() into
   // OAuth2CallFlow::Start().
   PostReadFileTask(url_loader_factory, access_token);
@@ -421,7 +414,7 @@
     scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
     const std::string& access_token) {
   auto read_file_task = base::BindOnce(&BoxWholeFileUploadApiCallFlow::ReadFile,
-                                       local_file_path_);
+                                       local_file_path_, target_file_name_);
   auto read_file_reply = base::BindOnce(
       &BoxWholeFileUploadApiCallFlow::OnFileRead, weak_factory_.GetWeakPtr(),
       url_loader_factory, access_token);
@@ -431,27 +424,35 @@
       std::move(read_file_task), std::move(read_file_reply));
 }
 
-absl::optional<std::string> BoxWholeFileUploadApiCallFlow::ReadFile(
-    const base::FilePath& path) {
-  std::string content;
-  return base::ReadFileToStringWithMaxSize(path, &content,
-                                           kWholeFileUploadMaxSize)
-             ? absl::optional<std::string>(std::move(content))
-             : absl::nullopt;
+// static
+absl::optional<BoxWholeFileUploadApiCallFlow::FileRead>
+BoxWholeFileUploadApiCallFlow::ReadFile(
+    const base::FilePath& path,
+    const base::FilePath& target_file_name) {
+  FileRead file_read;
+  file_read.mime = GetMimeType(target_file_name);
+  if (file_read.mime.empty() ||  // Ensure that file extension was valid.
+      !base::ReadFileToStringWithMaxSize(path, &file_read.content,
+                                         kWholeFileUploadMaxSize)) {
+    DLOG(ERROR) << "File " << path << " with target name " << target_file_name;
+    return absl::nullopt;
+  }
+  DCHECK_LE(file_read.content.size(), kWholeFileUploadMaxSize);
+  return absl::optional<FileRead>(std::move(file_read));
 }
 
 void BoxWholeFileUploadApiCallFlow::OnFileRead(
     scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
     const std::string& access_token,
-    absl::optional<std::string> file_read) {
+    absl::optional<FileRead> file_read) {
   if (!file_read) {
     DLOG(ERROR) << "[BoxApiCallFlow] WholeFileUpload read file failed";
     // TODO(https://crbug.com/1165972): error handling
     std::move(callback_).Run(false, 0, GURL());
     return;
   }
-  DCHECK_LE(file_read->size(), kWholeFileUploadMaxSize);
-  file_content_ = std::move(*file_read);
+  DCHECK_LE(file_read->content.size(), kWholeFileUploadMaxSize);
+  file_read_ = std::move(*file_read);
 
   // Continue to the original call flow after file has been read.
   OAuth2ApiCallFlow::Start(url_loader_factory, access_token);
@@ -464,7 +465,7 @@
 std::string BoxWholeFileUploadApiCallFlow::CreateApiCallBody() {
   CHECK(!folder_id_.empty());
   CHECK(!target_file_name_.empty());
-  CHECK(!file_mime_type_.empty());
+  CHECK(!file_read_.mime.empty()) << target_file_name_;
   CHECK(!multipart_boundary_.empty());
 
   base::Value attr(base::Value::Type::DICTIONARY);
@@ -479,8 +480,8 @@
                                   "application/json", &body);
 
   net::AddMultipartValueForUploadWithFileName(
-      "file", target_file_name_.MaybeAsASCII(), file_content_,
-      multipart_boundary_, file_mime_type_, &body);
+      "file", target_file_name_.MaybeAsASCII(), file_read_.content,
+      multipart_boundary_, file_read_.mime, &body);
   net::AddMultipartFinalDelimiterForUpload(multipart_boundary_, &body);
 
   return body;
@@ -516,6 +517,13 @@
   std::move(callback_).Run(false, response_code, GURL());
 }
 
+void BoxWholeFileUploadApiCallFlow::SetFileReadForTesting(
+    std::string content,
+    std::string mime_type) {
+  file_read_.content = std::move(content);
+  file_read_.mime = std::move(mime_type);
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // ChunkedUpload: CreateUploadSession
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/chrome/browser/enterprise/connectors/file_system/box_api_call_flow.h b/chrome/browser/enterprise/connectors/file_system/box_api_call_flow.h
index add472e..cf1a213 100644
--- a/chrome/browser/enterprise/connectors/file_system/box_api_call_flow.h
+++ b/chrome/browser/enterprise/connectors/file_system/box_api_call_flow.h
@@ -157,32 +157,38 @@
                              const network::mojom::URLResponseHead* head,
                              std::unique_ptr<std::string> body) override;
 
+  void SetFileReadForTesting(std::string content, std::string mime_type);
+
  private:
+  struct FileRead {
+    std::string content;
+    std::string mime;
+  };
   // Post a task to ThreadPool to read the local file, forward the
   // parameters from Start() into OnFileRead(), which is the callback that then
   // kicks off OAuth2CallFlow::Start() after file content is read.
   void PostReadFileTask(
       scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
       const std::string& access_token);
-
-  // Helper functions to read and delete the local file.
-  // Task posted to ThreadPool to read the local file. Return type is
-  // absl::optional in case file is read successfully but the file content is
-  // really empty.
-  static absl::optional<std::string> ReadFile(const base::FilePath& path);
   // Callback attached in PostReadFileTask(). Take in read file content and
   // kick off OAuth2CallFlow::Start().
   void OnFileRead(
       scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
       const std::string& access_token,
-      absl::optional<std::string> content);
+      absl::optional<FileRead> file_read);
+
+  // Task posted to ThreadPool to read the local file. Return type is
+  // base::Optional in case file is read successfully but the file content is
+  // really empty.
+  static absl::optional<FileRead> ReadFile(
+      const base::FilePath& path,
+      const base::FilePath& target_file_name);
 
   const std::string folder_id_;
   const base::FilePath target_file_name_;
   const base::FilePath local_file_path_;
-  const std::string file_mime_type_;
   const std::string multipart_boundary_;
-  std::string file_content_;
+  FileRead file_read_;
 
   // Callback from the uploader to report success.
   TaskCallback callback_;
diff --git a/chrome/browser/enterprise/connectors/file_system/box_api_call_flow_unittest.cc b/chrome/browser/enterprise/connectors/file_system/box_api_call_flow_unittest.cc
index ec44371..09ba13f 100644
--- a/chrome/browser/enterprise/connectors/file_system/box_api_call_flow_unittest.cc
+++ b/chrome/browser/enterprise/connectors/file_system/box_api_call_flow_unittest.cc
@@ -17,7 +17,6 @@
 #include "base/strings/stringprintf.h"
 #include "base/test/task_environment.h"
 #include "chrome/browser/enterprise/connectors/file_system/box_api_call_test_helper.h"
-#include "net/base/mime_util.h"
 #include "net/base/net_errors.h"
 #include "net/http/http_status_code.h"
 #include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
@@ -338,6 +337,7 @@
   using BoxWholeFileUploadApiCallFlow::IsExpectedSuccessCode;
   using BoxWholeFileUploadApiCallFlow::ProcessApiCallFailure;
   using BoxWholeFileUploadApiCallFlow::ProcessApiCallSuccess;
+  using BoxWholeFileUploadApiCallFlow::SetFileReadForTesting;
 };
 
 class BoxWholeFileUploadApiCallFlowTest
@@ -361,10 +361,43 @@
       std::move(quit_closure_).Run();
   }
 
-  std::string folder_id_{"13579"};
+  std::string MakeExpectedBody() {
+    // Body format for multipart/form-data reference:
+    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
+    // Request body fields reference:
+    // https://developer.box.com/reference/post-files-content/
+    std::string content_type = flow_->CreateApiCallBodyContentType();
+    std::string expected_type("multipart/form-data; boundary=");
+
+    std::string multipart_boundary =
+        "--" + content_type.substr(expected_type.size());
+    std::string expected_body(multipart_boundary + "\r\n");
+    expected_body +=
+        "Content-Disposition: form-data; name=\"attributes\"\r\n"
+        "Content-Type: application/json\r\n\r\n"
+        "{\"name\":\"";
+    expected_body +=
+        file_name_.AsUTF8Unsafe() +  // AsUTF8Unsafe() to compile on Windows
+        "\","
+        "\"parent\":{\"id\":\"" +
+        folder_id_ + "\"}}\r\n";
+    expected_body += multipart_boundary + "\r\n";
+    expected_body +=
+        "Content-Disposition: form-data; name=\"file\"; filename=\"";
+    expected_body +=
+        file_name_.AsUTF8Unsafe() +  // AsUTF8Unsafe() to compile on Windows
+        "\"\r\nContent-Type: " + mime_type_ + "\r\n\r\n";
+    expected_body += file_content_ + "\r\n";
+    expected_body += multipart_boundary + "--\r\n";
+    return expected_body;
+  }
+
+  base::FilePath file_path_;
+  const std::string folder_id_{"13579"};
+  const std::string mime_type_{"text/plain"};
   const base::FilePath file_name_{
       FILE_PATH_LITERAL("box_whole_file_upload_test.txt")};
-  base::FilePath file_path_;
+  const std::string file_content_{"<TestContent>~~~123456789~~~</TestContent>"};
 
   GURL file_url_;
 
@@ -385,32 +418,8 @@
   std::string expected_type("multipart/form-data; boundary=");
   ASSERT_EQ(content_type.substr(0, expected_type.size()), expected_type);
 
-  // Body format for multipart/form-data reference:
-  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
-  // Request body fields reference:
-  // https://developer.box.com/reference/post-files-content/
-  std::string multipart_boundary =
-      "--" + content_type.substr(expected_type.size());
-  std::string expected_body(multipart_boundary + "\r\n");
-  std::string mime_type;
-  net::GetMimeTypeFromExtension(FILE_PATH_LITERAL("txt"), &mime_type);
-  expected_body +=
-      "Content-Disposition: form-data; name=\"attributes\"\r\n"
-      "Content-Type: application/json\r\n\r\n"
-      "{\"name\":\"";
-  expected_body +=
-      file_name_.AsUTF8Unsafe() +  // AsUTF8Unsafe() to compile on Windows
-      "\","
-      "\"parent\":{\"id\":\"" +
-      folder_id_ + "\"}}\r\n";
-  expected_body += multipart_boundary + "\r\n";
-  expected_body += "Content-Disposition: form-data; name=\"file\"; filename=\"";
-  expected_body +=
-      file_name_.AsUTF8Unsafe() +  // AsUTF8Unsafe() to compile on Windows
-      "\"\r\nContent-Type: " + mime_type + "\r\n\r\n\r\n";
-  expected_body += multipart_boundary + "--\r\n";
-  std::string body = flow_->CreateApiCallBody();
-  ASSERT_EQ(body, expected_body);
+  flow_->SetFileReadForTesting(file_content_, mime_type_);
+  ASSERT_EQ(flow_->CreateApiCallBody(), MakeExpectedBody());
 }
 
 TEST_F(BoxWholeFileUploadApiCallFlowTest, IsExpectedSuccessCode) {
@@ -514,17 +523,29 @@
   BoxWholeFileUploadApiCallFlowFileReadTest()
       : url_factory_(
             base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
-                &test_url_loader_factory_)) {}
+                &test_url_loader_factory_)) {
+    test_url_loader_factory_.SetInterceptor(base::BindRepeating(
+        &BoxWholeFileUploadApiCallFlowFileReadTest::VerifyRequest,
+        base::Unretained(this)));
+  }
 
  protected:
+  void VerifyRequest(const network::ResourceRequest& request) {
+    ASSERT_EQ(request.url, kFileSystemBoxDirectUploadUrl);
+    // Check that file was read and formatted into request body string properly,
+    // without going down the rabbit hole of request.request_body->elements()->
+    // front().As<network::DataElementBytes>().AsStringPiece().
+    ASSERT_EQ(flow_->CreateApiCallBody(), MakeExpectedBody());
+    ++request_sent_count_;
+  }
+
+  size_t request_sent_count_ = 0;
   network::TestURLLoaderFactory test_url_loader_factory_;
   scoped_refptr<network::SharedURLLoaderFactory> url_factory_;
 };
 
 TEST_F(BoxWholeFileUploadApiCallFlowFileReadTest, GoodUpload) {
-  ASSERT_TRUE(
-      base::WriteFile(file_path_, "BoxWholeFileUploadApiCallFlowFileReadTest"))
-      << file_path_;
+  ASSERT_TRUE(base::WriteFile(file_path_, file_content_)) << file_path_;
 
   test_url_loader_factory_.AddResponse(kFileSystemBoxDirectUploadUrl,
                                        std::string(), net::HTTP_CREATED);
@@ -532,9 +553,10 @@
 
   base::RunLoop run_loop;
   quit_closure_ = run_loop.QuitClosure();
-  flow_->Start(url_factory_, "dummytoken");
+  flow_->Start(url_factory_, "test_token");
   run_loop.Run();
 
+  ASSERT_EQ(request_sent_count_, 1U);
   ASSERT_EQ(response_code_, net::HTTP_CREATED);
   ASSERT_TRUE(processed_success_) << "Failed with file " << file_path_;
   ASSERT_TRUE(base::PathExists(file_path_))
@@ -548,11 +570,12 @@
 
   base::RunLoop run_loop;
   quit_closure_ = run_loop.QuitClosure();
-  flow_->Start(url_factory_, "dummytoken");
+  flow_->Start(url_factory_, "test_token");
   run_loop.Run();
 
-  // There should be no HTTP response code, because it should already fail when
-  // reading file, before making any actual API calls.
+  // Because file read already failed before any actual API calls are made,
+  // there should be no API calls made, and therefore no HTTP response code.
+  ASSERT_EQ(request_sent_count_, 0U);
   ASSERT_EQ(response_code_, 0);
   ASSERT_FALSE(processed_success_);
 }
diff --git a/chrome/browser/extensions/api/developer_private/developer_private_api.cc b/chrome/browser/extensions/api/developer_private/developer_private_api.cc
index 87ac6ad..0fa6ecd0 100644
--- a/chrome/browser/extensions/api/developer_private/developer_private_api.cc
+++ b/chrome/browser/extensions/api/developer_private/developer_private_api.cc
@@ -505,8 +505,9 @@
 }
 
 void DeveloperPrivateEventRouter::OnExtensionManagementSettingsChanged() {
-  std::unique_ptr<base::ListValue> args(new base::ListValue());
-  args->Append(DeveloperPrivateAPI::CreateProfileInfo(profile_)->ToValue());
+  std::vector<base::Value> args;
+  args.push_back(base::Value::FromUniquePtrValue(
+      DeveloperPrivateAPI::CreateProfileInfo(profile_)->ToValue()));
 
   std::unique_ptr<Event> event(
       new Event(events::DEVELOPER_PRIVATE_ON_PROFILE_STATE_CHANGED,
@@ -533,8 +534,9 @@
 }
 
 void DeveloperPrivateEventRouter::OnProfilePrefChanged() {
-  std::unique_ptr<base::ListValue> args(new base::ListValue());
-  args->Append(DeveloperPrivateAPI::CreateProfileInfo(profile_)->ToValue());
+  std::vector<base::Value> args;
+  args.push_back(base::Value::FromUniquePtrValue(
+      DeveloperPrivateAPI::CreateProfileInfo(profile_)->ToValue()));
   std::unique_ptr<Event> event(
       new Event(events::DEVELOPER_PRIVATE_ON_PROFILE_STATE_CHANGED,
                 developer::OnProfileStateChanged::kEventName, std::move(args)));
@@ -587,8 +589,8 @@
         std::make_unique<developer::ExtensionInfo>(std::move(infos[0]));
   }
 
-  std::unique_ptr<base::ListValue> args(new base::ListValue());
-  args->Append(event_data.ToValue());
+  std::vector<base::Value> args;
+  args.push_back(base::Value::FromUniquePtrValue(event_data.ToValue()));
   std::unique_ptr<Event> event(
       new Event(events::DEVELOPER_PRIVATE_ON_ITEM_STATE_CHANGED,
                 developer::OnItemStateChanged::kEventName, std::move(args)));
diff --git a/chrome/browser/extensions/api/file_system/file_system_apitest_chromeos.cc b/chrome/browser/extensions/api/file_system/file_system_apitest_chromeos.cc
index 26808fa..a443710 100644
--- a/chrome/browser/extensions/api/file_system/file_system_apitest_chromeos.cc
+++ b/chrome/browser/extensions/api/file_system/file_system_apitest_chromeos.cc
@@ -331,8 +331,8 @@
       GetDriveMountPoint().AppendASCII("root/open_existing.txt");
   FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathForTest(
       &test_file);
-  ASSERT_TRUE(RunExtensionTest({.name = "api_test/file_system/open_existing",
-                                .launch_as_platform_app = true}))
+  ASSERT_TRUE(RunExtensionTest("api_test/file_system/open_existing",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -342,9 +342,8 @@
       GetDriveMountPoint().AppendASCII("root/open_existing.txt");
   FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathForTest(
       &test_file);
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "api_test/file_system/open_existing_with_write",
-                        .launch_as_platform_app = true}))
+  ASSERT_TRUE(RunExtensionTest("api_test/file_system/open_existing_with_write",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -355,9 +354,9 @@
   ASSERT_TRUE(base::PathService::OverrideAndCreateIfNeeded(
       chrome::DIR_USER_DOCUMENTS, test_file.DirName(), true, false));
   FileSystemChooseEntryFunction::SkipPickerAndSelectSuggestedPathForTest();
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "api_test/file_system/open_multiple_with_suggested_name",
-       .launch_as_platform_app = true}))
+  ASSERT_TRUE(
+      RunExtensionTest("api_test/file_system/open_multiple_with_suggested_name",
+                       {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -372,9 +371,8 @@
   test_files.push_back(test_file2);
   FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathsForTest(
       &test_files);
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "api_test/file_system/open_multiple_existing",
-                        .launch_as_platform_app = true}))
+  ASSERT_TRUE(RunExtensionTest("api_test/file_system/open_multiple_existing",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -384,8 +382,8 @@
       GetDriveMountPoint().AppendASCII("root/subdir");
   FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathForTest(
       &test_directory);
-  ASSERT_TRUE(RunExtensionTest({.name = "api_test/file_system/open_directory",
-                                .launch_as_platform_app = true}))
+  ASSERT_TRUE(RunExtensionTest("api_test/file_system/open_directory",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -395,9 +393,8 @@
       GetDriveMountPoint().AppendASCII("root/subdir");
   FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathForTest(
       &test_directory);
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "api_test/file_system/open_directory_with_write",
-       .launch_as_platform_app = true}))
+  ASSERT_TRUE(RunExtensionTest("api_test/file_system/open_directory_with_write",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -407,9 +404,9 @@
       GetDriveMountPoint().AppendASCII("root/subdir");
   FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathForTest(
       &test_directory);
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "api_test/file_system/open_directory_without_permission",
-       .launch_as_platform_app = true}))
+  ASSERT_TRUE(
+      RunExtensionTest("api_test/file_system/open_directory_without_permission",
+                       {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -419,9 +416,9 @@
       GetDriveMountPoint().AppendASCII("root/subdir");
   FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathForTest(
       &test_directory);
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "api_test/file_system/open_directory_with_only_write",
-       .launch_as_platform_app = true}))
+  ASSERT_TRUE(
+      RunExtensionTest("api_test/file_system/open_directory_with_only_write",
+                       {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -431,8 +428,8 @@
       GetDriveMountPoint().AppendASCII("root/save_new.txt");
   FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathForTest(
       &test_file);
-  ASSERT_TRUE(RunExtensionTest({.name = "api_test/file_system/save_new",
-                                .launch_as_platform_app = true}))
+  ASSERT_TRUE(RunExtensionTest("api_test/file_system/save_new",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -442,8 +439,8 @@
       GetDriveMountPoint().AppendASCII("root/save_existing.txt");
   FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathForTest(
       &test_file);
-  ASSERT_TRUE(RunExtensionTest({.name = "api_test/file_system/save_existing",
-                                .launch_as_platform_app = true}))
+  ASSERT_TRUE(RunExtensionTest("api_test/file_system/save_existing",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -453,9 +450,8 @@
       GetDriveMountPoint().AppendASCII("root/save_new.txt");
   FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathForTest(
       &test_file);
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "api_test/file_system/save_new_with_write",
-                        .launch_as_platform_app = true}))
+  ASSERT_TRUE(RunExtensionTest("api_test/file_system/save_new_with_write",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -465,53 +461,52 @@
       GetDriveMountPoint().AppendASCII("root/save_existing.txt");
   FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathForTest(
       &test_file);
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "api_test/file_system/save_existing_with_write",
-                        .launch_as_platform_app = true}))
+  ASSERT_TRUE(RunExtensionTest("api_test/file_system/save_existing_with_write",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemApiTestForRequestFileSystem, Background) {
   EnterKioskSession();
   ScopedSkipRequestFileSystemDialog dialog_skipper(ui::DIALOG_BUTTON_OK);
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "api_test/file_system/request_file_system_background",
-       .launch_as_platform_app = true}))
+  ASSERT_TRUE(
+      RunExtensionTest("api_test/file_system/request_file_system_background",
+                       {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemApiTestForRequestFileSystem, ReadOnly) {
   EnterKioskSession();
   ScopedSkipRequestFileSystemDialog dialog_skipper(ui::DIALOG_BUTTON_OK);
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "api_test/file_system/request_file_system_read_only",
-       .launch_as_platform_app = true}))
+  ASSERT_TRUE(
+      RunExtensionTest("api_test/file_system/request_file_system_read_only",
+                       {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemApiTestForRequestFileSystem, Writable) {
   EnterKioskSession();
   ScopedSkipRequestFileSystemDialog dialog_skipper(ui::DIALOG_BUTTON_OK);
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "api_test/file_system/request_file_system_writable",
-       .launch_as_platform_app = true}))
+  ASSERT_TRUE(
+      RunExtensionTest("api_test/file_system/request_file_system_writable",
+                       {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemApiTestForRequestFileSystem, UserReject) {
   EnterKioskSession();
   ScopedSkipRequestFileSystemDialog dialog_skipper(ui::DIALOG_BUTTON_CANCEL);
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "api_test/file_system/request_file_system_user_reject",
-       .launch_as_platform_app = true}))
+  ASSERT_TRUE(
+      RunExtensionTest("api_test/file_system/request_file_system_user_reject",
+                       {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemApiTestForRequestFileSystem, NotKioskSession) {
   ScopedSkipRequestFileSystemDialog dialog_skipper(ui::DIALOG_BUTTON_OK);
   ASSERT_TRUE(RunExtensionTest(
-      {.name = "api_test/file_system/request_file_system_not_kiosk_session",
-       .launch_as_platform_app = true}))
+      "api_test/file_system/request_file_system_not_kiosk_session",
+      {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -519,9 +514,8 @@
                        AllowlistedComponent) {
   ScopedSkipRequestFileSystemDialog dialog_skipper(ui::DIALOG_BUTTON_CANCEL);
   ASSERT_TRUE(RunExtensionTest(
-      {.name = "api_test/file_system/request_file_system_whitelisted_component",
-       .launch_as_platform_app = true},
-      {.load_as_component = true}))
+      "api_test/file_system/request_file_system_whitelisted_component",
+      {.launch_as_platform_app = true}, {.load_as_component = true}))
       << message_;
 }
 
@@ -529,25 +523,23 @@
                        NotAllowlistedComponent) {
   ScopedSkipRequestFileSystemDialog dialog_skipper(ui::DIALOG_BUTTON_OK);
   ASSERT_TRUE(RunExtensionTest(
-      {.name =
-           "api_test/file_system/request_file_system_not_whitelisted_component",
-       .launch_as_platform_app = true},
-      {.load_as_component = true}))
+      "api_test/file_system/request_file_system_not_whitelisted_component",
+      {.launch_as_platform_app = true}, {.load_as_component = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemApiTestForRequestFileSystem, GetVolumeList) {
   EnterKioskSession();
-  ASSERT_TRUE(RunExtensionTest({.name = "api_test/file_system/get_volume_list",
-                                .launch_as_platform_app = true}))
+  ASSERT_TRUE(RunExtensionTest("api_test/file_system/get_volume_list",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemApiTestForRequestFileSystem,
                        GetVolumeList_NotKioskSession) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "api_test/file_system/get_volume_list_not_kiosk_session",
-       .launch_as_platform_app = true}))
+  ASSERT_TRUE(
+      RunExtensionTest("api_test/file_system/get_volume_list_not_kiosk_session",
+                       {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -561,9 +553,8 @@
       base::BindOnce(&FileSystemApiTestForRequestFileSystem::MountFakeVolume,
                      base::Unretained(this)));
 
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "api_test/file_system/on_volume_list_changed",
-                        .launch_as_platform_app = true}))
+  ASSERT_TRUE(RunExtensionTest("api_test/file_system/on_volume_list_changed",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -571,8 +562,8 @@
                        AllowlistedExtensionForDownloads) {
   ScopedSkipRequestFileSystemDialog dialog_skipper(ui::DIALOG_BUTTON_CANCEL);
   ASSERT_TRUE(RunExtensionTest(
-      {.name = "api_test/file_system/request_downloads_whitelisted_extension",
-       .launch_as_platform_app = true}))
+      "api_test/file_system/request_downloads_whitelisted_extension",
+      {.launch_as_platform_app = true}))
       << message_;
 }
 
diff --git a/chrome/browser/extensions/api/notifications/extension_notification_handler.cc b/chrome/browser/extensions/api/notifications/extension_notification_handler.cc
index 97c0834..dec81253 100644
--- a/chrome/browser/extensions/api/notifications/extension_notification_handler.cc
+++ b/chrome/browser/extensions/api/notifications/extension_notification_handler.cc
@@ -131,7 +131,7 @@
     return;
 
   std::unique_ptr<Event> event(
-      new Event(histogram_value, event_name, std::move(args)));
+      new Event(histogram_value, event_name, args->TakeList()));
   event->user_gesture = user_gesture;
   event_router->DispatchEventToExtension(extension_id, std::move(event));
 }
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_event_router.cc b/chrome/browser/extensions/api/passwords_private/passwords_private_event_router.cc
index efb17bf..a48be71 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_event_router.cc
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_event_router.cc
@@ -76,8 +76,8 @@
   params.status = status;
   params.folder_name = std::make_unique<std::string>(std::move(folder_name));
 
-  auto event_value = std::make_unique<base::ListValue>();
-  event_value->Append(params.ToValue());
+  std::vector<base::Value> event_value;
+  event_value.push_back(base::Value::FromUniquePtrValue(params.ToValue()));
 
   auto extension_event = std::make_unique<Event>(
       events::PASSWORDS_PRIVATE_ON_PASSWORDS_FILE_EXPORT_PROGRESS,
diff --git a/chrome/browser/extensions/api/resources_private/resources_private_api.cc b/chrome/browser/extensions/api/resources_private/resources_private_api.cc
index abaa862..f799b14 100644
--- a/chrome/browser/extensions/api/resources_private/resources_private_api.cc
+++ b/chrome/browser/extensions/api/resources_private/resources_private_api.cc
@@ -76,14 +76,14 @@
     case api::resources_private::COMPONENT_IDENTITY:
       AddStringsForIdentity(dict.get());
       break;
+    case api::resources_private::COMPONENT_PDF:
 #if BUILDFLAG(ENABLE_PDF)
-    case api::resources_private::COMPONENT_PDF: {
       pdf_extension_util::AddStrings(pdf_extension_util::PdfViewerContext::kAll,
                                      dict.get());
       pdf_extension_util::AddAdditionalData(
           IsPdfAnnotationsEnabled(browser_context()), dict.get());
-    } break;
 #endif  // BUILDFLAG(ENABLE_PDF)
+      break;
     case api::resources_private::COMPONENT_NONE:
       NOTREACHED();
   }
diff --git a/chrome/browser/extensions/api/safe_browsing_private/safe_browsing_private_event_router.cc b/chrome/browser/extensions/api/safe_browsing_private/safe_browsing_private_event_router.cc
index 25b59319..82cbcbd 100644
--- a/chrome/browser/extensions/api/safe_browsing_private/safe_browsing_private_event_router.cc
+++ b/chrome/browser/extensions/api/safe_browsing_private/safe_browsing_private_event_router.cc
@@ -4,16 +4,15 @@
 
 #include "chrome/browser/extensions/api/safe_browsing_private/safe_browsing_private_event_router.h"
 
-#include "base/callback_helpers.h"
-#include "build/build_config.h"
-
 #include <utility>
 #include <vector>
 
 #include "base/bind.h"
+#include "base/callback_helpers.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/stringprintf.h"
 #include "base/time/time.h"
+#include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/chrome_content_browser_client.h"
@@ -224,8 +223,8 @@
 
   // |event_router_| can be null in tests.
   if (event_router_) {
-    auto event_value = std::make_unique<base::ListValue>();
-    event_value->Append(params.ToValue());
+    std::vector<base::Value> event_value;
+    event_value.push_back(base::Value::FromUniquePtrValue(params.ToValue()));
 
     auto extension_event = std::make_unique<Event>(
         events::
@@ -256,8 +255,8 @@
     const std::string& user_name) {
   // |event_router_| can be null in tests.
   if (event_router_) {
-    auto event_value = std::make_unique<base::ListValue>();
-    event_value->Append(std::make_unique<base::Value>(user_name));
+    std::vector<base::Value> event_value;
+    event_value.push_back(base::Value(user_name));
     auto extension_event = std::make_unique<Event>(
         events::SAFE_BROWSING_PRIVATE_ON_POLICY_SPECIFIED_PASSWORD_CHANGED,
         api::safe_browsing_private::OnPolicySpecifiedPasswordChanged::
@@ -296,8 +295,8 @@
 
   // |event_router_| can be null in tests.
   if (event_router_) {
-    auto event_value = std::make_unique<base::ListValue>();
-    event_value->Append(params.ToValue());
+    std::vector<base::Value> event_value;
+    event_value.push_back(base::Value::FromUniquePtrValue(params.ToValue()));
 
     auto extension_event = std::make_unique<Event>(
         events::SAFE_BROWSING_PRIVATE_ON_DANGEROUS_DOWNLOAD_OPENED,
@@ -352,8 +351,8 @@
 
   // |event_router_| can be null in tests.
   if (event_router_) {
-    auto event_value = std::make_unique<base::ListValue>();
-    event_value->Append(params.ToValue());
+    std::vector<base::Value> event_value;
+    event_value.push_back(base::Value::FromUniquePtrValue(params.ToValue()));
 
     auto extension_event = std::make_unique<Event>(
         events::SAFE_BROWSING_PRIVATE_ON_SECURITY_INTERSTITIAL_SHOWN,
@@ -401,8 +400,8 @@
 
   // |event_router_| can be null in tests.
   if (event_router_) {
-    auto event_value = std::make_unique<base::ListValue>();
-    event_value->Append(params.ToValue());
+    std::vector<base::Value> event_value;
+    event_value.push_back(base::Value::FromUniquePtrValue(params.ToValue()));
 
     auto extension_event = std::make_unique<Event>(
         events::SAFE_BROWSING_PRIVATE_ON_SECURITY_INTERSTITIAL_PROCEEDED,
diff --git a/chrome/browser/extensions/api/sessions/sessions_api.cc b/chrome/browser/extensions/api/sessions/sessions_api.cc
index 435ab44..fd35f638 100644
--- a/chrome/browser/extensions/api/sessions/sessions_api.cc
+++ b/chrome/browser/extensions/api/sessions/sessions_api.cc
@@ -624,10 +624,9 @@
 
 void SessionsEventRouter::TabRestoreServiceChanged(
     sessions::TabRestoreService* service) {
-  std::unique_ptr<base::ListValue> args(new base::ListValue());
   EventRouter::Get(profile_)->BroadcastEvent(std::make_unique<Event>(
       events::SESSIONS_ON_CHANGED, api::sessions::OnChanged::kEventName,
-      std::move(args)));
+      std::vector<base::Value>()));
 }
 
 void SessionsEventRouter::TabRestoreServiceDestroyed(
diff --git a/chrome/browser/extensions/api/tabs/windows_event_router.cc b/chrome/browser/extensions/api/tabs/windows_event_router.cc
index bcba0980..e79970ab 100644
--- a/chrome/browser/extensions/api/tabs/windows_event_router.cc
+++ b/chrome/browser/extensions/api/tabs/windows_event_router.cc
@@ -304,7 +304,7 @@
 
   std::unique_ptr<Event> event = std::make_unique<Event>(
       events::WINDOWS_ON_FOCUS_CHANGED, windows::OnFocusChanged::kEventName,
-      std::make_unique<base::ListValue>());
+      std::vector<base::Value>());
   event->will_dispatch_callback =
       base::BindRepeating(&WillDispatchWindowFocusedEvent, window_controller);
   EventRouter::Get(profile_)->BroadcastEvent(std::move(event));
diff --git a/chrome/browser/extensions/api/terminal/terminal_private_api.cc b/chrome/browser/extensions/api/terminal/terminal_private_api.cc
index 2667237..743b9b9f 100644
--- a/chrome/browser/extensions/api/terminal/terminal_private_api.cc
+++ b/chrome/browser/extensions/api/terminal/terminal_private_api.cc
@@ -140,10 +140,10 @@
     return;
   }
 
-  std::unique_ptr<base::ListValue> args(new base::ListValue());
-  args->AppendString(terminal_id);
-  args->AppendString(output_type);
-  args->AppendString(output);
+  std::vector<base::Value> args;
+  args.push_back(base::Value(terminal_id));
+  args.push_back(base::Value(output_type));
+  args.push_back(base::Value(output));
 
   extensions::EventRouter* event_router =
       extensions::EventRouter::Get(browser_context);
@@ -159,9 +159,8 @@
                        const std::string& pref_name,
                        extensions::events::HistogramValue histogram,
                        const char* eventName) {
-  auto args = std::make_unique<base::ListValue>();
-  args->Append(base::Value::ToUniquePtrValue(
-      profile->GetPrefs()->Get(pref_name)->Clone()));
+  std::vector<base::Value> args;
+  args.push_back(profile->GetPrefs()->Get(pref_name)->Clone());
   extensions::EventRouter* event_router = extensions::EventRouter::Get(profile);
   if (event_router) {
     auto event = std::make_unique<extensions::Event>(histogram, eventName,
diff --git a/chrome/browser/extensions/api/webrtc_audio_private/webrtc_audio_private_api.cc b/chrome/browser/extensions/api/webrtc_audio_private/webrtc_audio_private_api.cc
index dafc0a9..e601a2b5 100644
--- a/chrome/browser/extensions/api/webrtc_audio_private/webrtc_audio_private_api.cc
+++ b/chrome/browser/extensions/api/webrtc_audio_private/webrtc_audio_private_api.cc
@@ -92,9 +92,9 @@
     const std::string& extension_id = extension->id();
     if (router->ExtensionHasEventListener(extension_id, kEventName) &&
         extension->permissions_data()->HasAPIPermission("webrtcAudioPrivate")) {
-      std::unique_ptr<Event> event = std::make_unique<Event>(
-          events::WEBRTC_AUDIO_PRIVATE_ON_SINKS_CHANGED, kEventName,
-          std::make_unique<base::ListValue>());
+      std::unique_ptr<Event> event =
+          std::make_unique<Event>(events::WEBRTC_AUDIO_PRIVATE_ON_SINKS_CHANGED,
+                                  kEventName, std::vector<base::Value>());
       router->DispatchEventToExtension(extension_id, std::move(event));
     }
   }
diff --git a/chrome/browser/extensions/blocklist_states_interaction_unittest.cc b/chrome/browser/extensions/blocklist_states_interaction_unittest.cc
index b9a32e6..eb05593c 100644
--- a/chrome/browser/extensions/blocklist_states_interaction_unittest.cc
+++ b/chrome/browser/extensions/blocklist_states_interaction_unittest.cc
@@ -8,6 +8,7 @@
 #include "chrome/browser/extensions/test_blocklist.h"
 #include "components/safe_browsing/buildflags.h"
 #include "extensions/browser/blocklist_state.h"
+#include "extensions/common/extension_features.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 // The interaction tests rely on the safe-browsing database.
@@ -27,10 +28,17 @@
 // is in the correct extension set under different circumstances.
 class BlocklistStatesInteractionUnitTest : public ExtensionServiceTestBase {
  public:
+  BlocklistStatesInteractionUnitTest() {
+    feature_list_.InitAndEnableFeature(
+        extensions_features::kDisablePolicyViolationExtensionsRemotely);
+  }
+
   void SetUp() override {
+    ExtensionServiceTestBase::SetUp();
     InitializeGoodInstalledExtensionService();
     test_blocklist_.Attach(service()->blocklist_);
     service()->Init();
+    extension_prefs_ = ExtensionPrefs::Get(profile());
   }
 
  protected:
@@ -52,8 +60,11 @@
     service()->PerformActionBasedOnOmahaAttributes(extension_id, attributes);
   }
 
+  ExtensionPrefs* extension_prefs() { return extension_prefs_; }
+
  private:
   TestBlocklist test_blocklist_;
+  ExtensionPrefs* extension_prefs_;
 };
 
 // 1. The extension is added to the Safe Browsing blocklist with
@@ -143,13 +154,12 @@
                                             BLOCKLISTED_POTENTIALLY_UNWANTED);
   EXPECT_FALSE(blocklisted_extensions.Contains(kTestExtensionId));
   EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
-  EXPECT_TRUE(ExtensionPrefs::Get(profile())->HasDisableReason(
+  EXPECT_TRUE(extension_prefs()->HasDisableReason(
       kTestExtensionId, disable_reason::DISABLE_GREYLIST));
 
   SetOmahaBlocklistStateForExtension(kTestExtensionId, "_malware", true);
   EXPECT_EQ(BLOCKLISTED_MALWARE,
-            ExtensionPrefs::Get(profile())->GetExtensionBlocklistState(
-                kTestExtensionId));
+            extension_prefs()->GetExtensionBlocklistState(kTestExtensionId));
   EXPECT_TRUE(blocklisted_extensions.Contains(kTestExtensionId));
   EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
 
@@ -164,14 +174,10 @@
   // greylist. It will be fixed after we decouple Safe Browsing blocklist state
   // and Omaha attribute blocklist state.
   EXPECT_EQ(NOT_BLOCKLISTED,
-            ExtensionPrefs::Get(profile())->GetExtensionBlocklistState(
-                kTestExtensionId));
+            extension_prefs()->GetExtensionBlocklistState(kTestExtensionId));
   EXPECT_FALSE(blocklisted_extensions.Contains(kTestExtensionId));
-  // TODO(crbug.com/1193695): The extension is still disabled with
-  // DISABLE_GREYLIST reason even though the blocklist state is cleared. This
-  // should be fixed when we start to consume Omaha attribute greylist.
-  EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
-  EXPECT_TRUE(ExtensionPrefs::Get(profile())->HasDisableReason(
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_FALSE(extension_prefs()->HasDisableReason(
       kTestExtensionId, disable_reason::DISABLE_GREYLIST));
 
   // The Safe Browsing greylist state should be set correctly after the Safe
@@ -179,22 +185,71 @@
   SetSafeBrowsingBlocklistStateForExtension(kTestExtensionId,
                                             BLOCKLISTED_POTENTIALLY_UNWANTED);
   EXPECT_EQ(BLOCKLISTED_POTENTIALLY_UNWANTED,
-            ExtensionPrefs::Get(profile())->GetExtensionBlocklistState(
-                kTestExtensionId));
+            extension_prefs()->GetExtensionBlocklistState(kTestExtensionId));
 
   EXPECT_FALSE(blocklisted_extensions.Contains(kTestExtensionId));
   // The extension should be kept disabled because it's still in the Safe
   // Browsing greylist.
   EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
-  EXPECT_TRUE(ExtensionPrefs::Get(profile())->HasDisableReason(
+  EXPECT_TRUE(extension_prefs()->HasDisableReason(
       kTestExtensionId, disable_reason::DISABLE_GREYLIST));
 
   SetSafeBrowsingBlocklistStateForExtension(kTestExtensionId, NOT_BLOCKLISTED);
   EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
-  EXPECT_FALSE(ExtensionPrefs::Get(profile())->HasDisableReason(
+  EXPECT_FALSE(extension_prefs()->HasDisableReason(
       kTestExtensionId, disable_reason::DISABLE_GREYLIST));
 }
 
+// 1. The extension is added to the Safe Browsing blocklist with
+// BLOCKLISTED_MALWARE state.
+// 2. The extension is added to the Omaha attribute greylist with
+// _policy_violation attribute.
+// 3. The extension is removed from the Safe Browsing blocklist.
+// 4. The extension is removed from the Omaha attribute greylist.
+TEST_F(BlocklistStatesInteractionUnitTest,
+       SafeBrowsingMalwareThenOmahaAttributePolicyViolation) {
+  const ExtensionSet& blocklisted_extensions =
+      registry()->blocklisted_extensions();
+  const ExtensionSet& enabled_extensions = registry()->enabled_extensions();
+  const ExtensionSet& disabled_extensions = registry()->disabled_extensions();
+  EXPECT_FALSE(blocklisted_extensions.Contains(kTestExtensionId));
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_FALSE(disabled_extensions.Contains(kTestExtensionId));
+
+  SetSafeBrowsingBlocklistStateForExtension(kTestExtensionId,
+                                            BLOCKLISTED_MALWARE);
+  EXPECT_TRUE(blocklisted_extensions.Contains(kTestExtensionId));
+  EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_EQ(BLOCKLISTED_MALWARE,
+            extension_prefs()->GetExtensionBlocklistState(kTestExtensionId));
+
+  SetOmahaBlocklistStateForExtension(kTestExtensionId, "_policy_violation",
+                                     true);
+  EXPECT_TRUE(blocklisted_extensions.Contains(kTestExtensionId));
+  EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_TRUE(blocklist_prefs::HasOmahaBlocklistState(
+      kTestExtensionId, BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION,
+      extension_prefs()));
+
+  SetSafeBrowsingBlocklistStateForExtension(kTestExtensionId, NOT_BLOCKLISTED);
+  EXPECT_FALSE(blocklisted_extensions.Contains(kTestExtensionId));
+  // The extension should be kept disabled because it's still in the Omaha
+  // attribute greylist.
+  EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_TRUE(disabled_extensions.Contains(kTestExtensionId));
+  EXPECT_TRUE(extension_prefs()->HasDisableReason(
+      kTestExtensionId, disable_reason::DISABLE_GREYLIST));
+  EXPECT_EQ(NOT_BLOCKLISTED,
+            extension_prefs()->GetExtensionBlocklistState(kTestExtensionId));
+  EXPECT_TRUE(blocklist_prefs::HasOmahaBlocklistState(
+      kTestExtensionId, BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION,
+      extension_prefs()));
+
+  SetOmahaBlocklistStateForExtension(kTestExtensionId, "_policy_violation",
+                                     false);
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+}
+
 // 1. The extension is added to the Safe Browsing greylist with
 // BLOCKLISTED_CWS_POLICY_VIOLATION state.
 // 2. The extension is added to the Omaha attribute greylist with
@@ -209,35 +264,118 @@
   SetSafeBrowsingBlocklistStateForExtension(kTestExtensionId,
                                             BLOCKLISTED_CWS_POLICY_VIOLATION);
   EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
-  EXPECT_TRUE(ExtensionPrefs::Get(profile())->HasDisableReason(
+  EXPECT_TRUE(extension_prefs()->HasDisableReason(
       kTestExtensionId, disable_reason::DISABLE_GREYLIST));
 
-  // TODO(crbug.com/1180996): Call SetOmahaBlocklistStateForExtension directly
-  // once we start to consume the _policy_violation attribute.
-  blocklist_prefs::AddOmahaBlocklistState(
-      kTestExtensionId, BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION,
-      ExtensionPrefs::Get(profile()));
+  SetOmahaBlocklistStateForExtension(kTestExtensionId, "_policy_violation",
+                                     true);
   EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
-  EXPECT_TRUE(ExtensionPrefs::Get(profile())->HasDisableReason(
+  EXPECT_TRUE(extension_prefs()->HasDisableReason(
       kTestExtensionId, disable_reason::DISABLE_GREYLIST));
 
   SetSafeBrowsingBlocklistStateForExtension(kTestExtensionId, NOT_BLOCKLISTED);
   // The extension should be kept disabled because it's still in the Omaha
   // attribute greylist.
   EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
-  EXPECT_TRUE(ExtensionPrefs::Get(profile())->HasDisableReason(
+  EXPECT_TRUE(extension_prefs()->HasDisableReason(
       kTestExtensionId, disable_reason::DISABLE_GREYLIST));
 
-  // TODO(crbug.com/1180996): Call SetOmahaBlocklistStateForExtension directly
-  // once we start to consume the _policy_violation attribute.
-  blocklist_prefs::RemoveOmahaBlocklistState(
-      kTestExtensionId, BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION,
-      ExtensionPrefs::Get(profile()));
-  service()->ClearGreylistedAcknowledgedStateAndMaybeReenable(kTestExtensionId);
+  SetOmahaBlocklistStateForExtension(kTestExtensionId, "_policy_violation",
+                                     false);
   EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
-  EXPECT_FALSE(ExtensionPrefs::Get(profile())->HasDisableReason(
+  EXPECT_FALSE(extension_prefs()->HasDisableReason(
       kTestExtensionId, disable_reason::DISABLE_GREYLIST));
 }
+
+// 1. The extension is added to the Omaha attribute greylist with
+// BLOCKLISTED_CWS_POLICY_VIOLATION state.
+// 2. The extension is added to the Safe Browsing greylist with
+// _policy_violation attribute.
+// 3. The extension is removed from the Omaha attribute greylist.
+// 4. The extension is removed from the Safe Browsing greylist.
+TEST_F(BlocklistStatesInteractionUnitTest,
+       OmahaAttributePolicyViolationThenSafeBrowsingPolicyViolation) {
+  const ExtensionSet& enabled_extensions = registry()->enabled_extensions();
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+
+  SetOmahaBlocklistStateForExtension(kTestExtensionId, "_policy_violation",
+                                     true);
+  EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_TRUE(extension_prefs()->HasDisableReason(
+      kTestExtensionId, disable_reason::DISABLE_GREYLIST));
+
+  SetSafeBrowsingBlocklistStateForExtension(kTestExtensionId,
+                                            BLOCKLISTED_CWS_POLICY_VIOLATION);
+  EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_TRUE(extension_prefs()->HasDisableReason(
+      kTestExtensionId, disable_reason::DISABLE_GREYLIST));
+
+  SetOmahaBlocklistStateForExtension(kTestExtensionId, "_policy_violation",
+                                     false);
+  // The extension should be kept disabled because it's still in the Safe
+  // Browsing greylist.
+  EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_TRUE(extension_prefs()->HasDisableReason(
+      kTestExtensionId, disable_reason::DISABLE_GREYLIST));
+
+  SetSafeBrowsingBlocklistStateForExtension(kTestExtensionId, NOT_BLOCKLISTED);
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_FALSE(extension_prefs()->HasDisableReason(
+      kTestExtensionId, disable_reason::DISABLE_GREYLIST));
+}
+
+// 1. The extension is added to the Safe Browsing greylist with
+// BLOCKLISTED_CWS_POLICY_VIOLATION state.
+// 2. User re-enabled the extension.
+// 3. The extension is added to the Omaha attribute greylist with
+// _policy_violation attribute.
+// 4. The extension is removed from the Safe Browsing greylist.
+// 5. The extension is removed from the Omaha attribute greylist.
+TEST_F(
+    BlocklistStatesInteractionUnitTest,
+    SafeBrowsingPolicyViolationThenOmahaAttributePolicyViolationWithUserAction) {
+  const ExtensionSet& enabled_extensions = registry()->enabled_extensions();
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+
+  SetSafeBrowsingBlocklistStateForExtension(kTestExtensionId,
+                                            BLOCKLISTED_CWS_POLICY_VIOLATION);
+  EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_TRUE(extension_prefs()->HasDisableReason(
+      kTestExtensionId, disable_reason::DISABLE_GREYLIST));
+  EXPECT_TRUE(blocklist_prefs::HasAcknowledgedBlocklistState(
+      kTestExtensionId, BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION,
+      extension_prefs()));
+
+  // The extension is manually re-enabled.
+  service()->EnableExtension(kTestExtensionId);
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_FALSE(extension_prefs()->HasDisableReason(
+      kTestExtensionId, disable_reason::DISABLE_GREYLIST));
+
+  SetOmahaBlocklistStateForExtension(kTestExtensionId, "_policy_violation",
+                                     true);
+  // The extension is not disabled again, because it was previously manually
+  // re-enabled.
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_FALSE(extension_prefs()->HasDisableReason(
+      kTestExtensionId, disable_reason::DISABLE_GREYLIST));
+
+  SetSafeBrowsingBlocklistStateForExtension(kTestExtensionId, NOT_BLOCKLISTED);
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+  // The acknowledged state should not be cleared yet, because it is still in
+  // the Omaha attribute greylist.
+  EXPECT_TRUE(blocklist_prefs::HasAcknowledgedBlocklistState(
+      kTestExtensionId, BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION,
+      extension_prefs()));
+
+  SetOmahaBlocklistStateForExtension(kTestExtensionId, "_policy_violation",
+                                     false);
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+  // The acknowledged state should be removed now.
+  EXPECT_FALSE(blocklist_prefs::HasAcknowledgedBlocklistState(
+      kTestExtensionId, BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION,
+      extension_prefs()));
+}
 #endif
 
 }  // namespace extensions
diff --git a/chrome/browser/extensions/chrome_component_extension_resource_manager.cc b/chrome/browser/extensions/chrome_component_extension_resource_manager.cc
index 7aa8e3c..e56ca28 100644
--- a/chrome/browser/extensions/chrome_component_extension_resource_manager.cc
+++ b/chrome/browser/extensions/chrome_component_extension_resource_manager.cc
@@ -24,10 +24,6 @@
 #include "ppapi/buildflags/buildflags.h"
 #include "ui/base/resource/resource_bundle.h"
 
-#if BUILDFLAG(ENABLE_PLUGINS)
-#include "chrome/grit/pdf_resources_map.h"
-#endif
-
 #if BUILDFLAG(IS_CHROMEOS_ASH)
 #include "ash/keyboard/ui/resources/keyboard_resource_util.h"
 #include "base/command_line.h"
@@ -46,6 +42,7 @@
 #if BUILDFLAG(ENABLE_PDF)
 #include <utility>
 #include "chrome/browser/pdf/pdf_extension_util.h"
+#include "chrome/grit/pdf_resources_map.h"
 #endif  // BUILDFLAG(ENABLE_PDF)
 
 namespace extensions {
@@ -109,10 +106,6 @@
   AddComponentResourceEntries(kExtraComponentExtensionResources,
                               base::size(kExtraComponentExtensionResources));
 
-#if BUILDFLAG(ENABLE_PLUGINS)
-  AddComponentResourceEntries(kPdfResources, kPdfResourcesSize);
-#endif
-
 #if BUILDFLAG(IS_CHROMEOS_ASH)
   // Add Files app JS modules resources.
   AddComponentResourceEntries(kFileManagerResources, kFileManagerResourcesSize);
@@ -136,6 +129,8 @@
 #endif
 
 #if BUILDFLAG(ENABLE_PDF)
+  AddComponentResourceEntries(kPdfResources, kPdfResourcesSize);
+
   // ResourceBundle is not always initialized in unit tests.
   if (ui::ResourceBundle::HasSharedInstance()) {
     base::Value dict(base::Value::Type::DICTIONARY);
diff --git a/chrome/browser/extensions/extension_service.cc b/chrome/browser/extensions/extension_service.cc
index 65c387c6..125d128 100644
--- a/chrome/browser/extensions/extension_service.cc
+++ b/chrome/browser/extensions/extension_service.cc
@@ -54,6 +54,7 @@
 #include "chrome/browser/extensions/forced_extensions/install_stage_tracker.h"
 #include "chrome/browser/extensions/install_verifier.h"
 #include "chrome/browser/extensions/installed_loader.h"
+#include "chrome/browser/extensions/omaha_attributes_handler.h"
 #include "chrome/browser/extensions/pending_extension_manager.h"
 #include "chrome/browser/extensions/permissions_updater.h"
 #include "chrome/browser/extensions/shared_module_service.h"
@@ -375,6 +376,7 @@
       safe_browsing_verdict_handler_(extension_prefs,
                                      ExtensionRegistry::Get(profile),
                                      this),
+      omaha_attributes_handler_(extension_prefs, this),
       registry_(ExtensionRegistry::Get(profile)),
       pending_extension_manager_(profile),
       install_directory_(install_directory),
@@ -842,17 +844,20 @@
     const base::Value& attributes) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
   HandleMalwareOmahaAttribute(extension_id, attributes);
-  omaha_attributes_handler_.PerformActionBasedOnOmahaAttributes(attributes);
+  omaha_attributes_handler_.PerformActionBasedOnOmahaAttributes(extension_id,
+                                                                attributes);
   allowlist_.PerformActionBasedOnOmahaAttributes(extension_id, attributes);
 }
 
 void ExtensionService::HandleMalwareOmahaAttribute(
     const std::string& extension_id,
     const base::Value& attributes) {
-  const base::Value* malware_value = attributes.FindKey("_malware");
+  bool has_malware_value =
+      OmahaAttributesHandler::HasOmahaBlocklistStateInAttributes(
+          attributes, BitMapBlocklistState::BLOCKLISTED_MALWARE);
   if (!base::FeatureList::IsEnabled(
           extensions_features::kDisableMalwareExtensionsRemotely) ||
-      malware_value == nullptr || !malware_value->GetBool()) {
+      !has_malware_value) {
     OmahaAttributesHandler::ReportNoUpdateCheckKeys();
     // Omaha attributes may have previously have the "_malware" key.
     MaybeEnableRemotelyDisabledExtension(extension_id);
diff --git a/chrome/browser/extensions/extension_service.h b/chrome/browser/extensions/extension_service.h
index b123bc4..376fb41 100644
--- a/chrome/browser/extensions/extension_service.h
+++ b/chrome/browser/extensions/extension_service.h
@@ -552,6 +552,7 @@
   bool CanBlockExtension(const Extension* extension) const;
 
   // Handles the malware Omaha attribute for remotely disabled extensions.
+  // TODO(crbug.com/1193695): Move this function to OmahaAttributesHandler.
   void HandleMalwareOmahaAttribute(const std::string& extension_id,
                                    const base::Value& attributes);
 
diff --git a/chrome/browser/extensions/extension_service_unittest.cc b/chrome/browser/extensions/extension_service_unittest.cc
index 105512a3..9e44a6e 100644
--- a/chrome/browser/extensions/extension_service_unittest.cc
+++ b/chrome/browser/extensions/extension_service_unittest.cc
@@ -95,6 +95,8 @@
 #include "components/prefs/scoped_user_pref_update.h"
 #include "components/safe_browsing/buildflags.h"
 #include "components/services/storage/public/mojom/indexed_db_control.mojom.h"
+#include "components/services/storage/public/mojom/local_storage_control.mojom.h"
+#include "components/services/storage/public/mojom/storage_usage_info.mojom.h"
 #include "components/sync/model/string_ordinal.h"
 #include "components/sync_preferences/pref_service_syncable.h"
 #include "components/sync_preferences/testing_pref_service_syncable.h"
@@ -159,6 +161,7 @@
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "testing/platform_test.h"
+#include "third_party/blink/public/mojom/dom_storage/storage_area.mojom.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "url/gurl.h"
 #include "url/origin.h"
@@ -5100,18 +5103,25 @@
       base::BindOnce(&CreateDatabase, base::Unretained(db_tracker), origin_id));
   task_environment()->RunUntilIdle();
 
-  // Create local storage. We only simulate this by creating the backing files.
-  // Note: This test depends on details of how the dom_storage library
-  // stores data in the host file system.
-  base::FilePath lso_dir_path =
-      profile()->GetPath().AppendASCII("Local Storage");
-  base::FilePath lso_file_path = lso_dir_path.AppendASCII(origin_id)
-      .AddExtension(FILE_PATH_LITERAL(".localstorage"));
-  EXPECT_TRUE(base::CreateDirectory(lso_dir_path));
-  EXPECT_EQ(0, base::WriteFile(lso_file_path, nullptr, 0));
-  EXPECT_TRUE(base::PathExists(lso_file_path));
+  // Create local storage.
+  auto* local_storage_control =
+      profile()->GetDefaultStoragePartition()->GetLocalStorageControl();
+  mojo::Remote<blink::mojom::StorageArea> area;
+  local_storage_control->BindStorageArea(url::Origin::Create(ext_url),
+                                         area.BindNewPipeAndPassReceiver());
+  {
+    bool success = false;
+    base::RunLoop run_loop;
+    area->Put({'k', 'e', 'y'}, {'v', 'a', 'l', 'u', 'e'}, absl::nullopt,
+              "source", base::BindLambdaForTesting([&](bool success_in) {
+                success = success_in;
+                run_loop.Quit();
+              }));
+    run_loop.Run();
+    ASSERT_TRUE(success);
+  }
 
-  // Create indexed db. Similarly, it is enough to only simulate this by
+  // Create indexed db. It is enough to only simulate this by
   // creating the directory on the disk, and resetting the caches of
   // "known" origins.
   auto& idb_control =
@@ -5161,8 +5171,18 @@
                      base::Unretained(db_tracker)));
   task_environment()->RunUntilIdle();
 
-  // Check that the LSO file has been removed.
-  EXPECT_FALSE(base::PathExists(lso_file_path));
+  // Check that the localStorage data been removed.
+  std::vector<storage::mojom::StorageUsageInfoPtr> usage_infos;
+  {
+    base::RunLoop run_loop;
+    local_storage_control->GetUsage(base::BindLambdaForTesting(
+        [&](std::vector<storage::mojom::StorageUsageInfoPtr> usage_infos_in) {
+          usage_infos.swap(usage_infos_in);
+          run_loop.Quit();
+        }));
+    run_loop.Run();
+  }
+  EXPECT_TRUE(usage_infos.empty());
 
   // Check if the indexed db has disappeared too.
   EXPECT_FALSE(base::DirectoryExists(idb_path));
@@ -5258,18 +5278,25 @@
       base::BindOnce(&CreateDatabase, base::Unretained(db_tracker), origin_id));
   task_environment()->RunUntilIdle();
 
-  // Create local storage. We only simulate this by creating the backing files.
-  // Note: This test depends on details of how the dom_storage library
-  // stores data in the host file system.
-  base::FilePath lso_dir_path =
-      profile()->GetPath().AppendASCII("Local Storage");
-  base::FilePath lso_file_path = lso_dir_path.AppendASCII(origin_id)
-      .AddExtension(FILE_PATH_LITERAL(".localstorage"));
-  EXPECT_TRUE(base::CreateDirectory(lso_dir_path));
-  EXPECT_EQ(0, base::WriteFile(lso_file_path, nullptr, 0));
-  EXPECT_TRUE(base::PathExists(lso_file_path));
+  // Create local storage.
+  auto* local_storage_control =
+      profile()->GetDefaultStoragePartition()->GetLocalStorageControl();
+  mojo::Remote<blink::mojom::StorageArea> area;
+  local_storage_control->BindStorageArea(url::Origin::Create(origin1),
+                                         area.BindNewPipeAndPassReceiver());
+  {
+    bool success = false;
+    base::RunLoop run_loop;
+    area->Put({'k', 'e', 'y'}, {'v', 'a', 'l', 'u', 'e'}, absl::nullopt,
+              "source", base::BindLambdaForTesting([&](bool success_in) {
+                success = success_in;
+                run_loop.Quit();
+              }));
+    run_loop.Run();
+    ASSERT_TRUE(success);
+  }
 
-  // Create indexed db. Similarly, it is enough to only simulate this by
+  // Create indexed db. It is enough to only simulate this by
   // creating the directory on the disk, and resetting the caches of
   // "known" origins.
   auto& idb_control =
@@ -5341,8 +5368,18 @@
                      base::Unretained(db_tracker)));
   task_environment()->RunUntilIdle();
 
-  // Check that the LSO file has been removed.
-  EXPECT_FALSE(base::PathExists(lso_file_path));
+  // Check that the localStorage data been removed.
+  std::vector<storage::mojom::StorageUsageInfoPtr> usage_infos;
+  {
+    base::RunLoop run_loop;
+    local_storage_control->GetUsage(base::BindLambdaForTesting(
+        [&](std::vector<storage::mojom::StorageUsageInfoPtr> usage_infos_in) {
+          usage_infos.swap(usage_infos_in);
+          run_loop.Quit();
+        }));
+    run_loop.Run();
+  }
+  EXPECT_TRUE(usage_infos.empty());
 
   // Check if the indexed db has disappeared too.
   EXPECT_FALSE(base::DirectoryExists(idb_path));
diff --git a/chrome/browser/extensions/extension_tabs_apitest.cc b/chrome/browser/extensions/extension_tabs_apitest.cc
index 6f763d64..86b9e3e4f 100644
--- a/chrome/browser/extensions/extension_tabs_apitest.cc
+++ b/chrome/browser/extensions/extension_tabs_apitest.cc
@@ -75,107 +75,92 @@
   browser()->profile()->GetPrefs()->SetBoolean(
       prefs::kHomePageIsNewTabPage, true);
 
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "crud.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "crud.html"}))
       << message_;
 }
 
 // TODO(crbug.com/1177118) Re-enable test
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, DISABLED_TabAudible) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "audible.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "audible.html"}))
       << message_;
 }
 
 // http://crbug.com/521410
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, DISABLED_TabMuted) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "muted.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "muted.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, Tabs2) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "crud2.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "crud2.html"}))
       << message_;
 }
 
 // crbug.com/149924
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, DISABLED_TabDuplicate) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "duplicate.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "duplicate.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabSize) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "tab_size.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "tab_size.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabUpdate) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "update.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "update.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabPinned) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "pinned.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "pinned.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabMove) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "move.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "move.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabEvents) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "events.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "events.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabRelativeURLs) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "tabs/basics", .page_url = "relative_urls.html"}))
+  ASSERT_TRUE(
+      RunExtensionTest("tabs/basics", {.page_url = "relative_urls.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabQuery) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "query.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "query.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabHighlight) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "highlight.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "highlight.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabCrashBrowser) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "crash.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "crash.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabOpener) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "opener.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "opener.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabRemove) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "remove.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "remove.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabRemoveMultiple) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "tabs/basics", .page_url = "remove-multiple.html"}))
+  ASSERT_TRUE(
+      RunExtensionTest("tabs/basics", {.page_url = "remove-multiple.html"}))
       << message_;
 }
 
@@ -208,22 +193,22 @@
 };
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiCaptureTest, CaptureVisibleTabJpeg) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "tabs/capture_visible_tab", .page_url = "test_jpeg.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/capture_visible_tab",
+                               {.page_url = "test_jpeg.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiCaptureTest, CaptureVisibleTabPng) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "tabs/capture_visible_tab", .page_url = "test_png.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/capture_visible_tab",
+                               {.page_url = "test_png.html"}))
       << message_;
 }
 
 // TODO(crbug.com/1177118) Re-enable test
 IN_PROC_BROWSER_TEST_F(ExtensionApiCaptureTest,
                        DISABLED_CaptureVisibleTabRace) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "tabs/capture_visible_tab", .page_url = "test_race.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/capture_visible_tab",
+                               {.page_url = "test_race.html"}))
       << message_;
 }
 
@@ -234,17 +219,17 @@
 #define MAYBE_CaptureVisibleFile CaptureVisibleFile
 #endif
 IN_PROC_BROWSER_TEST_F(ExtensionApiCaptureTest, MAYBE_CaptureVisibleFile) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "tabs/capture_visible_tab", .page_url = "test_file.html"},
-      {.allow_file_access = true}))
+  ASSERT_TRUE(RunExtensionTest("tabs/capture_visible_tab",
+                               {.page_url = "test_file.html"},
+                               {.allow_file_access = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiCaptureTest, CaptureVisibleDisabled) {
   browser()->profile()->GetPrefs()->SetBoolean(prefs::kDisableScreenshots,
                                                true);
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "tabs/capture_visible_tab", .page_url = "test_disabled.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/capture_visible_tab",
+                               {.page_url = "test_disabled.html"}))
       << message_;
 }
 
@@ -326,20 +311,19 @@
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, GetViewsOfCreatedPopup) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "tabs/basics", .page_url = "get_views_popup.html"}))
+  ASSERT_TRUE(
+      RunExtensionTest("tabs/basics", {.page_url = "get_views_popup.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, GetViewsOfCreatedWindow) {
-  ASSERT_TRUE(RunExtensionTest(
-      {.name = "tabs/basics", .page_url = "get_views_window.html"}))
+  ASSERT_TRUE(
+      RunExtensionTest("tabs/basics", {.page_url = "get_views_window.html"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, OnUpdatedDiscardedState) {
-  ASSERT_TRUE(
-      RunExtensionTest({.name = "tabs/basics", .page_url = "discarded.html"}))
+  ASSERT_TRUE(RunExtensionTest("tabs/basics", {.page_url = "discarded.html"}))
       << message_;
 }
 
@@ -360,10 +344,9 @@
       is_incognito_enabled ? "true" : "false",
       extensions::ExtensionTabUtil::GetWindowId(incognito_browser));
 
-  EXPECT_TRUE(RunExtensionTest({.name = "tabs/basics",
-                                .page_url = "incognito.html",
-                                .custom_arg = args.c_str()},
-                               {.allow_in_incognito = is_incognito_enabled}))
+  EXPECT_TRUE(RunExtensionTest(
+      "tabs/basics", {.page_url = "incognito.html", .custom_arg = args.c_str()},
+      {.allow_in_incognito = is_incognito_enabled}))
       << message_;
 }
 
diff --git a/chrome/browser/extensions/omaha_attributes_handler.cc b/chrome/browser/extensions/omaha_attributes_handler.cc
index 1c1191d..700ed79 100644
--- a/chrome/browser/extensions/omaha_attributes_handler.cc
+++ b/chrome/browser/extensions/omaha_attributes_handler.cc
@@ -6,7 +6,11 @@
 
 #include "base/metrics/histogram_functions.h"
 #include "base/values.h"
+#include "chrome/browser/extensions/blocklist_extension_prefs.h"
+#include "chrome/browser/extensions/extension_service.h"
 #include "content/public/browser/browser_thread.h"
+#include "extensions/browser/blocklist_state.h"
+#include "extensions/common/extension_features.h"
 
 namespace extensions {
 
@@ -35,27 +39,85 @@
   base::UmaHistogramCounts100("Extensions.ExtensionReenabledRemotely", 1);
 }
 
+// static
+bool OmahaAttributesHandler::HasOmahaBlocklistStateInAttributes(
+    const base::Value& attributes,
+    BitMapBlocklistState state) {
+  const base::Value* state_value = nullptr;
+  switch (state) {
+    case BitMapBlocklistState::BLOCKLISTED_MALWARE:
+      state_value = attributes.FindKey("_malware");
+      break;
+    case BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION:
+      state_value = attributes.FindKey("_policy_violation");
+      break;
+    case BitMapBlocklistState::BLOCKLISTED_POTENTIALLY_UNWANTED:
+      state_value = attributes.FindKey("_potentially_uws");
+      break;
+    case BitMapBlocklistState::NOT_BLOCKLISTED:
+    case BitMapBlocklistState::BLOCKLISTED_SECURITY_VULNERABILITY:
+      NOTREACHED()
+          << "The other states are not applicable in Omaha attributes.";
+      state_value = nullptr;
+      break;
+  }
+  return state_value && state_value->GetBool();
+}
+
+OmahaAttributesHandler::OmahaAttributesHandler(
+    ExtensionPrefs* extension_prefs,
+    ExtensionService* extension_service)
+    : extension_prefs_(extension_prefs),
+      extension_service_(extension_service) {}
+
 void OmahaAttributesHandler::PerformActionBasedOnOmahaAttributes(
+    const ExtensionId& extension_id,
     const base::Value& attributes) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
   ReportPolicyViolationUWSOmahaAttributes(attributes);
-  // TODO(crbug.com/1180996): Perform action based on the attributes.
+  HandleGreylistOmahaAttribute(
+      extension_id, attributes,
+      extensions_features::kDisablePolicyViolationExtensionsRemotely,
+      BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION);
 }
 
 void OmahaAttributesHandler::ReportPolicyViolationUWSOmahaAttributes(
     const base::Value& attributes) {
-  const base::Value* uws_value = attributes.FindKey("_potentially_uws");
-  if (uws_value != nullptr && uws_value->GetBool()) {
+  bool has_uws_value = HasOmahaBlocklistStateInAttributes(
+      attributes, BitMapBlocklistState::BLOCKLISTED_POTENTIALLY_UNWANTED);
+  if (has_uws_value) {
     ReportExtensionDisabledRemotely(
         /*should_be_remotely_disabled=*/false,
         ExtensionUpdateCheckDataKey::kPotentiallyUWS);
   }
-  const base::Value* pv_value = attributes.FindKey("_policy_violation");
-  if (pv_value != nullptr && pv_value->GetBool()) {
+  bool has_pv_value = HasOmahaBlocklistStateInAttributes(
+      attributes, BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION);
+  if (has_pv_value) {
     ReportExtensionDisabledRemotely(
         /*should_be_remotely_disabled=*/false,
         ExtensionUpdateCheckDataKey::kPolicyViolation);
   }
 }
 
+void OmahaAttributesHandler::HandleGreylistOmahaAttribute(
+    const ExtensionId& extension_id,
+    const base::Value& attributes,
+    const base::Feature& feature_flag,
+    BitMapBlocklistState greylist_state) {
+  bool has_attribute_value =
+      HasOmahaBlocklistStateInAttributes(attributes, greylist_state);
+  if (!base::FeatureList::IsEnabled(feature_flag) || !has_attribute_value) {
+    blocklist_prefs::RemoveOmahaBlocklistState(extension_id, greylist_state,
+                                               extension_prefs_);
+    extension_service_->ClearGreylistedAcknowledgedStateAndMaybeReenable(
+        extension_id);
+    return;
+  }
+
+  blocklist_prefs::AddOmahaBlocklistState(extension_id, greylist_state,
+                                          extension_prefs_);
+  extension_service_->MaybeDisableGreylistedExtension(extension_id,
+                                                      greylist_state);
+}
+
 }  // namespace extensions
diff --git a/chrome/browser/extensions/omaha_attributes_handler.h b/chrome/browser/extensions/omaha_attributes_handler.h
index 3e311fb..01c06c0 100644
--- a/chrome/browser/extensions/omaha_attributes_handler.h
+++ b/chrome/browser/extensions/omaha_attributes_handler.h
@@ -5,11 +5,17 @@
 #ifndef CHROME_BROWSER_EXTENSIONS_OMAHA_ATTRIBUTES_HANDLER_H_
 #define CHROME_BROWSER_EXTENSIONS_OMAHA_ATTRIBUTES_HANDLER_H_
 
+#include "chrome/browser/extensions/blocklist.h"
+#include "extensions/browser/blocklist_state.h"
+#include "extensions/common/extension_id.h"
+
 namespace base {
 class Value;
 }
 
 namespace extensions {
+class ExtensionPrefs;
+class ExtensionService;
 
 // These values are logged to UMA. Entries should not be renumbered and
 // numeric values should never be reused. Please keep in sync with
@@ -32,7 +38,8 @@
 // Manages the Omaha attributes blocklist/greylist states in extension pref.
 class OmahaAttributesHandler {
  public:
-  OmahaAttributesHandler() = default;
+  OmahaAttributesHandler(ExtensionPrefs* extension_prefs,
+                         ExtensionService* extension_service);
   OmahaAttributesHandler(const OmahaAttributesHandler&) = delete;
   OmahaAttributesHandler& operator=(const OmahaAttributesHandler&) = delete;
   ~OmahaAttributesHandler() = default;
@@ -48,13 +55,30 @@
   // Logs UMA metrics when a remotely disabled extension is re-enabled.
   static void ReportReenableExtensionFromMalware();
 
+  // Checks whether the `state` is in the `attributes`.
+  static bool HasOmahaBlocklistStateInAttributes(const base::Value& attributes,
+                                                 BitMapBlocklistState state);
+
   // Performs action based on Omaha attributes for the extension.
   // TODO(crbug.com/1193695): This function currently only handles greylist
   // states. We should move blocklist handling into this class too.
-  void PerformActionBasedOnOmahaAttributes(const base::Value& attributes);
+  void PerformActionBasedOnOmahaAttributes(const ExtensionId& extension_id,
+                                           const base::Value& attributes);
 
  private:
   void ReportPolicyViolationUWSOmahaAttributes(const base::Value& attributes);
+
+  // Performs action based on `attributes` for the `extension_id`. If the
+  // extension is not in the `greylist_state` or the `feature_flag` is disabled,
+  // remove it from the Omaha blocklist state and maybe re-enable it. Otherwise,
+  // add it to the Omaha blocklist state and maybe disable it.
+  void HandleGreylistOmahaAttribute(const ExtensionId& extension_id,
+                                    const base::Value& attributes,
+                                    const base::Feature& feature_flag,
+                                    BitMapBlocklistState greylist_state);
+
+  ExtensionPrefs* extension_prefs_ = nullptr;
+  ExtensionService* extension_service_ = nullptr;
 };
 
 }  // namespace extensions
diff --git a/chrome/browser/extensions/omaha_attributes_handler_unittest.cc b/chrome/browser/extensions/omaha_attributes_handler_unittest.cc
index b66d747e..3430657 100644
--- a/chrome/browser/extensions/omaha_attributes_handler_unittest.cc
+++ b/chrome/browser/extensions/omaha_attributes_handler_unittest.cc
@@ -5,9 +5,13 @@
 #include "chrome/browser/extensions/omaha_attributes_handler.h"
 
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/values.h"
+#include "chrome/browser/extensions/blocklist_extension_prefs.h"
 #include "chrome/browser/extensions/extension_service.h"
 #include "chrome/browser/extensions/extension_service_test_base.h"
+#include "extensions/browser/extension_prefs.h"
+#include "extensions/common/extension_features.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace extensions {
@@ -20,13 +24,19 @@
 }  // namespace
 
 // Test suite to test Omaha attribute handler.
-using OmahaAttributesHandlerUnitTest = ExtensionServiceTestBase;
+class OmahaAttributesHandlerUnitTest : public ExtensionServiceTestBase {
+ public:
+  OmahaAttributesHandlerUnitTest() {
+    feature_list_.InitAndEnableFeature(
+        extensions_features::kDisablePolicyViolationExtensionsRemotely);
+  }
+};
 
 TEST_F(OmahaAttributesHandlerUnitTest, LogPolicyViolationUWSMetrics) {
   base::HistogramTester histograms;
   base::Value attributes(base::Value::Type::DICTIONARY);
-  attributes.SetKey("_potentially_uws", base::Value(true));
-  attributes.SetKey("_policy_violation", base::Value(true));
+  attributes.SetBoolKey("_policy_violation", true);
+  attributes.SetBoolKey("_potentially_uws", true);
   InitializeEmptyExtensionService();
 
   service()->PerformActionBasedOnOmahaAttributes(kTestExtensionId, attributes);
@@ -41,4 +51,110 @@
       /* expected_count */ 1);
 }
 
+TEST_F(OmahaAttributesHandlerUnitTest, DisableRemotelyForPolicyViolation) {
+  InitializeGoodInstalledExtensionService();
+  service()->Init();
+
+  const ExtensionSet& enabled_extensions = registry()->enabled_extensions();
+  const ExtensionSet& disabled_extensions = registry()->disabled_extensions();
+
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+
+  base::Value attributes(base::Value::Type::DICTIONARY);
+  attributes.SetBoolKey("_policy_violation", true);
+  service()->PerformActionBasedOnOmahaAttributes(kTestExtensionId, attributes);
+
+  EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_TRUE(disabled_extensions.Contains(kTestExtensionId));
+
+  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(blocklist_prefs::HasOmahaBlocklistState(
+      kTestExtensionId, BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION,
+      prefs));
+  EXPECT_EQ(disable_reason::DISABLE_GREYLIST,
+            prefs->GetDisableReasons(kTestExtensionId));
+
+  // Remove extensions from greylist.
+  attributes.SetBoolKey("_policy_violation", false);
+  service()->PerformActionBasedOnOmahaAttributes(kTestExtensionId, attributes);
+  EXPECT_FALSE(blocklist_prefs::HasOmahaBlocklistState(
+      kTestExtensionId, BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION,
+      ExtensionPrefs::Get(profile())));
+  EXPECT_EQ(disable_reason::DISABLE_NONE,
+            prefs->GetDisableReasons(kTestExtensionId));
+
+  EXPECT_FALSE(blocklist_prefs::HasOmahaBlocklistState(
+      kTestExtensionId, BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION,
+      prefs));
+
+  // The extension is re-enabled.
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_FALSE(disabled_extensions.Contains(kTestExtensionId));
+}
+
+TEST_F(OmahaAttributesHandlerUnitTest, KeepDisabledWhenMalwareRemoved) {
+  InitializeGoodInstalledExtensionService();
+  service()->Init();
+
+  const ExtensionSet& enabled_extensions = registry()->enabled_extensions();
+  const ExtensionSet& blocklisted_extensions =
+      registry()->blocklisted_extensions();
+
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_FALSE(blocklisted_extensions.Contains(kTestExtensionId));
+
+  base::Value attributes(base::Value::Type::DICTIONARY);
+  attributes.SetBoolKey("_malware", true);
+  attributes.SetBoolKey("_policy_violation", true);
+  service()->PerformActionBasedOnOmahaAttributes(kTestExtensionId, attributes);
+
+  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
+  EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_TRUE(blocklisted_extensions.Contains(kTestExtensionId));
+  EXPECT_EQ(disable_reason::DISABLE_REMOTELY_FOR_MALWARE |
+                disable_reason::DISABLE_GREYLIST,
+            prefs->GetDisableReasons(kTestExtensionId));
+
+  // Remove malware.
+  attributes.SetBoolKey("_malware", false);
+  service()->PerformActionBasedOnOmahaAttributes(kTestExtensionId, attributes);
+
+  // The extension is not enabled because the policy violation bit is not
+  // cleared.
+  EXPECT_FALSE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_FALSE(blocklisted_extensions.Contains(kTestExtensionId));
+  EXPECT_EQ(disable_reason::DISABLE_GREYLIST,
+            prefs->GetDisableReasons(kTestExtensionId));
+}
+
+// Test suite to test Omaha attribute handler when features are disabled.
+class OmahaAttributesHandlerWithFeatureDisabledUnitTest
+    : public ExtensionServiceTestBase {
+ public:
+  OmahaAttributesHandlerWithFeatureDisabledUnitTest() {
+    feature_list_.InitAndDisableFeature(
+        extensions_features::kDisablePolicyViolationExtensionsRemotely);
+  }
+};
+
+TEST_F(OmahaAttributesHandlerWithFeatureDisabledUnitTest,
+       DoNotDisableRemotelyWhenPolicyViolationFlagDisabled) {
+  InitializeGoodInstalledExtensionService();
+  service()->Init();
+
+  const ExtensionSet& enabled_extensions = registry()->enabled_extensions();
+  const ExtensionSet& disabled_extensions = registry()->disabled_extensions();
+
+  base::Value attributes(base::Value::Type::DICTIONARY);
+  attributes.SetBoolKey("_policy_violation", true);
+  service()->PerformActionBasedOnOmahaAttributes(kTestExtensionId, attributes);
+
+  // Since the flag is disabled, we don't expect the extension to be affected.
+  EXPECT_TRUE(enabled_extensions.Contains(kTestExtensionId));
+  EXPECT_FALSE(disabled_extensions.Contains(kTestExtensionId));
+  EXPECT_FALSE(blocklist_prefs::HasOmahaBlocklistState(
+      kTestExtensionId, BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION,
+      ExtensionPrefs::Get(profile())));
+}
+
 }  // namespace extensions
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index c74f3e07..c72f054 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -1723,12 +1723,12 @@
   {
     "name": "enable-experimental-accessibility-language-detection",
     "owners": [ "chrishall", "//ui/accessibility/OWNERS" ],
-    "expiry_milestone": 91
+    "expiry_milestone": 98
   },
   {
     "name": "enable-experimental-accessibility-language-detection-dynamic",
     "owners": [ "chrishall", "//ui/accessibility/OWNERS" ],
-    "expiry_milestone": 91
+    "expiry_milestone": 98
   },
   {
     "name": "enable-experimental-accessibility-switch-access-setup-guide",
@@ -2996,7 +2996,7 @@
   {
     "name": "forced-colors",
     "owners": [ "almaher@microsoft.com" ],
-    "expiry_milestone": 92
+    "expiry_milestone": 95
   },
   {
     "name": "form-controls-dark-mode",
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 60c5a96..359f0dc 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -3185,10 +3185,6 @@
     "Enable a history sub page to the page info menu, and a button to forget "
     "a site, removing all preferences and history.";
 
-const char kPageInfoV2Name[] = "Page info version two";
-const char kPageInfoV2Description[] =
-    "Enable the second version of the page info menu.";
-
 extern const char kPageInfoV2DesktopName[];
 extern const char kPageInfoV2DesktopDescription[];
 
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 9f38272..e203545 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1844,9 +1844,6 @@
 extern const char kPageInfoHistoryName[];
 extern const char kPageInfoHistoryDescription[];
 
-extern const char kPageInfoV2Name[];
-extern const char kPageInfoV2Description[];
-
 extern const char kPhotoPickerVideoSupportName[];
 extern const char kPhotoPickerVideoSupportDescription[];
 
diff --git a/chrome/browser/lookalikes/lookalike_url_navigation_throttle.cc b/chrome/browser/lookalikes/lookalike_url_navigation_throttle.cc
index a4fb98e..c7a4fc5 100644
--- a/chrome/browser/lookalikes/lookalike_url_navigation_throttle.cc
+++ b/chrome/browser/lookalikes/lookalike_url_navigation_throttle.cc
@@ -421,7 +421,7 @@
           &reputation::IsTargetHostAllowlistedBySafetyTipsComponent, proto);
   std::string matched_domain;
   if (GetMatchingDomain(navigated_domain, engaged_sites, in_target_allowlist,
-                        &matched_domain, match_type)) {
+                        proto, &matched_domain, match_type)) {
     DCHECK(!matched_domain.empty());
 
     // matched_domain can be a top domain or an engaged domain. Simply use its
diff --git a/chrome/browser/lookalikes/lookalike_url_navigation_throttle_browsertest.cc b/chrome/browser/lookalikes/lookalike_url_navigation_throttle_browsertest.cc
index 0edfd1bc..45787c3 100644
--- a/chrome/browser/lookalikes/lookalike_url_navigation_throttle_browsertest.cc
+++ b/chrome/browser/lookalikes/lookalike_url_navigation_throttle_browsertest.cc
@@ -578,7 +578,7 @@
                        TargetEmbedding_EmbedderAllowlist) {
   const GURL kNavigatedUrl = GetURL("google.com.allowlisted.com");
   SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
-  reputation::SetSafetyTipAllowlistPatterns({"allowlisted.com/"}, {});
+  reputation::SetSafetyTipAllowlistPatterns({"allowlisted.com/"}, {}, {});
   TestInterstitialNotShown(browser(), kNavigatedUrl);
   CheckNoUkm();
 }
@@ -588,7 +588,17 @@
                        TargetEmbedding_TargetAllowlist) {
   const GURL kNavigatedUrl = GetURL("foo.scholar.google.com.com");
   SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
-  reputation::SetSafetyTipAllowlistPatterns({}, {"scholar\\.google\\.com"});
+  reputation::SetSafetyTipAllowlistPatterns({}, {"scholar\\.google\\.com"}, {});
+  TestInterstitialNotShown(browser(), kNavigatedUrl);
+  CheckNoUkm();
+}
+
+// Target embedding shouldn't trigger on component-delivered common words.
+IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
+                       TargetEmbedding_ComponentCommonWords) {
+  const GURL kNavigatedUrl = GetURL("google.com.example.com");
+  SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
+  reputation::SetSafetyTipAllowlistPatterns({}, {}, {"google"});
   TestInterstitialNotShown(browser(), kNavigatedUrl);
   CheckNoUkm();
 }
@@ -599,7 +609,7 @@
                        TargetEmbedding_TargetAllowlistWithNoSeparators) {
   const GURL kNavigatedUrl = GetURL("googlecom.example.com");
   SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
-  reputation::SetSafetyTipAllowlistPatterns({}, {"google\\.com"});
+  reputation::SetSafetyTipAllowlistPatterns({}, {"google\\.com"}, {});
   TestInterstitialNotShown(browser(), kNavigatedUrl);
   CheckNoUkm();
 }
@@ -781,7 +791,7 @@
 IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
                        EditDistance_TopDomain_Target_Allowlist) {
   base::HistogramTester histograms;
-  reputation::SetSafetyTipAllowlistPatterns({}, {"google\\.com"});
+  reputation::SetSafetyTipAllowlistPatterns({}, {"google\\.com"}, {});
 
   // The skeleton of this domain, gooogle.corn, is one 1 edit away from
   // google.corn, the skeleton of google.com.
@@ -802,7 +812,7 @@
                        EditDistance_EngagedDomain_Target_Allowlist) {
   base::HistogramTester histograms;
   SetEngagementScore(browser(), GURL("https://test-site.com"), kHighEngagement);
-  reputation::SetSafetyTipAllowlistPatterns({}, {"test-site\\.com"});
+  reputation::SetSafetyTipAllowlistPatterns({}, {"test-site\\.com"}, {});
 
   // The skeleton of this domain is one 1 edit away from the skeleton of
   // test-site.com.
@@ -989,7 +999,7 @@
   reputation::SetSafetyTipAllowlistPatterns(
       {"xn--googl-fsa.com/",  // googlé.com in punycode
        "site.test/", "another-site.test/"},
-      {});
+      {}, {});
   TestInterstitialNotShown(browser(), GetURL("googlé.com"));
   CheckNoUkm();
 
diff --git a/chrome/browser/media/cast_mirroring_service_host.cc b/chrome/browser/media/cast_mirroring_service_host.cc
index 4f58382..37aef84 100644
--- a/chrome/browser/media/cast_mirroring_service_host.cc
+++ b/chrome/browser/media/cast_mirroring_service_host.cc
@@ -20,6 +20,7 @@
 #include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
 #include "chrome/browser/media/webrtc/media_stream_capture_indicator.h"
 #include "chrome/browser/net/system_network_context_manager.h"
+#include "chrome/browser/service_sandbox_type.h"
 #include "components/mirroring/browser/single_client_video_capture_host.h"
 #include "components/mirroring/mojom/cast_message_channel.mojom.h"
 #include "components/mirroring/mojom/session_observer.mojom.h"
diff --git a/chrome/browser/media/webrtc/desktop_capture_devices_util.cc b/chrome/browser/media/webrtc/desktop_capture_devices_util.cc
index 10b104ad..f30bc7ff 100644
--- a/chrome/browser/media/webrtc/desktop_capture_devices_util.cc
+++ b/chrome/browser/media/webrtc/desktop_capture_devices_util.cc
@@ -19,6 +19,7 @@
 #include "media/audio/audio_device_description.h"
 #include "media/mojo/mojom/capture_handle.mojom.h"
 #include "media/mojo/mojom/display_media_information.mojom.h"
+#include "third_party/blink/public/mojom/media/capture_handle_config.mojom.h"
 #include "ui/base/l10n/l10n_util.h"
 
 namespace {
diff --git a/chrome/browser/nearby_sharing/nearby_sharing_service_impl.cc b/chrome/browser/nearby_sharing/nearby_sharing_service_impl.cc
index d4075ccf..020e43c 100644
--- a/chrome/browser/nearby_sharing/nearby_sharing_service_impl.cc
+++ b/chrome/browser/nearby_sharing/nearby_sharing_service_impl.cc
@@ -13,6 +13,7 @@
 #include "base/files/file.h"
 #include "base/hash/hash.h"
 #include "base/logging.h"
+#include "base/metrics/histogram_functions.h"
 #include "base/numerics/checked_math.h"
 #include "base/rand_util.h"
 #include "base/strings/string_number_conversions.h"
@@ -262,7 +263,8 @@
     std::unique_ptr<NearbyConnectionsManager> nearby_connections_manager,
     chromeos::nearby::NearbyProcessManager* process_manager,
     std::unique_ptr<PowerClient> power_client)
-    : profile_(profile),
+    : prefs_(prefs),
+      profile_(profile),
       nearby_connections_manager_(std::move(nearby_connections_manager)),
       process_manager_(process_manager),
       power_client_(std::move(power_client)),
@@ -300,7 +302,7 @@
   DCHECK(nearby_connections_manager_);
   DCHECK(power_client_);
 
-  RecordNearbyShareEnabledMetric(prefs);
+  RecordNearbyShareEnabledMetric(prefs_);
 
   auto* session_controller = ash::SessionController::Get();
   if (session_controller) {
@@ -1140,6 +1142,8 @@
 
 void NearbySharingServiceImpl::OnEnabledChanged(bool enabled) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  RecordNearbyShareEnabledMetric(prefs_);
+  base::UmaHistogramBoolean("Nearby.Share.EnabledStateChanged", enabled);
   if (enabled) {
     NS_LOG(VERBOSE) << __func__ << ": Nearby sharing enabled!";
     local_device_data_manager_->Start();
diff --git a/chrome/browser/nearby_sharing/nearby_sharing_service_impl.h b/chrome/browser/nearby_sharing/nearby_sharing_service_impl.h
index 3b4a19c..7d22ba38 100644
--- a/chrome/browser/nearby_sharing/nearby_sharing_service_impl.h
+++ b/chrome/browser/nearby_sharing/nearby_sharing_service_impl.h
@@ -392,6 +392,7 @@
   void AbortAndCloseConnectionIfNecessary(const TransferMetadata::Status status,
                                           const ShareTarget& share_target);
 
+  PrefService* prefs_ = nullptr;
   Profile* profile_;
   std::unique_ptr<NearbyConnectionsManager> nearby_connections_manager_;
   chromeos::nearby::NearbyProcessManager* process_manager_;
diff --git a/chrome/browser/notifications/notifier_state_tracker.cc b/chrome/browser/notifications/notifier_state_tracker.cc
index 8916238..5529e0d1e 100644
--- a/chrome/browser/notifications/notifier_state_tracker.cc
+++ b/chrome/browser/notifications/notifier_state_tracker.cc
@@ -180,8 +180,9 @@
   extensions::api::notifications::PermissionLevel permission =
       enabled ? extensions::api::notifications::PERMISSION_LEVEL_GRANTED
               : extensions::api::notifications::PERMISSION_LEVEL_DENIED;
-  std::unique_ptr<base::ListValue> args(new base::ListValue());
-  args->AppendString(extensions::api::notifications::ToString(permission));
+  std::vector<base::Value> args;
+  args.push_back(
+      base::Value(extensions::api::notifications::ToString(permission)));
   std::unique_ptr<extensions::Event> event(new extensions::Event(
       extensions::events::NOTIFICATIONS_ON_PERMISSION_LEVEL_CHANGED,
       extensions::api::notifications::OnPermissionLevelChanged::kEventName,
diff --git a/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer.cc b/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer.cc
index 712821e5..a3334cf5 100644
--- a/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer.cc
+++ b/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer.cc
@@ -94,10 +94,6 @@
     return STOP_OBSERVING;
   after_srp_metrics_ = tab_helper->after_srp_metrics();
 
-  data_saver_enabled_at_commit_ = data_reduction_proxy::
-      DataReductionProxySettings::IsDataSaverEnabledByUser(
-          profile->IsOffTheRecord(), profile->GetPrefs());
-
   history::HistoryService* history_service =
       HistoryServiceFactory::GetForProfileIfExists(
           profile, ServiceAccessType::IMPLICIT_ACCESS);
@@ -229,10 +225,6 @@
       "PageLoad.Clients.SubresourceLoading.LoadedCSSJSBeforeFCP.Noncached",
       loaded_css_js_from_network_before_fcp_);
 
-  // Only record UKM for Data Saver users.
-  if (!data_saver_enabled_at_commit_)
-    return;
-
   RecordPrefetchProxyEvent();
   RecordAfterSRPEvent();
 }
diff --git a/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer.h b/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer.h
index 19b2f54..7823511 100644
--- a/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer.h
+++ b/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer.h
@@ -73,9 +73,6 @@
           resources) override;
   void OnEventOccurred(page_load_metrics::PageLoadMetricsEvent event) override;
 
-  // Whether data saver was enabled for this page load when it committed.
-  bool data_saver_enabled_at_commit_ = false;
-
   // The time that the navigation started. Used to timebox the history service
   // query on commit.
   base::Time navigation_start_;
diff --git a/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer_browsertest.cc b/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer_browsertest.cc
index 819aa68..7f63e52 100644
--- a/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer_browsertest.cc
+++ b/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer_browsertest.cc
@@ -26,16 +26,11 @@
 #include "services/metrics/public/cpp/ukm_source.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
-class IsolatedPrerenderPageLoadMetricsObserverBrowserTest
+class PrefetchProxyPageLoadMetricsObserverBrowserTest
     : public InProcessBrowserTest {
  public:
-  IsolatedPrerenderPageLoadMetricsObserverBrowserTest() = default;
-  ~IsolatedPrerenderPageLoadMetricsObserverBrowserTest() override = default;
-
-  void EnableDataSaver() {
-    base::CommandLine::ForCurrentProcess()->AppendSwitch(
-        data_reduction_proxy::switches::kEnableDataReductionProxy);
-  }
+  PrefetchProxyPageLoadMetricsObserverBrowserTest() = default;
+  ~PrefetchProxyPageLoadMetricsObserverBrowserTest() override = default;
 
   void SetUpOnMainThread() override {
     InProcessBrowserTest::SetUpOnMainThread();
@@ -100,7 +95,7 @@
   std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
 };
 
-IN_PROC_BROWSER_TEST_F(IsolatedPrerenderPageLoadMetricsObserverBrowserTest,
+IN_PROC_BROWSER_TEST_F(PrefetchProxyPageLoadMetricsObserverBrowserTest,
                        BeforeFCPPlumbing) {
   base::HistogramTester histogram_tester;
   NavigateToOriginPath("/index.html");
@@ -117,7 +112,7 @@
 #else
 #define MAYBE_HistoryPlumbing HistoryPlumbing
 #endif
-IN_PROC_BROWSER_TEST_F(IsolatedPrerenderPageLoadMetricsObserverBrowserTest,
+IN_PROC_BROWSER_TEST_F(PrefetchProxyPageLoadMetricsObserverBrowserTest,
                        MAYBE_HistoryPlumbing) {
   base::HistogramTester histogram_tester;
   NavigateToOriginPath("/index.html");
@@ -139,9 +134,8 @@
       "PageLoad.Clients.SubresourceLoading.DaysSinceLastVisitToOrigin", 0, 1);
 }
 
-IN_PROC_BROWSER_TEST_F(IsolatedPrerenderPageLoadMetricsObserverBrowserTest,
+IN_PROC_BROWSER_TEST_F(PrefetchProxyPageLoadMetricsObserverBrowserTest,
                        RecordNothingOnUntrackedPage) {
-  EnableDataSaver();
   base::HistogramTester histogram_tester;
 
   NavigateAway();
diff --git a/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer_unittest.cc b/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer_unittest.cc
index 878314b..ce993607 100644
--- a/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer_unittest.cc
+++ b/chrome/browser/page_load_metrics/observers/prefetch_proxy_page_load_metrics_observer_unittest.cc
@@ -54,14 +54,9 @@
   void set_navigation_url(const GURL& url) { navigation_url_ = url; }
   void set_in_main_frame(bool in_main_frame) { in_main_frame_ = in_main_frame; }
 
-  void StartTest(bool data_saver_enabled) {
+  void StartTest() {
     ResetTest();
 
-    if (data_saver_enabled) {
-      base::CommandLine::ForCurrentProcess()->AppendSwitch(
-          data_reduction_proxy::switches::kEnableDataReductionProxy);
-    }
-
     NavigateAndCommit(navigation_url_);
     tester()->SimulateTimingUpdate(timing_);
   }
@@ -163,7 +158,7 @@
 };
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, BeforeFCP_CSS) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
 
   std::vector<page_load_metrics::mojom::ResourceDataUpdatePtr> resources;
   resources.push_back(CreateCSSResource(true /* was_cached */,
@@ -198,7 +193,7 @@
 }
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, BeforeFCP_JS) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
 
   std::vector<page_load_metrics::mojom::ResourceDataUpdatePtr> resources;
   resources.push_back(CreateJSResource(true /* was_cached */,
@@ -233,7 +228,7 @@
 }
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, BeforeFCP_Other) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
 
   std::vector<page_load_metrics::mojom::ResourceDataUpdatePtr> resources;
   resources.push_back(CreateOtherResource(true /* was_cached */,
@@ -268,7 +263,7 @@
 }
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, BeforeFCP_NotComplete) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
 
   std::vector<page_load_metrics::mojom::ResourceDataUpdatePtr> resources;
   resources.push_back(CreateCSSResource(true /* was_cached */,
@@ -303,7 +298,7 @@
 }
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, BeforeFCP_Subframe) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
   set_in_main_frame(false);
 
   std::vector<page_load_metrics::mojom::ResourceDataUpdatePtr> resources;
@@ -339,7 +334,7 @@
 }
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, AfterFCP) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
 
   std::vector<page_load_metrics::mojom::ResourceDataUpdatePtr> resources;
   resources.push_back(CreateCSSResource(true /* was_cached */,
@@ -374,7 +369,7 @@
 }
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, BeforeFCP_MaxUKM) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
 
   std::vector<page_load_metrics::mojom::ResourceDataUpdatePtr> resources;
   resources.push_back(CreateCSSResource(true /* was_cached */,
@@ -425,33 +420,10 @@
   VerifyUKMEntry(UkmEntry::kcount_css_js_loaded_cache_before_fcpName, 10);
 }
 
-TEST_F(PrefetchProxyPageLoadMetricsObserverTest, BeforeFCP_NoUKM) {
-  StartTest(false /* data_saver_enabled */);
-
-  std::vector<page_load_metrics::mojom::ResourceDataUpdatePtr> resources;
-  resources.push_back(CreateCSSResource(true /* was_cached */,
-                                        true /* is_complete */,
-                                        true /* completed_before_fcp */));
-  resources.push_back(CreateCSSResource(true /* was_cached */,
-                                        true /* is_complete */,
-                                        true /* completed_before_fcp */));
-
-  tester()->SimulateResourceDataUseUpdate(resources);
-  tester()->NavigateToUntrackedUrl();
-
-  tester()->histogram_tester().ExpectUniqueSample(
-      "PageLoad.Clients.SubresourceLoading.LoadedCSSJSBeforeFCP.Noncached", 0,
-      1);
-  tester()->histogram_tester().ExpectUniqueSample(
-      "PageLoad.Clients.SubresourceLoading.LoadedCSSJSBeforeFCP.Cached", 2, 1);
-
-  VerifyNoUKM();
-}
-
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, DontRecordForNonHttp) {
   set_navigation_url(GURL("chrome://version"));
 
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
 
   std::vector<page_load_metrics::mojom::ResourceDataUpdatePtr> resources;
   resources.push_back(CreateCSSResource(true /* was_cached */,
@@ -483,7 +455,7 @@
 }
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, LastVisitToHost_None) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
 
   tester()->NavigateToUntrackedUrl();
 
@@ -497,7 +469,7 @@
 }
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, LastVisitToHost_Fail) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
   plm_observer()->CallOnOriginLastVisitResult(
       {false /* success */, base::Time()});
   tester()->NavigateToUntrackedUrl();
@@ -512,7 +484,7 @@
 }
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, LastVisitToHost_NullTime) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
   plm_observer()->CallOnOriginLastVisitResult(
       {true /* success */, base::Time()});
   tester()->NavigateToUntrackedUrl();
@@ -527,7 +499,7 @@
 }
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, LastVisitToHost_Today) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
   plm_observer()->CallOnOriginLastVisitResult(
       {true /* success */, base::Time::Now()});
 
@@ -543,7 +515,7 @@
 }
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, LastVisitToHost_Yesterday) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
   plm_observer()->CallOnOriginLastVisitResult(
       {true /* success */, base::Time::Now() - base::TimeDelta::FromDays(1)});
 
@@ -559,7 +531,7 @@
 }
 
 TEST_F(PrefetchProxyPageLoadMetricsObserverTest, LastVisitToHost_MaxUKM) {
-  StartTest(true /* data_saver_enabled */);
+  StartTest();
   plm_observer()->CallOnOriginLastVisitResult(
       {true /* success */, base::Time::Now() - base::TimeDelta::FromDays(181)});
 
@@ -574,18 +546,3 @@
   VerifyUKMEntry(UkmEntry::kdays_since_last_visit_to_originName,
                  /*ukm::GetExponentialBucketMin(180,1.70)=*/119);
 }
-
-TEST_F(PrefetchProxyPageLoadMetricsObserverTest, LastVisitToHost_NoUKM) {
-  StartTest(false /* data_saver_enabled */);
-  plm_observer()->CallOnOriginLastVisitResult(
-      {true /* success */, base::Time::Now() - base::TimeDelta::FromDays(1)});
-
-  tester()->NavigateToUntrackedUrl();
-
-  tester()->histogram_tester().ExpectUniqueSample(
-      "PageLoad.Clients.SubresourceLoading.HasPreviousVisitToOrigin", true, 1);
-  tester()->histogram_tester().ExpectUniqueSample(
-      "PageLoad.Clients.SubresourceLoading.DaysSinceLastVisitToOrigin", 1, 1);
-
-  VerifyNoUKM();
-}
diff --git a/chrome/browser/policy/configuration_policy_handler_list_factory.cc b/chrome/browser/policy/configuration_policy_handler_list_factory.cc
index f2be95d3..533c23b7 100644
--- a/chrome/browser/policy/configuration_policy_handler_list_factory.cc
+++ b/chrome/browser/policy/configuration_policy_handler_list_factory.cc
@@ -31,7 +31,6 @@
 #include "chrome/browser/policy/homepage_location_policy_handler.h"
 #include "chrome/browser/policy/javascript_policy_handler.h"
 #include "chrome/browser/policy/network_prediction_policy_handler.h"
-#include "chrome/browser/policy/printing_restrictions_policy_handler.h"
 #include "chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler.h"
 #include "chrome/browser/profiles/force_safe_search_policy_handler.h"
 #include "chrome/browser/profiles/force_youtube_safety_mode_policy_handler.h"
@@ -100,6 +99,7 @@
 #include "extensions/buildflags/buildflags.h"
 #include "media/media_buildflags.h"
 #include "ppapi/buildflags/buildflags.h"
+#include "printing/buildflags/buildflags.h"
 
 #if defined(OS_ANDROID)
 #include "chrome/browser/first_run/android/first_run_prefs.h"
@@ -155,6 +155,10 @@
 #include "extensions/common/manifest.h"
 #endif  // BUILDFLAG(ENABLE_EXTENSIONS)
 
+#if BUILDFLAG(ENABLE_PRINTING)
+#include "chrome/browser/policy/printing_restrictions_policy_handler.h"
+#endif
+
 #if BUILDFLAG(ENABLE_SPELLCHECK)
 #include "components/spellcheck/browser/pref_names.h"
 #endif  // BUILDFLAG(ENABLE_SPELLCHECK)
@@ -1513,12 +1517,6 @@
       SimpleSchemaValidatingPolicyHandler::MANDATORY_ALLOWED));
   handlers->AddHandler(
       std::make_unique<WebUsbAllowDevicesForUrlsPolicyHandler>(chrome_schema));
-  handlers->AddHandler(
-      std::make_unique<PrintingAllowedBackgroundGraphicsModesPolicyHandler>());
-  handlers->AddHandler(
-      std::make_unique<PrintingBackgroundGraphicsDefaultPolicyHandler>());
-  handlers->AddHandler(
-      std::make_unique<PrintingPaperSizeDefaultPolicyHandler>());
   handlers->AddHandler(std::make_unique<DeveloperToolsPolicyHandler>());
   handlers->AddHandler(std::make_unique<FileSelectionDialogsPolicyHandler>());
   handlers->AddHandler(std::make_unique<JavascriptPolicyHandler>());
@@ -2040,6 +2038,15 @@
 
 #endif  // BUILDFLAG(ENABLE_EXTENSIONS)
 
+#if BUILDFLAG(ENABLE_PRINTING)
+  handlers->AddHandler(
+      std::make_unique<PrintingAllowedBackgroundGraphicsModesPolicyHandler>());
+  handlers->AddHandler(
+      std::make_unique<PrintingBackgroundGraphicsDefaultPolicyHandler>());
+  handlers->AddHandler(
+      std::make_unique<PrintingPaperSizeDefaultPolicyHandler>());
+#endif
+
 #if BUILDFLAG(ENABLE_SPELLCHECK)
   handlers->AddHandler(std::make_unique<SpellcheckLanguagePolicyHandler>());
   handlers->AddHandler(
diff --git a/chrome/browser/printing/print_backend_browsertest.cc b/chrome/browser/printing/print_backend_browsertest.cc
index 7e73a04da..de068b5 100644
--- a/chrome/browser/printing/print_backend_browsertest.cc
+++ b/chrome/browser/printing/print_backend_browsertest.cc
@@ -70,7 +70,7 @@
   // Initialize and load the backend service with some test print drivers.
   void LaunchService() {
     print_backend_service_ = PrintBackendServiceTestImpl::LaunchForTesting(
-        remote_, test_print_backend_);
+        remote_, test_print_backend_, /*sandboxed=*/true);
   }
 
   // Load the test backend with a default printer driver.
diff --git a/chrome/browser/printing/print_backend_service_manager.cc b/chrome/browser/printing/print_backend_service_manager.cc
index daa52f3..b9b7a65 100644
--- a/chrome/browser/printing/print_backend_service_manager.cc
+++ b/chrome/browser/printing/print_backend_service_manager.cc
@@ -7,12 +7,14 @@
 #include <string>
 
 #include "base/containers/flat_map.h"
-#include "base/no_destructor.h"
+#include "base/containers/flat_set.h"
+#include "base/logging.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
 #include "chrome/browser/service_sandbox_type.h"
 #include "chrome/grit/generated_resources.h"
 #include "chrome/services/printing/public/mojom/print_backend_service.mojom.h"
+#include "content/public/browser/browser_thread.h"
 #include "content/public/browser/service_process_host.h"
 #include "mojo/public/cpp/bindings/remote.h"
 
@@ -24,18 +26,38 @@
 constexpr base::TimeDelta kResetOnIdleTimeout =
     base::TimeDelta::FromSeconds(20);
 
+PrintBackendServiceManager* g_print_backend_service_manager_singleton = nullptr;
+
 }  // namespace
 
 PrintBackendServiceManager::PrintBackendServiceManager() = default;
 
 PrintBackendServiceManager::~PrintBackendServiceManager() = default;
 
+bool PrintBackendServiceManager::ShouldSandboxPrintBackendService() const {
+  return sandbox_service_;
+}
+
 const mojo::Remote<printing::mojom::PrintBackendService>&
 PrintBackendServiceManager::GetService(const std::string& locale,
                                        const std::string& printer_name) {
-  if (service_remote_for_test_)
-    return *service_remote_for_test_;
+  // Value of `sandbox_service_` will be referenced during the service launch
+  // by `ShouldSandboxPrintBackendService()` if the service is started via
+  // `content::ServiceProcessHost::Launch()`.
+  sandbox_service_ = !PrinterDriverRequiresElevatedPrivilege(printer_name);
 
+  if (sandboxed_service_remote_for_test_) {
+    // The presence of a sandboxed remote for testing signals a testing
+    // environment.  If no unsandboxed test service was provided for fallback
+    // processing then use the sandboxed one for that as well.
+    if (!sandbox_service_ && unsandboxed_service_remote_for_test_)
+      return *unsandboxed_service_remote_for_test_;
+
+    return *sandboxed_service_remote_for_test_;
+  }
+
+  RemotesMap& remote =
+      sandbox_service_ ? sandbox_remotes_ : unsandboxed_remotes_;
   std::string remote_id;
 #if defined(OS_WIN)
   // Windows drivers are not thread safe.  Use a process per driver to prevent
@@ -43,16 +65,19 @@
   // https://crbug.com/957242
   remote_id = printer_name;
 #endif
-  auto iter = remotes_.find(remote_id);
-  if (iter == remotes_.end()) {
+  auto iter = remote.find(remote_id);
+  if (iter == remote.end()) {
     // First time for this `remote_id`.
-    auto result = remotes_.emplace(
+    auto result = remote.emplace(
         printer_name, mojo::Remote<printing::mojom::PrintBackendService>());
     iter = result.first;
   }
 
   mojo::Remote<printing::mojom::PrintBackendService>& service = iter->second;
   if (!service) {
+    VLOG(1) << "Launching print backend "
+            << (sandbox_service_ ? "sandboxed" : "unsandboxed") << " for '"
+            << remote_id << "'";
     content::ServiceProcessHost::Launch(
         service.BindNewPipeAndPassReceiver(),
         content::ServiceProcessHost::Options()
@@ -82,15 +107,44 @@
   return service;
 }
 
+bool PrintBackendServiceManager::PrinterDriverRequiresElevatedPrivilege(
+    const std::string& printer_name) const {
+  return drivers_requiring_elevated_privilege_.contains(printer_name);
+}
+
+void PrintBackendServiceManager::SetPrinterDriverRequiresElevatedPrivilege(
+    const std::string& printer_name) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  VLOG(1) << "Destination '" << printer_name
+          << "' requires elevated privileges.";
+  drivers_requiring_elevated_privilege_.emplace(printer_name);
+}
+
 void PrintBackendServiceManager::SetServiceForTesting(
     mojo::Remote<printing::mojom::PrintBackendService>* remote) {
-  service_remote_for_test_ = remote;
+  sandboxed_service_remote_for_test_ = remote;
+}
+
+void PrintBackendServiceManager::SetServiceForFallbackTesting(
+    mojo::Remote<printing::mojom::PrintBackendService>* remote) {
+  unsandboxed_service_remote_for_test_ = remote;
 }
 
 // static
 PrintBackendServiceManager& PrintBackendServiceManager::GetInstance() {
-  static base::NoDestructor<PrintBackendServiceManager> singleton;
-  return *singleton;
+  if (!g_print_backend_service_manager_singleton) {
+    g_print_backend_service_manager_singleton =
+        new PrintBackendServiceManager();
+  }
+  return *g_print_backend_service_manager_singleton;
+}
+
+// static
+void PrintBackendServiceManager::ResetForTesting() {
+  if (g_print_backend_service_manager_singleton) {
+    delete g_print_backend_service_manager_singleton;
+    g_print_backend_service_manager_singleton = nullptr;
+  }
 }
 
 }  // namespace printing
diff --git a/chrome/browser/printing/print_backend_service_manager.h b/chrome/browser/printing/print_backend_service_manager.h
index 7e3c80f..0b2bb110 100644
--- a/chrome/browser/printing/print_backend_service_manager.h
+++ b/chrome/browser/printing/print_backend_service_manager.h
@@ -8,6 +8,7 @@
 #include <string>
 
 #include "base/containers/flat_map.h"
+#include "base/containers/flat_set.h"
 #include "base/no_destructor.h"
 #include "chrome/services/printing/public/mojom/print_backend_service.mojom.h"
 #include "mojo/public/cpp/bindings/remote.h"
@@ -20,20 +21,43 @@
   PrintBackendServiceManager& operator=(const PrintBackendServiceManager&) =
       delete;
 
+  // Returns true if the print backend service should be sandboxed, false
+  // otherwise.
+  bool ShouldSandboxPrintBackendService() const;
+
   // Acquires a remote handle to the Print Backend Service instance, launching a
   // process to host the service if necessary.
   const mojo::Remote<printing::mojom::PrintBackendService>& GetService(
       const std::string& locale,
       const std::string& printer_name);
 
+  // Query if printer driver has been found to require elevated privilege in
+  // order to have print queries/commands succeed.
+  bool PrinterDriverRequiresElevatedPrivilege(
+      const std::string& printer_name) const;
+
+  // Make note that `printer_name` has been detected as requiring elevated
+  // privileges in order to operate.
+  void SetPrinterDriverRequiresElevatedPrivilege(
+      const std::string& printer_name);
+
   // Overrides the print backend service for testing.  Caller retains ownership
   // of `remote`.
   void SetServiceForTesting(
       mojo::Remote<printing::mojom::PrintBackendService>* remote);
 
+  // Overrides the print backend service for testing when an alternate service
+  // is required for fallback processing after an access denied error.  Caller
+  // retains ownership of `remote`.
+  void SetServiceForFallbackTesting(
+      mojo::Remote<printing::mojom::PrintBackendService>* remote);
+
   // There is to be at most one instance of this at a time.
   static PrintBackendServiceManager& GetInstance();
 
+  // Test support to revert to a fresh instance.
+  static void ResetForTesting();
+
  private:
   friend base::NoDestructor<PrintBackendServiceManager>;
 
@@ -44,11 +68,25 @@
       base::flat_map<std::string,
                      mojo::Remote<printing::mojom::PrintBackendService>>;
 
-  RemotesMap remotes_;
+  // Keep separate mapping of remotes for sandboxed vs. unsandboxed services.
+  RemotesMap sandbox_remotes_;
+  RemotesMap unsandboxed_remotes_;
+
+  // Track if next service started should be sandboxed.
+  bool sandbox_service_ = true;
+
+  // Set of printer drivers which require elevated permissions to operate.
+  // It is expected that most print drivers will succeed with the preconfigured
+  // sandbox permissions.  Should any drivers be discovered to require more than
+  // that (and thus fail with access denied errors) then we need to fallback to
+  // performing the operation with modified restrictions.
+  base::flat_set<std::string> drivers_requiring_elevated_privilege_;
 
   // Override of service to use for testing.
-  mojo::Remote<printing::mojom::PrintBackendService>* service_remote_for_test_ =
-      nullptr;
+  mojo::Remote<printing::mojom::PrintBackendService>*
+      sandboxed_service_remote_for_test_ = nullptr;
+  mojo::Remote<printing::mojom::PrintBackendService>*
+      unsandboxed_service_remote_for_test_ = nullptr;
 };
 
 }  // namespace printing
diff --git a/chrome/browser/printing/print_backend_service_test_impl.cc b/chrome/browser/printing/print_backend_service_test_impl.cc
index cd8f4ea6..1843ade 100644
--- a/chrome/browser/printing/print_backend_service_test_impl.cc
+++ b/chrome/browser/printing/print_backend_service_test_impl.cc
@@ -37,7 +37,8 @@
 std::unique_ptr<PrintBackendServiceTestImpl>
 PrintBackendServiceTestImpl::LaunchForTesting(
     mojo::Remote<mojom::PrintBackendService>& remote,
-    scoped_refptr<TestPrintBackend> backend) {
+    scoped_refptr<TestPrintBackend> backend,
+    bool sandboxed) {
   std::unique_ptr<PrintBackendServiceTestImpl> service =
       LaunchUninitialized(remote);
 
@@ -47,7 +48,12 @@
 
   // Register this test version of print backend service to be used instead of
   // launching instances out-of-process on-demand.
-  PrintBackendServiceManager::GetInstance().SetServiceForTesting(&remote);
+  if (sandboxed) {
+    PrintBackendServiceManager::GetInstance().SetServiceForTesting(&remote);
+  } else {
+    PrintBackendServiceManager::GetInstance().SetServiceForFallbackTesting(
+        &remote);
+  }
 
   return service;
 }
diff --git a/chrome/browser/printing/print_backend_service_test_impl.h b/chrome/browser/printing/print_backend_service_test_impl.h
index 41838dd..cffe7199b 100644
--- a/chrome/browser/printing/print_backend_service_test_impl.h
+++ b/chrome/browser/printing/print_backend_service_test_impl.h
@@ -33,9 +33,12 @@
   void Init(const std::string& locale) override;
 
   // Launch the service in-process for testing using the provided backend.
+  // `sandboxed` identifies if this service is potentially subject to
+  // experiencing access-denied errors on some commands.
   static std::unique_ptr<PrintBackendServiceTestImpl> LaunchForTesting(
       mojo::Remote<mojom::PrintBackendService>& remote,
-      scoped_refptr<TestPrintBackend> backend);
+      scoped_refptr<TestPrintBackend> backend,
+      bool sandboxed);
 
  private:
   friend class PrintBackendBrowserTest;
@@ -44,6 +47,10 @@
   static std::unique_ptr<PrintBackendServiceTestImpl> LaunchUninitialized(
       mojo::Remote<mojom::PrintBackendService>& remote);
 
+  // When pretending to be sandboxed, have the possibility of getting access
+  // denied errors.
+  bool is_sandboxed_ = false;
+
   scoped_refptr<TestPrintBackend> test_print_backend_;
 };
 
diff --git a/chrome/browser/profiles/BUILD.gn b/chrome/browser/profiles/BUILD.gn
index 691b53e..15f69bd 100644
--- a/chrome/browser/profiles/BUILD.gn
+++ b/chrome/browser/profiles/BUILD.gn
@@ -39,6 +39,7 @@
     "//components/data_reduction_proxy/core/browser",
     "//components/keyed_service/content",
     "//components/language/core/browser",
+    "//components/live_caption:constants",
     "//components/media_router/common",
     "//components/pref_registry",
     "//components/profile_metrics",
diff --git a/chrome/browser/profiles/DEPS b/chrome/browser/profiles/DEPS
index a662808e..dbdc3fe9 100644
--- a/chrome/browser/profiles/DEPS
+++ b/chrome/browser/profiles/DEPS
@@ -1,5 +1,6 @@
 include_rules = [
   "+ash/components/account_manager",
+  "+components/live_caption:constants",
   "+components/profile_metrics",
 ]
 
diff --git a/chrome/browser/profiles/profile.cc b/chrome/browser/profiles/profile.cc
index 2c5ce26..7f6c64b 100644
--- a/chrome/browser/profiles/profile.cc
+++ b/chrome/browser/profiles/profile.cc
@@ -23,6 +23,7 @@
 #include "components/data_reduction_proxy/core/browser/data_reduction_proxy_prefs.h"
 #include "components/keyed_service/content/browser_context_dependency_manager.h"
 #include "components/language/core/browser/pref_names.h"
+#include "components/live_caption/pref_names.h"
 #include "components/media_router/common/pref_names.h"
 #include "components/pref_registry/pref_registry_syncable.h"
 #include "components/profile_metrics/browser_profile_type.h"
diff --git a/chrome/browser/reputation/local_heuristics.cc b/chrome/browser/reputation/local_heuristics.cc
index 2ccd50e0..ff9970c 100644
--- a/chrome/browser/reputation/local_heuristics.cc
+++ b/chrome/browser/reputation/local_heuristics.cc
@@ -12,6 +12,7 @@
 #include "chrome/browser/lookalikes/lookalike_url_navigation_throttle.h"
 #include "chrome/browser/lookalikes/lookalike_url_service.h"
 #include "chrome/common/chrome_features.h"
+#include "components/lookalikes/core/features.h"
 #include "components/lookalikes/core/lookalike_url_util.h"
 #include "components/reputation/core/safety_tips_config.h"
 #include "components/security_state/core/features.h"
@@ -27,8 +28,9 @@
 const base::FeatureParam<bool> kEnableLookalikeEditDistanceSiteEngagement{
     &security_state::features::kSafetyTipUI, "editdistance_siteengagement",
     true};
-const base::FeatureParam<bool> kEnableLookalikeTargetEmbedding{
-    &security_state::features::kSafetyTipUI, "targetembedding", false};
+const base::FeatureParam<bool> kEnableTargetEmbeddingSafetyTips{
+    &lookalikes::features::kDetectTargetEmbeddingLookalikes, "safety_tips",
+    true};
 
 // Binary search through |words| to find |needle|.
 bool SortedWordListContains(const std::string& needle,
@@ -64,7 +66,7 @@
       base::BindRepeating(
           &reputation::IsTargetHostAllowlistedBySafetyTipsComponent, config);
   if (!GetMatchingDomain(navigated_domain, engaged_sites, in_target_allowlist,
-                         &matched_domain, &match_type)) {
+                         config, &matched_domain, &match_type)) {
     return false;
   }
 
@@ -103,7 +105,12 @@
       // Target Embedding should block URL Navigation.
       return false;
     case LookalikeUrlMatchType::kTargetEmbeddingForSafetyTips:
-      return kEnableLookalikeTargetEmbedding.Get();
+      // Require that target embedding is enabled globally *and* the feature
+      // parameter for safety tips is enabled, too. This allows disabling only
+      // safety tips by enabling the feature and unsetting this parameter.
+      return base::FeatureList::IsEnabled(
+                 lookalikes::features::kDetectTargetEmbeddingLookalikes) &&
+             kEnableTargetEmbeddingSafetyTips.Get();
     case LookalikeUrlMatchType::kSkeletonMatchTop5k:
       return is_safety_tip_for_simplified_domains_enabled ||
              kEnableLookalikeTopSites.Get();
diff --git a/chrome/browser/reputation/url_elision_policy_unittest.cc b/chrome/browser/reputation/url_elision_policy_unittest.cc
index 7eb0f12..e130de59 100644
--- a/chrome/browser/reputation/url_elision_policy_unittest.cc
+++ b/chrome/browser/reputation/url_elision_policy_unittest.cc
@@ -76,7 +76,7 @@
 
   // ...but not when allowlisted.
   reputation::SetSafetyTipAllowlistPatterns(
-      {"alongbutstillallowlisteddomain.com/"}, {});
+      {"alongbutstillallowlisteddomain.com/"}, {}, {});
   EXPECT_FALSE(ShouldElideToRegistrableDomain(kUrl));
 }
 
diff --git a/chrome/browser/resources/BUILD.gn b/chrome/browser/resources/BUILD.gn
index 80b30c8..1ce480f 100644
--- a/chrome/browser/resources/BUILD.gn
+++ b/chrome/browser/resources/BUILD.gn
@@ -53,6 +53,7 @@
       "chromeos/login:resources",
       "chromeos/multidevice_internals:resources",
       "chromeos/network_ui:resources",
+      "chromeos/projector:resources",
       "nearby_internals:resources",
       "nearby_share:resources",
       "settings/chromeos:resources",
@@ -141,6 +142,7 @@
     if (is_chromeos_ash) {
       deps += [
         "chromeos:closure_compile",
+        "chromeos/projector:closure_compile",
         "nearby_share:closure_compile",
         "nearby_share/shared:closure_compile",
         "nearby_share/shared:closure_compile_module",
diff --git a/chrome/browser/resources/bookmarks/toolbar.js b/chrome/browser/resources/bookmarks/toolbar.js
index 9b603b5..0e5cdde 100644
--- a/chrome/browser/resources/bookmarks/toolbar.js
+++ b/chrome/browser/resources/bookmarks/toolbar.js
@@ -3,13 +3,13 @@
 // found in the LICENSE file.
 
 import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.m.js';
-import 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
-import 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js';
 import 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_selection_overlay.js';
 import 'chrome://resources/cr_elements/icons.m.js';
 import './shared_style.js';
 import './strings.m.js';
 
+import {CrToolbarElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
+import {CrToolbarSearchFieldElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js';
 import {assert} from 'chrome://resources/js/assert.m.js';
 import {StoreObserver} from 'chrome://resources/js/cr/ui/store.m.js';
 import {StoreClientInterface as CrUiStoreClientInterface} from 'chrome://resources/js/cr/ui/store_client.m.js';
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/ui_manager.js b/chrome/browser/resources/chromeos/accessibility/select_to_speak/ui_manager.js
index 5778472..47b0165 100644
--- a/chrome/browser/resources/chromeos/accessibility/select_to_speak/ui_manager.js
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/ui_manager.js
@@ -14,17 +14,18 @@
 const SelectToSpeakPanelAction =
     chrome.accessibilityPrivate.SelectToSpeakPanelAction;
 
-// This must be the same as in ash/system/accessibility/select_to_speak_tray.cc:
+// This must be the same as in
+// ash/system/accessibility/select_to_speak/select_to_speak_tray.cc:
 // ash::kSelectToSpeakTrayClassName.
 export const SELECT_TO_SPEAK_TRAY_CLASS_NAME =
     'tray/TrayBackgroundView/SelectToSpeakTray';
 
 // This must match the name of view class that implements the menu view:
-// ash/system/accessibility/select_to_speak_menu_view.h
+// ash/system/accessibility/select_to_speak/select_to_speak_menu_view.h
 const SELECT_TO_SPEAK_MENU_CLASS_NAME = 'SelectToSpeakMenuView';
 
 // This must match the name of view class that implements the speed view:
-// ash/system/accessibility/select_to_speak_speed_view.h
+// ash/system/accessibility/select_to_speak/select_to_speak_speed_view.h
 const SELECT_TO_SPEAK_SPEED_CLASS_NAME = 'SelectToSpeakSpeedView';
 
 // This must match the name of view class that implements the bubble views:
@@ -396,4 +397,4 @@
       return n.className === SELECT_TO_SPEAK_TRAY_CLASS_NAME;
     }) !== undefined;
   }
-}
\ No newline at end of file
+}
diff --git a/chrome/browser/resources/chromeos/projector/BUILD.gn b/chrome/browser/resources/chromeos/projector/BUILD.gn
index 5e7155b05..e8e8312d 100644
--- a/chrome/browser/resources/chromeos/projector/BUILD.gn
+++ b/chrome/browser/resources/chromeos/projector/BUILD.gn
@@ -2,7 +2,6 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import("//chrome/common/features.gni")
 import("//third_party/closure_compiler/compile_js.gni")
 import("//tools/grit/grit_rule.gni")
 import("//ui/webui/resources/tools/generate_grd.gni")
@@ -15,13 +14,12 @@
   input_files = [
     "selfie_cam.html",
     "selfie_cam.js",
+    "selfie_cam.css",
   ]
   input_files_base_dir = rebase_path(".", "//")
 }
 
 grit("resources") {
-  defines = chrome_grit_defines
-
   # These arguments are needed since the grd is generated at build time.
   enable_input_discovery_for_gn_analyze = false
   source = "$target_gen_dir/resources.grd"
diff --git a/chrome/browser/resources/chromeos/projector/selfie_cam.css b/chrome/browser/resources/chromeos/projector/selfie_cam.css
new file mode 100644
index 0000000..f2366de
--- /dev/null
+++ b/chrome/browser/resources/chromeos/projector/selfie_cam.css
@@ -0,0 +1,21 @@
+/* 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. */
+
+body {
+  background-color: transparent;
+  bottom: 0;
+  height: 100%;
+  left: 0;
+  margin: 0;
+  top: 0;
+  width: 100%;
+}
+
+.flip-horizontally {
+  transform: scaleX(-1);
+}
+
+.circular {
+  border-radius: 50%;
+}
diff --git a/chrome/browser/resources/chromeos/projector/selfie_cam.html b/chrome/browser/resources/chromeos/projector/selfie_cam.html
index 7736ca0..3e260b4 100644
--- a/chrome/browser/resources/chromeos/projector/selfie_cam.html
+++ b/chrome/browser/resources/chromeos/projector/selfie_cam.html
@@ -6,8 +6,11 @@
 
 <!DOCTYPE html>
 <html>
+  <head>
+    <link rel="stylesheet" href="selfie_cam.css">
+  </head>
   <body>
-    <video autoplay></video>
+    <video class="flip-horizontally circular" autoplay></video>
     <script type="module" src="selfie_cam.js"></script>
   </body>
 </html>
diff --git a/chrome/browser/resources/chromeos/projector/selfie_cam.js b/chrome/browser/resources/chromeos/projector/selfie_cam.js
index 33d8ca1..84428b06 100644
--- a/chrome/browser/resources/chromeos/projector/selfie_cam.js
+++ b/chrome/browser/resources/chromeos/projector/selfie_cam.js
@@ -2,12 +2,22 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// Grabs video feed from user's camera. If the user's device does not have a
-// camera, then the error is caught below.
-navigator.mediaDevices.getUserMedia({video: true})
-    .then(stream => {
-      document.body.querySelector('video').srcObject = stream;
-    })
-    .catch(err => {
-      console.error(err.name + ': ' + err.message);
-    });
+/**
+ * Grabs video feed from user's camera. If the user's device does not have a
+ * camera, then the error is caught below.
+ * @param {!MediaStreamConstraints} constraints
+ */
+async function getMedia(constraints) {
+  let stream = null;
+  try {
+    stream = await navigator.mediaDevices.getUserMedia(constraints);
+    document.body.querySelector('video').srcObject = stream;
+  } catch (err) {
+    console.error(err.name + ': ' + err.message);
+  }
+}
+
+// Since the selfie cam is circular, we want equal width and height.
+document.addEventListener('DOMContentLoaded', function() {
+  getMedia({video: {width: window.innerWidth, height: window.innerHeight}});
+}, false);
diff --git a/chrome/browser/resources/download_shelf/download_button.html b/chrome/browser/resources/download_shelf/download_button.html
index 1bed746..837e39c 100644
--- a/chrome/browser/resources/download_shelf/download_button.html
+++ b/chrome/browser/resources/download_shelf/download_button.html
@@ -13,7 +13,9 @@
   cursor: pointer;
   font-family: Roboto;
   font-weight: 500;
+  min-width: 79px;
   padding: 8px 16px;
+  text-align: center;
   transition: background-color 300ms;
   user-select: none;
 }
diff --git a/chrome/browser/resources/download_shelf/download_item.html b/chrome/browser/resources/download_shelf/download_item.html
index 9f10fea20..66fd83f 100644
--- a/chrome/browser/resources/download_shelf/download_item.html
+++ b/chrome/browser/resources/download_shelf/download_item.html
@@ -5,7 +5,8 @@
   --pi: 3.14159265358979;
   --spinner-color: var(--google-grey-900-rgb);
   --text-width: 140px;
-  --warn-text-width: 245px;
+  --warn-text-width: 241px;
+  --warn-save-text-width: 318px;
   --transparent-button-color: rgba(0, 0, 0, 0);
 }
 
@@ -104,7 +105,8 @@
   border: none;
   border-radius: 4px;
   height: 32px;
-  margin: 8px;
+  margin-inline-end: 4px;
+  margin-inline-start: 8px;
   transition: background-color 300ms;
   width: 32px;
   z-index: 2;
@@ -154,6 +156,7 @@
 #separator {
   background-color: rgba(0, 0, 0, .1);
   height: 90%;
+  margin-inline-start: 4px;
   width: 1px;
 }
 
@@ -162,8 +165,15 @@
   line-height: 150%;
 }
 
+#save-button {
+  display: none;
+  margin-inline-start: 4px;
+  z-index: 2;
+}
+
 #discard-button {
   display: none;
+  margin-inline-start: 4px;
   z-index: 2;
 }
 
@@ -171,21 +181,33 @@
   width: var(--warn-text-width);
 }
 
-[data-display-mode='warn'] .progress {
+[data-display-mode='warn-save'] #text-container {
+  width: var(--warn-save-text-width);
+}
+
+[data-display-mode^='warn'] .progress {
   padding: 0 0 0 3px;
 }
 
-[data-display-mode='warn'] #discard-button,
-[data-display-mode='warn'] #error-icon,
-[data-display-mode='warn'] #warning-text {
+[data-display-mode^='warn'] #discard-button,
+[data-display-mode^='warn'] #error-icon,
+[data-display-mode^='warn'] #warning-text {
   display: block;
 }
 
-[data-display-mode='warn'] #shadow-mask,
-[data-display-mode='warn'] #file-icon,
-[data-display-mode='warn'] #filename,
-[data-display-mode='warn'] #status-text,
-[data-display-mode='warn'] .progress-indicator {
+[data-display-mode^='warn'] #shadow-mask,
+[data-display-mode^='warn'] #file-icon,
+[data-display-mode^='warn'] #filename,
+[data-display-mode^='warn'] #status-text,
+[data-display-mode^='warn'] .progress-indicator {
+  display: none;
+}
+
+[data-display-mode='warn-save'] #save-button {
+  display: block;
+}
+
+[data-display-mode='warn-save'] #dropdown-button {
   display: none;
 }
 
@@ -212,6 +234,7 @@
     <div id="status-text"></div>
     <div id="warning-text"></div>
   </div>
+  <download-button id="save-button"></download-button>
   <download-button id="discard-button"></download-button>
   <button id="dropdown-button">
     <svg id="dropdown-icon" viewBox="0 0 16 16">
diff --git a/chrome/browser/resources/download_shelf/download_item.js b/chrome/browser/resources/download_shelf/download_item.js
index 1b465154..1b20d98a 100644
--- a/chrome/browser/resources/download_shelf/download_item.js
+++ b/chrome/browser/resources/download_shelf/download_item.js
@@ -10,6 +10,7 @@
 import './download_button.js';
 import './strings.m.js';
 
+import {assert} from 'chrome://resources/js/assert.m.js';
 import {CustomElement} from 'chrome://resources/js/custom_element.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
 
@@ -18,8 +19,12 @@
 
 /** @enum {string} */
 const DisplayMode = {
+  // Shows icon + filename + context menu button.
   kNormal: 'normal',
-  kWarn: 'warn'
+  // Shows icon + warning text + discard button + context menu button.
+  kWarn: 'warn',
+  // Shows icon + warning text + save button + discard button.
+  kWarnSave: 'warn-save'
 };
 
 export class DownloadItemElement extends CustomElement {
@@ -60,6 +65,25 @@
     return this.item_;
   }
 
+  /**
+   * @private
+   * @return {string}
+   */
+  get clampedWarningText_() {
+    // Views uses ui/gfx/text_elider.cc to elide text given a maximum width.
+    // For simplicity, we instead elide text by restricting text length.
+    const maxFilenameLength = 19;
+    const warningText = this.item.warningText;
+    if (!warningText) {
+      return '';
+    }
+
+    const filepath = this.item.fileNameDisplayString;
+    const filename = filepath.substring(filepath.lastIndexOf('/') + 1);
+    return warningText.replace(
+        filename, this.elideFilename_(filename, maxFilenameLength));
+  }
+
   /** @private */
   update_() {
     const item = this.item_;
@@ -102,11 +126,18 @@
       this.$('#file-icon').src = icon;
     });
 
-    downloadElement.dataset.displayMode =
-        this.item_.mode === DownloadMode.kNormal ? DisplayMode.kNormal :
-                                                   DisplayMode.kWarn;
-    this.$('#warning-text').innerText =
-        item.warningText ? item.warningText : '';
+    if (item.mode === DownloadMode.kNormal) {
+      downloadElement.dataset.displayMode = DisplayMode.kNormal;
+    } else if (
+        item.mode === DownloadMode.kDangerous ||
+        item.mode === DownloadMode.kMixedContentWarn) {
+      downloadElement.dataset.displayMode = DisplayMode.kWarnSave;
+    } else {
+      downloadElement.dataset.displayMode = DisplayMode.kWarn;
+    }
+
+    this.$('#save-button').innerText = item.warningConfirmButtonText;
+    this.$('#warning-text').innerText = this.clampedWarningText_;
   }
 
   /** @param {number} value */
@@ -135,6 +166,30 @@
     // TODO(crbug.com/1182529): Notify C++ through mojo. Remove this item
     // from download_list.
   }
+
+  /**
+   * Elide a filename to a maximum length.
+   * The extension of the filename will be kept if it has one.
+   * @param {string} s A filename.
+   * @param {number} maxlen The maximum length after elided.
+   * @private
+   */
+  elideFilename_(s, maxlen) {
+    assert(maxlen > 6);
+
+    if (s.length <= maxlen) {
+      return s;
+    }
+
+    const extIndex = s.lastIndexOf('.');
+    if (extIndex === -1) {
+      // |s| does not have an extension.
+      return s.substr(0, maxlen - 3) + '...';
+    } else {
+      const subfix = '...' + s.substr(extIndex);
+      return s.substr(0, maxlen - subfix.length) + subfix;
+    }
+  }
 }
 
 customElements.define('download-item', DownloadItemElement);
diff --git a/chrome/browser/resources/downloads/toolbar.js b/chrome/browser/resources/downloads/toolbar.js
index e46911df..baf9b705 100644
--- a/chrome/browser/resources/downloads/toolbar.js
+++ b/chrome/browser/resources/downloads/toolbar.js
@@ -4,7 +4,6 @@
 
 import 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.m.js';
 import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.m.js';
-import 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
 import 'chrome://resources/cr_elements/hidden_style_css.m.js';
 import 'chrome://resources/cr_elements/icons.m.js';
 import 'chrome://resources/cr_elements/shared_vars_css.m.js';
@@ -13,6 +12,7 @@
 import './strings.m.js';
 
 import {getToastManager} from 'chrome://resources/cr_elements/cr_toast/cr_toast_manager.m.js';
+import {CrToolbarElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
 import {assert} from 'chrome://resources/js/assert.m.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
 import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
diff --git a/chrome/browser/resources/feed_internals/feed_internals.html b/chrome/browser/resources/feed_internals/feed_internals.html
index 8dd40627..70797978 100644
--- a/chrome/browser/resources/feed_internals/feed_internals.html
+++ b/chrome/browser/resources/feed_internals/feed_internals.html
@@ -27,6 +27,13 @@
 
 <body>
 
+  <h2>Web Feed</h2>
+  <label>
+    <input type="checkbox" id="enable-webfeed-follow-intro-debug"
+           name="enable-webfeed-follow-intro-debug">
+    <strong>Enable IPH debug mode</strong>
+  </label>
+
   <h2>Properties</h2>
   <table>
     <tr>
@@ -179,15 +186,5 @@
   <h2>Feed Stream Data</h2>
   <input id="feed-stream-data-file" type="file">
   <button id="feed-stream-data-override">Override</button>
-
-  <h2>WebFeed UI</h2>
-  <p>WebFeed Follow Intro Debug enabled: </p>
-  <p id="webfeed-follow-intro-debug-enabled-status"></p>
-  <label>
-    <input type="checkbox" id="enable-webfeed-follow-intro-debug"
-           name="enable-webfeed-follow-intro-debug">
-    Enable WebFeed Follow Intro Debug
-  </label>
-  <button id="enable-webfeed-follow-intro-debug-apply">Apply</button>
 </body>
 </html>
diff --git a/chrome/browser/resources/feed_internals/feed_internals.js b/chrome/browser/resources/feed_internals/feed_internals.js
index 149573f0..837d638 100644
--- a/chrome/browser/resources/feed_internals/feed_internals.js
+++ b/chrome/browser/resources/feed_internals/feed_internals.js
@@ -26,8 +26,9 @@
     $('load-stream-status').textContent = properties.loadStreamStatus;
     $('feed-fetch-url').textContent = properties.feedFetchUrl.url;
     $('feed-actions-url').textContent = properties.feedActionsUrl.url;
-    $('webfeed-follow-intro-debug-enabled-status').textContent =
+    $('enable-webfeed-follow-intro-debug').checked =
         properties.isWebFeedFollowIntroDebugEnabled;
+    $('enable-webfeed-follow-intro-debug').disabled = false;
   });
 }
 
@@ -169,11 +170,11 @@
     }
   });
 
-  $('enable-webfeed-follow-intro-debug-apply')
-      .addEventListener('click', function() {
-        pageHandler.setWebFeedFollowIntroDebugEnabled(
-            $('enable-webfeed-follow-intro-debug').checked);
-      });
+  $('enable-webfeed-follow-intro-debug').addEventListener('click', function() {
+    pageHandler.setWebFeedFollowIntroDebugEnabled(
+        $('enable-webfeed-follow-intro-debug').checked);
+    $('enable-webfeed-follow-intro-debug').disabled = true;
+  });
 }
 
 function updatePage() {
diff --git a/chrome/browser/resources/history/history_toolbar.js b/chrome/browser/resources/history/history_toolbar.js
index ae712d0..6fd5cf1 100644
--- a/chrome/browser/resources/history/history_toolbar.js
+++ b/chrome/browser/resources/history/history_toolbar.js
@@ -3,10 +3,11 @@
 // found in the LICENSE file.
 
 import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.m.js';
-import 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
 import './shared_style.js';
 import './strings.m.js';
 
+import {CrToolbarElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
+import {CrToolbarSearchFieldElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
 import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
diff --git a/chrome/browser/resources/memories/app.js b/chrome/browser/resources/memories/app.js
index 598ab47..ca2b496 100644
--- a/chrome/browser/resources/memories/app.js
+++ b/chrome/browser/resources/memories/app.js
@@ -9,13 +9,14 @@
 import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
 import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';
 import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.m.js';
-import 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
 import 'chrome://resources/cr_elements/shared_style_css.m.js';
 import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
 import 'chrome://resources/polymer/v3_0/iron-scroll-threshold/iron-scroll-threshold.js';
 
 import {MemoriesResult, PageCallbackRouter, PageHandlerRemote} from '/chrome/browser/ui/webui/memories/memories.mojom-webui.js';
 import {Visit} from '/components/history_clusters/core/memories.mojom-webui.js';
+import {CrToolbarElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
+import {CrToolbarSearchFieldElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js';
 import {assert} from 'chrome://resources/js/assert.m.js';
 import {UnguessableToken} from 'chrome://resources/mojo/mojo/public/mojom/base/unguessable_token.mojom-webui.js';
 import {Url} from 'chrome://resources/mojo/url/mojom/url.mojom-webui.js';
diff --git a/chrome/browser/resources/settings/chromeos/internet_page/cellular_networks_list.html b/chrome/browser/resources/settings/chromeos/internet_page/cellular_networks_list.html
index 096c027..0ee0445 100644
--- a/chrome/browser/resources/settings/chromeos/internet_page/cellular_networks_list.html
+++ b/chrome/browser/resources/settings/chromeos/internet_page/cellular_networks_list.html
@@ -111,9 +111,11 @@
             </div>
           </div>
           <div id="alignEnd">
-            <paper-spinner-lite id="inhibitedSpinner"
-                active="[[isDeviceInhibited_]]">
-            </paper-spinner-lite>
+            <template is="dom-if" if="[[canShowSpinner]]" restamp>
+              <paper-spinner-lite id="inhibitedSpinner"
+                  active="[[isDeviceInhibited_]]">
+              </paper-spinner-lite>
+            </template>
             <template is="dom-if" if="[[showAddESimButton_(cellularDeviceState,
                 globalPolicy)]]" restamp>
               <cr-icon-button class="icon-add-cellular add-button"
diff --git a/chrome/browser/resources/settings/chromeos/internet_page/cellular_networks_list.js b/chrome/browser/resources/settings/chromeos/internet_page/cellular_networks_list.js
index 42f59bf..37b36ea 100644
--- a/chrome/browser/resources/settings/chromeos/internet_page/cellular_networks_list.js
+++ b/chrome/browser/resources/settings/chromeos/internet_page/cellular_networks_list.js
@@ -45,6 +45,15 @@
     },
 
     /**
+     * If true, inhibited spinner can be shown, it will be shown
+     * if true and cellular is inhibited.
+     * @type {boolean}
+     */
+    canShowSpinner: {
+      type: Boolean,
+    },
+
+    /**
      * Device state for the tether network type. This device state should be
      * used for instant tether networks.
      * @type {!OncMojo.DeviceStateProperties|undefined}
diff --git a/chrome/browser/resources/settings/chromeos/internet_page/internet_page.html b/chrome/browser/resources/settings/chromeos/internet_page/internet_page.html
index 5acbd417..b9012a3 100644
--- a/chrome/browser/resources/settings/chromeos/internet_page/internet_page.html
+++ b/chrome/browser/resources/settings/chromeos/internet_page/internet_page.html
@@ -161,7 +161,8 @@
               global-policy="[[globalPolicy_]]"
               vpn-providers="[[vpnProviders_]]"
               show-spinner="{{showSpinner_}}"
-              is-connected-to-non-cellular-network="[[isConnectedToNonCellularNetwork_]]">
+              is-connected-to-non-cellular-network="[[isConnectedToNonCellularNetwork_]]"
+              is-cellular-setup-active="[[showCellularSetupDialog_]]">
           </settings-internet-subpage>
         </settings-subpage>
       </template>
diff --git a/chrome/browser/resources/settings/chromeos/internet_page/internet_subpage.html b/chrome/browser/resources/settings/chromeos/internet_page/internet_subpage.html
index aee6113..7ce6b313 100644
--- a/chrome/browser/resources/settings/chromeos/internet_page/internet_subpage.html
+++ b/chrome/browser/resources/settings/chromeos/internet_page/internet_subpage.html
@@ -184,7 +184,8 @@
               cellular-device-state="[[deviceState]]"
               tether-device-state="[[tetherDeviceState]]"
               global-policy="[[globalPolicy]]"
-              is-connected-to-non-cellular-network="[[isConnectedToNonCellularNetwork]]">
+              is-connected-to-non-cellular-network="[[isConnectedToNonCellularNetwork]]"
+              can-show-spinner="[[!isCellularSetupActive]]">
           </cellular-networks-list>
         </template>
 
diff --git a/chrome/browser/resources/settings/chromeos/internet_page/internet_subpage.js b/chrome/browser/resources/settings/chromeos/internet_page/internet_subpage.js
index e783316e..fe753226 100644
--- a/chrome/browser/resources/settings/chromeos/internet_page/internet_subpage.js
+++ b/chrome/browser/resources/settings/chromeos/internet_page/internet_subpage.js
@@ -65,6 +65,10 @@
       type: Boolean,
     },
 
+    isCellularSetupActive: {
+      type: Boolean,
+    },
+
     /**
      * List of all network state data for the network type.
      * @private {!Array<!OncMojo.NetworkStateProperties>}
diff --git a/chrome/browser/resources/settings/chromeos/os_settings.gni b/chrome/browser/resources/settings/chromeos/os_settings.gni
index 674b00b..79a3a158 100644
--- a/chrome/browser/resources/settings/chromeos/os_settings.gni
+++ b/chrome/browser/resources/settings/chromeos/os_settings.gni
@@ -257,6 +257,8 @@
                              "chrome/browser/resources/settings/chromeos/os_settings_page/main_page_behavior.html|MainPageBehavior",
                              "chrome/browser/resources/settings/chromeos/search_handler.html|getSearchHandler, setSearchHandlerForTesting",
                              "ui/webui/resources/cr_components/chromeos/network/network_listener_behavior.html|NetworkListenerBehavior",
+                             "ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar.html|CrToolbarElement",
+                             "ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar_search_field.html|CrToolbarSearchFieldElement",
                              "ui/webui/resources/cr_elements/cr_slider/cr_slider.html|SliderTick",
                              "ui/webui/resources/html/assert.html|assert,assertNotReached",
                              "ui/webui/resources/html/cr.html|sendWithPromise,removeWebUIListener,addWebUIListener,WebUIListener",
diff --git a/chrome/browser/resources/settings/chromeos/os_settings_ui/os_settings_ui.html b/chrome/browser/resources/settings/chromeos/os_settings_ui/os_settings_ui.html
index b86d9167..09a6da6 100644
--- a/chrome/browser/resources/settings/chromeos/os_settings_ui/os_settings_ui.html
+++ b/chrome/browser/resources/settings/chromeos/os_settings_ui/os_settings_ui.html
@@ -3,6 +3,7 @@
 <link rel="import" href="chrome://resources/cr_elements/cr_container_shadow_behavior.html">
 <link rel="import" href="chrome://resources/cr_elements/cr_drawer/cr_drawer.html">
 <link rel="import" href="chrome://resources/cr_elements/cr_page_host_style_css.html">
+<link rel="import" href="chrome://resources/cr_elements/cr_toolbar/cr_toolbar_search_field.html">
 <link rel="import" href="chrome://resources/cr_elements/policy/cr_policy_indicator_behavior.html">
 <link rel="import" href="chrome://resources/cr_elements/icons.html">
 <link rel="import" href="chrome://resources/cr_elements/shared_vars_css.html">
diff --git a/chrome/browser/resources/settings/settings.js b/chrome/browser/resources/settings/settings.js
index e8f45136e..2c1b249 100644
--- a/chrome/browser/resources/settings/settings.js
+++ b/chrome/browser/resources/settings/settings.js
@@ -4,6 +4,8 @@
 
 import './settings_ui/settings_ui.js';
 
+export {CrToolbarElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
+export {CrToolbarSearchFieldElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js';
 export {PluralStringProxyImpl as SettingsPluralStringProxyImpl} from 'chrome://resources/js/plural_string_proxy.js';
 export {AboutPageBrowserProxy, AboutPageBrowserProxyImpl, PromoteUpdaterStatus, UpdateStatus} from './about_page/about_page_browser_proxy.js';
 export {AppearanceBrowserProxy, AppearanceBrowserProxyImpl} from './appearance_page/appearance_browser_proxy.js';
diff --git a/chrome/browser/resources/settings/settings_ui/settings_ui.js b/chrome/browser/resources/settings/settings_ui/settings_ui.js
index be9ccf4..b45f56f3 100644
--- a/chrome/browser/resources/settings/settings_ui/settings_ui.js
+++ b/chrome/browser/resources/settings/settings_ui/settings_ui.js
@@ -12,7 +12,6 @@
  */
 import 'chrome://resources/cr_elements/cr_drawer/cr_drawer.js';
 import 'chrome://resources/cr_elements/cr_page_host_style_css.js';
-import 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
 import 'chrome://resources/cr_elements/icons.m.js';
 import 'chrome://resources/cr_elements/shared_vars_css.m.js';
 import 'chrome://resources/polymer/v3_0/paper-styles/color.js';
@@ -24,6 +23,8 @@
 import '../settings_vars_css.js';
 
 import {CrContainerShadowBehavior} from 'chrome://resources/cr_elements/cr_container_shadow_behavior.m.js';
+import {CrToolbarElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
+import {CrToolbarSearchFieldElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js';
 import {FindShortcutBehavior} from 'chrome://resources/cr_elements/find_shortcut_behavior.js';
 import {assert} from 'chrome://resources/js/assert.m.js';
 import {isChromeOS} from 'chrome://resources/js/cr.m.js';
diff --git a/chrome/browser/safe_browsing/download_protection/check_client_download_request_base.cc b/chrome/browser/safe_browsing/download_protection/check_client_download_request_base.cc
index a9069959..9d7d059 100644
--- a/chrome/browser/safe_browsing/download_protection/check_client_download_request_base.cc
+++ b/chrome/browser/safe_browsing/download_protection/check_client_download_request_base.cc
@@ -503,8 +503,6 @@
       base::BindOnce(&CheckClientDownloadRequestBase::OnURLLoaderComplete,
                      GetWeakPtr()));
   request_start_time_ = base::TimeTicks::Now();
-  UMA_HISTOGRAM_COUNTS_1M("SBClientDownload.DownloadRequestPayloadSize",
-                          client_download_request_data_.size());
 
   // Add the access token to the proto for display on chrome://safe-browsing
   client_download_request_->set_access_token(access_token_);
diff --git a/chrome/browser/service_sandbox_type.h b/chrome/browser/service_sandbox_type.h
index 2192ca6..b9ac6749 100644
--- a/chrome/browser/service_sandbox_type.h
+++ b/chrome/browser/service_sandbox_type.h
@@ -9,8 +9,15 @@
 #include "build/chromeos_buildflags.h"
 #include "content/public/browser/service_process_host.h"
 #include "media/base/media_switches.h"
+#include "printing/buildflags/buildflags.h"
 #include "sandbox/policy/sandbox_type.h"
 
+#if (defined(OS_WIN) || defined(OS_MAC) || defined(OS_LINUX) || \
+     defined(OS_CHROMEOS)) &&                                   \
+    BUILDFLAG(ENABLE_PRINTING)
+#include "chrome/browser/printing/print_backend_service_manager.h"
+#endif
+
 // This file maps service classes to sandbox types.  Services which
 // require a non-utility sandbox can be added here.  See
 // ServiceProcessHost::Launch() for how these templates are consumed.
@@ -90,8 +97,22 @@
 }
 #endif  // !defined(OS_ANDROID)
 
+// mirroring::mojom::MirroringService
+#if defined(OS_MAC)
+namespace mirroring {
+namespace mojom {
+class MirroringService;
+}
+}  // namespace mirroring
+template <>
+inline sandbox::policy::SandboxType
+content::GetServiceSandboxType<mirroring::mojom::MirroringService>() {
+  return sandbox::policy::SandboxType::kMirroring;
+}
+#endif  // OS_MAC
+
 // printing::mojom::PrintingService
-#if defined(OS_WIN)
+#if defined(OS_WIN) && BUILDFLAG(ENABLE_PRINT_PREVIEW)
 namespace printing {
 namespace mojom {
 class PrintingService;
@@ -103,8 +124,12 @@
 content::GetServiceSandboxType<printing::mojom::PrintingService>() {
   return sandbox::policy::SandboxType::kPdfConversion;
 }
-#endif  // defined(OS_WIN)
+#endif  // defined(OS_WIN) && BUILDFLAG(ENABLE_PRINT_PREVIEW)
 
+// printing::mojom::PrintBackendService
+#if (defined(OS_WIN) || defined(OS_MAC) || defined(OS_LINUX) || \
+     defined(OS_CHROMEOS)) &&                                   \
+    BUILDFLAG(ENABLE_PRINTING)
 namespace printing {
 namespace mojom {
 class PrintBackendService;
@@ -114,8 +139,14 @@
 template <>
 inline sandbox::policy::SandboxType
 content::GetServiceSandboxType<printing::mojom::PrintBackendService>() {
-  return sandbox::policy::SandboxType::kPrintBackend;
+  return printing::PrintBackendServiceManager::GetInstance()
+                 .ShouldSandboxPrintBackendService()
+             ? sandbox::policy::SandboxType::kPrintBackend
+             : sandbox::policy::SandboxType::kNoSandbox;
 }
+#endif  // (defined(OS_WIN) || defined(OS_MAC) || defined(OS_LINUX) ||
+        //  defined(OS_CHROMEOS)) &&
+        // BUILDFLAG(ENABLE_PRINTING)
 
 // proxy_resolver::mojom::ProxyResolverFactory
 #if defined(OS_WIN)
diff --git a/chrome/browser/sessions/session_restore_interactive_uitest.cc b/chrome/browser/sessions/session_restore_interactive_uitest.cc
index f2e4dce..500c97e0 100644
--- a/chrome/browser/sessions/session_restore_interactive_uitest.cc
+++ b/chrome/browser/sessions/session_restore_interactive_uitest.cc
@@ -124,16 +124,9 @@
   Browser* restored = QuitBrowserAndRestore(browser(), 3);
   EXPECT_EQ(1, restored->tab_strip_model()->count());
 
-#if defined(OS_MAC)
-  // On macOS, minimized windows are neither active nor shown, to avoid causing
-  // space switches during session restore.
-  EXPECT_FALSE(restored->window()->IsActive());
-  EXPECT_FALSE(restored->window()->IsVisible());
-#else
   // Expect the window to be visible.
   // Prior to the fix for https://crbug.com/1018885, the window was active but
   // not visible.
   EXPECT_TRUE(restored->window()->IsActive());
   EXPECT_TRUE(restored->window()->IsVisible());
-#endif
 }
diff --git a/chrome/browser/speech/on_device_speech_recognizer_browsertest.cc b/chrome/browser/speech/on_device_speech_recognizer_browsertest.cc
index 845c052..99b690d 100644
--- a/chrome/browser/speech/on_device_speech_recognizer_browsertest.cc
+++ b/chrome/browser/speech/on_device_speech_recognizer_browsertest.cc
@@ -7,7 +7,6 @@
 #include <map>
 #include <memory>
 
-#include "chrome/browser/ash/accessibility/soda_installer_impl_chromeos.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/speech/cros_speech_recognition_service_factory.h"
 #include "chrome/browser/speech/fake_speech_recognition_service.h"
@@ -15,6 +14,7 @@
 #include "chrome/browser/ui/browser.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "components/soda/soda_installer.h"
+#include "components/soda/soda_installer_impl_chromeos.h"
 #include "content/public/test/browser_test.h"
 #include "media/audio/audio_system.h"
 #include "media/base/audio_parameters.h"
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index bf4821d..074584a 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -456,6 +456,7 @@
     "//components/language/core/browser",
     "//components/language/core/common",
     "//components/lens",
+    "//components/live_caption:constants",
     "//components/lookalikes/core",
     "//components/metrics_services_manager",
     "//components/navigation_metrics",
@@ -2455,6 +2456,8 @@
       "webui/chromeos/power_ui.h",
       "webui/chromeos/projector/projector_ui.cc",
       "webui/chromeos/projector/projector_ui.h",
+      "webui/chromeos/projector/selfie_cam_bubble_manager.cc",
+      "webui/chromeos/projector/selfie_cam_bubble_manager.h",
       "webui/chromeos/set_time_ui.cc",
       "webui/chromeos/set_time_ui.h",
       "webui/chromeos/slow_trace_ui.cc",
@@ -2690,7 +2693,6 @@
       "//chrome/browser/resources:internet_config_dialog_resources",
       "//chrome/browser/resources:internet_detail_dialog_resources",
       "//chrome/browser/resources/chromeos:multidevice_setup_resources",
-      "//chrome/browser/resources/chromeos/projector:resources",
       "//chrome/browser/ui/app_list/search/cros_action_history:cros_action_proto",
       "//chrome/browser/ui/app_list/search/search_result_ranker:app_launch_event_logger_proto",
       "//chrome/browser/ui/app_list/search/search_result_ranker:app_launch_predictor_proto",
@@ -4418,7 +4420,6 @@
       "//components/content_settings/browser/ui",
       "//components/fullscreen_control",
       "//components/live_caption",
-      "//components/live_caption:constants",
       "//components/media_message_center",
       "//components/page_info",
       "//components/payments/content",
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
index 991c723..8cbf8ab 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -1478,6 +1478,9 @@
       <message name="IDS_DOWNLOAD_LOCATION_DIALOG_TITLE" desc="Title for the dialog that asks where the user wants to save the download file before the download begins.">
         Choose where to download
       </message>
+      <message name="IDS_DOWNLOAD_LOCATION_DIALOG_TITLE_CONFIRM_DOWNLOAD" desc="Title for the dialog that informs the user to confirm the download.">
+        Confirm download
+      </message>
       <message name="IDS_DOWNLOAD_LOCATION_DIALOG_CHECKBOX" desc="Label for the checkbox that allows the user to indicate if they do not want the download location selection dialog to appear every time they initiate a download.">
         Don‘t show again
       </message>
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DOWNLOAD_LOCATION_DIALOG_TITLE_CONFIRM_DOWNLOAD.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DOWNLOAD_LOCATION_DIALOG_TITLE_CONFIRM_DOWNLOAD.png.sha1
new file mode 100644
index 0000000..7266b52
--- /dev/null
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DOWNLOAD_LOCATION_DIALOG_TITLE_CONFIRM_DOWNLOAD.png.sha1
@@ -0,0 +1 @@
+bb34c052c6ec63ae5659f657adea0dec4dbcf1b9
\ No newline at end of file
diff --git a/chrome/browser/ui/android/webid/BUILD.gn b/chrome/browser/ui/android/webid/BUILD.gn
index 8e520b5..7661bbff 100644
--- a/chrome/browser/ui/android/webid/BUILD.gn
+++ b/chrome/browser/ui/android/webid/BUILD.gn
@@ -7,6 +7,7 @@
 android_library("public_java") {
   deps = [
     "//base:base_java",
+    "//components/browser_ui/bottomsheet/android:java",
     "//third_party/androidx:androidx_annotation_annotation_java",
     "//ui/android:ui_java",
   ]
diff --git a/chrome/browser/ui/android/webid/DEPS b/chrome/browser/ui/android/webid/DEPS
new file mode 100644
index 0000000..1c417c3
--- /dev/null
+++ b/chrome/browser/ui/android/webid/DEPS
@@ -0,0 +1,4 @@
+include_rules = [
+  "+chrome/android",
+  "+components/browser_ui/bottomsheet/android",
+]
diff --git a/chrome/browser/ui/android/webid/README.md b/chrome/browser/ui/android/webid/README.md
index eaddf26..e4cb4036 100644
--- a/chrome/browser/ui/android/webid/README.md
+++ b/chrome/browser/ui/android/webid/README.md
@@ -15,8 +15,7 @@
 #### java/
 
 The root folder contains the public interface of this component and data that is
-used to fill it with content, e.g. Account. This folder also contains the
-factory to instantiate the component.
+used to fill it with content, e.g. Accounts.
 
 Add `chrome/browser/ui/android/webid/android:public_java` as dependency to use
 the interface and classes defined here.
@@ -27,5 +26,23 @@
 outside of this package. If you need access to any method, consider making it
 part of the public interface as defined in `AccountSelectionComponent`
 
-At the moment the implementation is a simple stub that selects the first
-account.
+This folder contains a separate [README](internal/README.md) that explains in
+detail how the architecture looks like and how to extend the component further.
+
+## Example usage
+
+``` java
+
+// Currently, you need access to internal/ to instantiate the component:
+AccountSelectionComponent component = new AccountSelectionCoordinator(/*...*/);
+
+component.initialize(activity, activity.getBottomSheetController(), () -> {
+  // Things to do when the component is dismissed.
+}));
+
+List<Account> accounts; // Add accounts to show!
+component.showAccounts("www.displayed-url.example", accounts, (account) -> {
+  // The |account| that was clicked should be used to fill something now.
+})
+
+```
diff --git a/chrome/browser/ui/android/webid/account_selection_view_android.cc b/chrome/browser/ui/android/webid/account_selection_view_android.cc
index 07653413..39e35a6 100644
--- a/chrome/browser/ui/android/webid/account_selection_view_android.cc
+++ b/chrome/browser/ui/android/webid/account_selection_view_android.cc
@@ -28,9 +28,10 @@
 
 namespace {
 
-std::vector<std::string> ConvertAccountToFields(const Account& account) {
+std::vector<std::string> ConvertAccountToFields(const Account& account,
+                                                const GURL& idp_url) {
   return {account.sub,        account.email,   account.name,
-          account.given_name, account.picture, ""};
+          account.given_name, account.picture, idp_url.spec()};
 }
 
 Account ConvertFieldsToAccount(JNIEnv* env,
@@ -59,7 +60,8 @@
   }
 }
 
-void AccountSelectionViewAndroid::Show(const GURL& url,
+void AccountSelectionViewAndroid::Show(const GURL& rp_url,
+                                       const GURL& idp_url,
                                        base::span<const Account> accounts) {
   if (!RecreateJavaObject()) {
     // It's possible that the constructor cannot access the bottom sheet clank
@@ -68,12 +70,12 @@
     delegate_->OnDismiss();
     return;
   }
+
   // Serialize the |accounts| span into a Java array and instruct the bridge
   // to show it together with |url| to the user.
-  // TODO(majidvp): Pass the serialized IdP GURL as part of the account to UI.
   std::vector<std::vector<std::string>> accounts_fields(accounts.size());
   for (size_t i = 0; i < accounts.size(); ++i) {
-    accounts_fields[i] = ConvertAccountToFields(accounts[i]);
+    accounts_fields[i] = ConvertAccountToFields(accounts[i], idp_url);
   }
 
   JNIEnv* env = AttachCurrentThread();
@@ -82,7 +84,7 @@
       base::android::ToJavaArrayOfStringArray(env, accounts_fields);
 
   Java_AccountSelectionBridge_showAccounts(
-      env, java_object_internal_, ConvertUTF8ToJavaString(env, url.spec()),
+      env, java_object_internal_, ConvertUTF8ToJavaString(env, rp_url.spec()),
       accounts_fields_obj);
 }
 
@@ -105,7 +107,7 @@
     Java_AccountSelectionBridge_destroy(AttachCurrentThread(),
                                         java_object_internal_);
   }
-  java_object_internal_ = Java_AccountSelectionBridge_Constructor(
+  java_object_internal_ = Java_AccountSelectionBridge_create(
       AttachCurrentThread(), reinterpret_cast<intptr_t>(this),
       delegate_->GetNativeView()->GetWindowAndroid()->GetJavaObject());
   return !!java_object_internal_;
diff --git a/chrome/browser/ui/android/webid/account_selection_view_android.h b/chrome/browser/ui/android/webid/account_selection_view_android.h
index 0880f9a..1c3fdc9 100644
--- a/chrome/browser/ui/android/webid/account_selection_view_android.h
+++ b/chrome/browser/ui/android/webid/account_selection_view_android.h
@@ -18,7 +18,9 @@
   ~AccountSelectionViewAndroid() override;
 
   // AccountSelectionView:
-  void Show(const GURL& url, base::span<const Account> accounts) override;
+  void Show(const GURL& rp_url,
+            const GURL& idp_url,
+            base::span<const Account> accounts) override;
 
   void OnAccountSelected(
       JNIEnv* env,
diff --git a/chrome/browser/ui/android/webid/internal/BUILD.gn b/chrome/browser/ui/android/webid/internal/BUILD.gn
index 8706a494..ccfa7f6 100644
--- a/chrome/browser/ui/android/webid/internal/BUILD.gn
+++ b/chrome/browser/ui/android/webid/internal/BUILD.gn
@@ -12,12 +12,23 @@
     "//base:base_java",
     "//base:jni_java",
     "//chrome/android:chrome_java",
+    "//chrome/browser/flags:java",
     "//chrome/browser/ui/android/webid:public_java",
+    "//chrome/browser/util:java",
+    "//components/browser_ui/bottomsheet/android:java",
+    "//components/browser_ui/widget/android:java",
+    "//components/embedder_support/android:util_java",
     "//third_party/androidx:androidx_annotation_annotation_java",
     "//ui/android:ui_java",
+    "//url:gurl_java",
   ]
 
-  sources = [ "java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionBridge.java" ]
+  sources = [
+    "java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionBridge.java",
+    "java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionCoordinator.java",
+    "java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionMediator.java",
+    "java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionProperties.java",
+  ]
 
   resources_package = "org.chromium.chrome.browser.ui.android.webid"
   annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
diff --git a/chrome/browser/ui/android/webid/internal/README.md b/chrome/browser/ui/android/webid/internal/README.md
new file mode 100644
index 0000000..ed3ea2c
--- /dev/null
+++ b/chrome/browser/ui/android/webid/internal/README.md
@@ -0,0 +1,55 @@
+# Account Selection Android Feature
+
+This folder contains the internal parts of the Account Selection component. Files,
+classes and methods defined in here are not meant to be used outside of this
+package.
+
+This document provides a brief overview of the architecture.
+
+[TOC]
+
+
+## Component structure
+
+This component follows the typical
+[MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)
+structure that is widely used in Chrome on Android. The MVC structures separates
+logic from representation:
+
+ * The [controller](#Controller) creates and connects all subcomponents and
+   performs logic that affects the visual appearance of the component.
+ * The [model](#Model) keeps all state that affects the visual appearance of the
+   component. Changes made to it are automatically propagated to the view by a
+   Model-Change-Processor (MCP) as defined in
+    `//src/ui/android/java/src/org/chromium/ui/modelutil/`
+ * The [view](#View) is the representation of the component. It enforces styles
+   and is mostly called by a view binder to set mutable properties.
+
+## Model
+
+The model holds state and event listeners connected to the view. An MCP
+automatically notifies listener about any change made to a property. To automate
+this Observer structure, the model is a `PropertyModel` as defined in
+`//src/ui/android/java/src/org/chromium/ui/modelutil/`. It is built by defining
+readable and writable properties and constructing a model with them. The
+properties (and a simple factory method for the model) are located in the static
+`AccountSelectionProperties` class.
+
+The details of the model will be added in follow up patches.
+
+## Controller
+
+The controller of this model implements the AccountSelectionComponent interface as
+defined in `public/` and contains all logic. The controller consists of two parts:
+
+  * **AccountSelectionCoordinator** which implements the public interface and creates all
+    parts of the component (model, mediator, view, etc.) and links them using
+    MCPs.
+  * **AccountSelectionMediator** which handles request to the component API and changes
+    the model accordingly. Interactions with the view are typically handled here
+    and either affect the model or notify callers of the component API.
+
+
+## View
+
+This will be added in follow-up patches.
diff --git a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionBridge.java b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionBridge.java
index f87f9ec..5e50de73 100644
--- a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionBridge.java
+++ b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionBridge.java
@@ -4,21 +4,40 @@
 
 package org.chromium.chrome.browser.ui.android.webid;
 
+import androidx.annotation.Nullable;
+
 import org.chromium.base.annotations.CalledByNative;
 import org.chromium.base.annotations.NativeMethods;
 import org.chromium.chrome.browser.ui.android.webid.data.Account;
+import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
+import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerProvider;
 import org.chromium.ui.base.WindowAndroid;
 
+import java.util.Arrays;
+
 /**
  * This bridge creates and initializes a {@link AccountSelectionComponent} on construction and
  * forwards native calls to it.
  */
 class AccountSelectionBridge implements AccountSelectionComponent.Delegate {
     private long mNativeView;
+    private final AccountSelectionComponent mAccountSelectionComponent;
+
+    private AccountSelectionBridge(long nativeView, WindowAndroid windowAndroid,
+            BottomSheetController bottomSheetController) {
+        mNativeView = nativeView;
+        mAccountSelectionComponent = new AccountSelectionCoordinator();
+        mAccountSelectionComponent.initialize(
+                windowAndroid.getContext().get(), bottomSheetController, this);
+    }
 
     @CalledByNative
-    private AccountSelectionBridge(long nativeView, WindowAndroid windowAndroid) {
-        mNativeView = nativeView;
+    private static @Nullable AccountSelectionBridge create(
+            long nativeView, WindowAndroid windowAndroid) {
+        BottomSheetController bottomSheetController =
+                BottomSheetControllerProvider.from(windowAndroid);
+        if (bottomSheetController == null) return null;
+        return new AccountSelectionBridge(nativeView, windowAndroid, bottomSheetController);
     }
 
     @CalledByNative
@@ -36,6 +55,7 @@
      */
     @CalledByNative
     private void showAccounts(String url, String[][] accountsFields) {
+        assert accountsFields != null && accountsFields.length > 0;
         Account[] accounts = new Account[accountsFields.length];
         for (int i = 0; i < accountsFields.length; i++) {
             String[] fields = accountsFields[i];
@@ -48,8 +68,7 @@
                     /* originUrl= */ fields[5]);
         }
 
-        // TODO(majidvp): Actually show UI that lists the account.
-        onAccountSelected(accounts[0]);
+        mAccountSelectionComponent.showAccounts(url, Arrays.asList(accounts));
     }
 
     @Override
diff --git a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionCoordinator.java b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionCoordinator.java
new file mode 100644
index 0000000..2c45ca0
--- /dev/null
+++ b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionCoordinator.java
@@ -0,0 +1,33 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.ui.android.webid;
+
+import android.content.Context;
+
+import org.chromium.chrome.browser.ui.android.webid.data.Account;
+import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
+import org.chromium.ui.modelutil.PropertyModel;
+
+import java.util.List;
+
+/**
+ * Creates the AccountSelection component.
+ */
+public class AccountSelectionCoordinator implements AccountSelectionComponent {
+    private final AccountSelectionMediator mMediator = new AccountSelectionMediator();
+    private final PropertyModel mModel = AccountSelectionProperties.createDefaultModel();
+
+    @Override
+    public void initialize(Context context, BottomSheetController sheetController,
+            AccountSelectionComponent.Delegate delegate) {
+        mMediator.initialize(delegate, mModel);
+        // TODO(majidvp): Create the view and setup model change processor.
+    }
+
+    @Override
+    public void showAccounts(String url, List<Account> accounts) {
+        mMediator.showAccounts(url, accounts);
+    }
+}
diff --git a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionMediator.java b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionMediator.java
new file mode 100644
index 0000000..3c9e51d2
--- /dev/null
+++ b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionMediator.java
@@ -0,0 +1,27 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.ui.android.webid;
+
+import org.chromium.chrome.browser.ui.android.webid.data.Account;
+import org.chromium.ui.modelutil.PropertyModel;
+
+import java.util.List;
+
+/**
+ * Contains the logic for the AccountSelection component. It sets the state of the model and reacts
+ * to events like clicks.
+ */
+class AccountSelectionMediator {
+    private AccountSelectionComponent.Delegate mDelegate;
+    private PropertyModel mModel;
+
+    void initialize(AccountSelectionComponent.Delegate delegate, PropertyModel model) {
+        assert delegate != null;
+        mDelegate = delegate;
+        mModel = model;
+    }
+
+    void showAccounts(String url, List<Account> accounts) {}
+}
diff --git a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionProperties.java b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionProperties.java
new file mode 100644
index 0000000..46fd688
--- /dev/null
+++ b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionProperties.java
@@ -0,0 +1,18 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.ui.android.webid;
+
+import org.chromium.ui.modelutil.PropertyModel;
+
+/**
+ * Properties defined here reflect the state of the AccountSelection-components.
+ */
+class AccountSelectionProperties {
+    static PropertyModel createDefaultModel() {
+        return new PropertyModel();
+    }
+
+    private AccountSelectionProperties() {}
+}
diff --git a/chrome/browser/ui/android/webid/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionComponent.java b/chrome/browser/ui/android/webid/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionComponent.java
index f176f1c..6c213fe 100644
--- a/chrome/browser/ui/android/webid/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionComponent.java
+++ b/chrome/browser/ui/android/webid/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionComponent.java
@@ -7,6 +7,7 @@
 import android.content.Context;
 
 import org.chromium.chrome.browser.ui.android.webid.data.Account;
+import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
 
 import java.util.List;
 
@@ -36,9 +37,10 @@
     /**
      * Initializes the component.
      * @param context A {@link Context} to create views and retrieve resources.
+     * @param sheetController A {@link BottomSheetController} used to show/hide the sheet.
      * @param delegate A {@link Delegate} that handles dismiss events.
      */
-    void initialize(Context context, Delegate delegate);
+    void initialize(Context context, BottomSheetController sheetController, Delegate delegate);
 
     /**
      * Displays the given accounts in a new bottom sheet.
diff --git a/chrome/browser/ui/app_list/app_list_test_util.cc b/chrome/browser/ui/app_list/app_list_test_util.cc
index 37dc0da3..6007366 100644
--- a/chrome/browser/ui/app_list/app_list_test_util.cc
+++ b/chrome/browser/ui/app_list/app_list_test_util.cc
@@ -9,7 +9,6 @@
 #include "chrome/browser/extensions/extension_service.h"
 #include "chrome/browser/web_applications/externally_managed_app_manager_impl.h"
 #include "chrome/browser/web_applications/system_web_apps/test/test_system_web_app_manager.h"
-#include "chrome/browser/web_applications/test/test_os_integration_manager.h"
 #include "chrome/browser/web_applications/test/test_web_app_provider.h"
 #include "chrome/browser/web_applications/test/test_web_app_url_loader.h"
 #include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
@@ -64,17 +63,8 @@
       std::make_unique<web_app::ExternallyManagedAppManagerImpl>(profile);
   externally_managed_app_manager->SetUrlLoaderForTesting(std::move(url_loader));
 
-  auto os_integration_manager =
-      std::make_unique<web_app::TestOsIntegrationManager>(
-          profile, /*app_shortcut_manager=*/nullptr,
-          /*file_handler_manager=*/nullptr,
-          /*protocol_handler_manager=*/nullptr,
-          /*url_handler_manager*/ nullptr);
-  os_integration_manager_ = os_integration_manager.get();
-
-  auto* provider = web_app::TestWebAppProvider::Get(profile);
+  auto* const provider = web_app::TestWebAppProvider::Get(profile);
   provider->SetExternallyManagedAppManager(
       std::move(externally_managed_app_manager));
-  provider->SetOsIntegrationManager(std::move(os_integration_manager));
   web_app::test::AwaitStartWebAppProviderAndSubsystems(profile);
 }
diff --git a/chrome/browser/ui/app_list/app_list_test_util.h b/chrome/browser/ui/app_list/app_list_test_util.h
index e989f55e..5f31548 100644
--- a/chrome/browser/ui/app_list/app_list_test_util.h
+++ b/chrome/browser/ui/app_list/app_list_test_util.h
@@ -8,7 +8,6 @@
 #include "chrome/browser/extensions/extension_service_test_base.h"
 
 namespace web_app {
-class TestOsIntegrationManager;
 class TestWebAppUrlLoader;
 }  // namespace web_app
 
@@ -24,15 +23,11 @@
 
   void SetUp() override;
 
-  web_app::TestOsIntegrationManager& os_integration_manager() {
-    return *os_integration_manager_;
-  }
   web_app::TestWebAppUrlLoader& url_loader() { return *url_loader_; }
 
  private:
   void ConfigureWebAppProvider();
 
-  web_app::TestOsIntegrationManager* os_integration_manager_ = nullptr;
   web_app::TestWebAppUrlLoader* url_loader_ = nullptr;
 };
 
diff --git a/chrome/browser/ui/app_list/app_service/app_service_app_model_builder_unittest.cc b/chrome/browser/ui/app_list/app_service/app_service_app_model_builder_unittest.cc
index 2f89567..af110ab 100644
--- a/chrome/browser/ui/app_list/app_service/app_service_app_model_builder_unittest.cc
+++ b/chrome/browser/ui/app_list/app_service/app_service_app_model_builder_unittest.cc
@@ -42,9 +42,7 @@
 #include "chrome/browser/ui/app_list/test/fake_app_list_model_updater.h"
 #include "chrome/browser/ui/app_list/test/test_app_list_controller_delegate.h"
 #include "chrome/browser/web_applications/components/app_icon_manager.h"
-#include "chrome/browser/web_applications/components/web_app_helpers.h"
 #include "chrome/browser/web_applications/components/web_application_info.h"
-#include "chrome/browser/web_applications/test/test_os_integration_manager.h"
 #include "chrome/browser/web_applications/test/test_web_app_provider.h"
 #include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
 #include "chrome/browser/web_applications/web_app_provider.h"
@@ -329,9 +327,6 @@
   std::string CreateWebApp(const std::string& app_name) {
     const GURL kAppUrl("https://example.com/");
 
-    os_integration_manager().SetNextCreateShortcutsResult(
-        web_app::GenerateAppIdFromURL(kAppUrl), true);
-
     auto web_app_info = std::make_unique<WebApplicationInfo>();
     web_app_info->title = base::UTF8ToUTF16(app_name);
     web_app_info->start_url = kAppUrl;
diff --git a/chrome/browser/ui/ash/projector/projector_client_impl.cc b/chrome/browser/ui/ash/projector/projector_client_impl.cc
index 6299182..946f5507 100644
--- a/chrome/browser/ui/ash/projector/projector_client_impl.cc
+++ b/chrome/browser/ui/ash/projector/projector_client_impl.cc
@@ -57,6 +57,18 @@
   recognizer_status_ = SPEECH_RECOGNIZER_OFF;
 }
 
+void ProjectorClientImpl::ShowSelfieCam() {
+  selfie_cam_bubble_manager_.Show(ProfileManager::GetPrimaryUserProfile());
+}
+
+void ProjectorClientImpl::CloseSelfieCam() {
+  selfie_cam_bubble_manager_.Close();
+}
+
+bool ProjectorClientImpl::IsSelfieCamVisible() const {
+  return selfie_cam_bubble_manager_.IsVisible();
+}
+
 void ProjectorClientImpl::OnSpeechResult(
     const std::u16string& text,
     bool is_final,
diff --git a/chrome/browser/ui/ash/projector/projector_client_impl.h b/chrome/browser/ui/ash/projector/projector_client_impl.h
index aa92c8e..0267575 100644
--- a/chrome/browser/ui/ash/projector/projector_client_impl.h
+++ b/chrome/browser/ui/ash/projector/projector_client_impl.h
@@ -11,6 +11,7 @@
 #include "base/memory/weak_ptr.h"
 #include "base/scoped_observation.h"
 #include "chrome/browser/speech/speech_recognizer_delegate.h"
+#include "chrome/browser/ui/webui/chromeos/projector/selfie_cam_bubble_manager.h"
 #include "components/soda/constants.h"
 #include "components/soda/soda_installer.h"
 
@@ -29,8 +30,10 @@
 
   // ash::ProjectorClient:
   void StartSpeechRecognition() override;
-
   void StopSpeechRecognition() override;
+  void ShowSelfieCam() override;
+  void CloseSelfieCam() override;
+  bool IsSelfieCamVisible() const override;
 
   // SpeechRecognizerDelegate:
   void OnSpeechResult(
@@ -62,6 +65,7 @@
                           speech::SodaInstaller::Observer>
       observed_soda_installer_{this};
   std::unique_ptr<OnDeviceSpeechRecognizer> speech_recognizer_;
+  chromeos::SelfieCamBubbleManager selfie_cam_bubble_manager_;
   base::WeakPtrFactory<ProjectorClientImpl> weak_ptr_factory_{this};
 };
 
diff --git a/chrome/browser/ui/ash/projector/projector_client_impl_browsertest.cc b/chrome/browser/ui/ash/projector/projector_client_impl_browsertest.cc
new file mode 100644
index 0000000..fc9fcf08
--- /dev/null
+++ b/chrome/browser/ui/ash/projector/projector_client_impl_browsertest.cc
@@ -0,0 +1,51 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/ash/projector/projector_client_impl.h"
+
+#include <memory>
+
+#include "ash/constants/ash_features.h"
+#include "ash/public/cpp/projector/projector_client.h"
+#include "base/test/scoped_feature_list.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "content/public/test/browser_test.h"
+
+namespace ash {
+
+class ProjectorClientTest : public InProcessBrowserTest {
+ public:
+  ProjectorClientTest() {
+    scoped_feature_list_.InitAndEnableFeature(features::kProjector);
+  }
+
+  ~ProjectorClientTest() override = default;
+  ProjectorClientTest(const ProjectorClientTest&) = delete;
+  ProjectorClientTest& operator=(const ProjectorClientTest&) = delete;
+
+  // InProcessBrowserTest:
+  void SetUpOnMainThread() override {
+    InProcessBrowserTest::SetUpOnMainThread();
+    client_ = std::make_unique<ProjectorClientImpl>();
+  }
+
+ protected:
+  std::unique_ptr<ProjectorClient> client_;
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(ProjectorClientTest, ShowOrCloseSelfieCamTest) {
+  EXPECT_FALSE(client_->IsSelfieCamVisible());
+  client_->ShowSelfieCam();
+  EXPECT_TRUE(client_->IsSelfieCamVisible());
+  client_->CloseSelfieCam();
+  EXPECT_FALSE(client_->IsSelfieCamVisible());
+}
+
+// TODO(crbug/1199396): Add a test to verify the selfie cam turns off when the
+// device goes inactive.
+
+}  // namespace ash
diff --git a/chrome/browser/ui/ash/wallpaper_controller_client_impl.cc b/chrome/browser/ui/ash/wallpaper_controller_client_impl.cc
index e2c85c9..631a1902 100644
--- a/chrome/browser/ui/ash/wallpaper_controller_client_impl.cc
+++ b/chrome/browser/ui/ash/wallpaper_controller_client_impl.cc
@@ -578,11 +578,10 @@
 
   extensions::EventRouter* event_router = extensions::EventRouter::Get(profile);
 
-  auto event_args = std::make_unique<base::ListValue>();
   auto event = std::make_unique<extensions::Event>(
       extensions::events::WALLPAPER_PRIVATE_ON_CLOSE_PREVIEW_WALLPAPER,
       extensions::api::wallpaper_private::OnClosePreviewWallpaper::kEventName,
-      std::move(event_args));
+      std::vector<base::Value>());
   event_router->DispatchEventToExtension(kWallpaperManagerId, std::move(event));
 }
 
diff --git a/chrome/browser/ui/autofill/edit_address_profile_dialog_controller_impl.cc b/chrome/browser/ui/autofill/edit_address_profile_dialog_controller_impl.cc
index 393bdf80f..ce0d8e6 100644
--- a/chrome/browser/ui/autofill/edit_address_profile_dialog_controller_impl.cc
+++ b/chrome/browser/ui/autofill/edit_address_profile_dialog_controller_impl.cc
@@ -10,6 +10,8 @@
 #include "chrome/browser/ui/browser_window.h"
 #include "components/autofill/core/browser/autofill_client.h"
 #include "components/autofill/core/common/autofill_features.h"
+#include "components/strings/grit/components_strings.h"
+#include "ui/base/l10n/l10n_util.h"
 
 namespace autofill {
 
@@ -39,16 +41,14 @@
 }
 
 std::u16string EditAddressProfileDialogControllerImpl::GetWindowTitle() const {
-  // TODO(crbug.com/1167060): Use internationalized string upon having final
-  // strings.
-  return is_update_ ? u"Update Address?" : u"Save Address?";
+  return l10n_util::GetStringUTF16(IDS_AUTOFILL_EDIT_ADDRESS_DIALOG_TITLE);
 }
 
 std::u16string EditAddressProfileDialogControllerImpl::GetOkButtonLabel()
     const {
-  // TODO(crbug.com/1167060): Use internationalized string upon having final
-  // strings.
-  return is_update_ ? u"Update" : u"Save";
+  return l10n_util::GetStringUTF16(
+      is_update_ ? IDS_AUTOFILL_EDIT_ADDRESS_DIALOG_OK_BUTTON_LABEL_UPDATE
+                 : IDS_AUTOFILL_EDIT_ADDRESS_DIALOG_OK_BUTTON_LABEL_SAVE);
 }
 
 const AutofillProfile&
diff --git a/chrome/browser/ui/autofill/save_address_profile_icon_controller.h b/chrome/browser/ui/autofill/save_address_profile_icon_controller.h
index 6b58abc..4cef9aa 100644
--- a/chrome/browser/ui/autofill/save_address_profile_icon_controller.h
+++ b/chrome/browser/ui/autofill/save_address_profile_icon_controller.h
@@ -26,6 +26,8 @@
 
   virtual bool IsBubbleActive() const = 0;
 
+  virtual std::u16string GetPageActionIconTootip() const = 0;
+
   virtual AutofillBubbleBase* GetSaveBubbleView() const = 0;
 };
 
diff --git a/chrome/browser/ui/autofill/save_update_address_profile_bubble_controller_impl.cc b/chrome/browser/ui/autofill/save_update_address_profile_bubble_controller_impl.cc
index 782cbab..4066a17d 100644
--- a/chrome/browser/ui/autofill/save_update_address_profile_bubble_controller_impl.cc
+++ b/chrome/browser/ui/autofill/save_update_address_profile_bubble_controller_impl.cc
@@ -12,6 +12,8 @@
 #include "chrome/browser/ui/browser_finder.h"
 #include "chrome/browser/ui/browser_window.h"
 #include "components/autofill/core/common/autofill_features.h"
+#include "components/strings/grit/components_strings.h"
+#include "ui/base/l10n/l10n_util.h"
 
 namespace autofill {
 
@@ -46,11 +48,9 @@
 
 std::u16string SaveUpdateAddressProfileBubbleControllerImpl::GetWindowTitle()
     const {
-  // TODO(crbug.com/1167060): Use internationalized string upon having final
-  // strings.
-  // TODO(crbug.com/1167060): Update prompt title should reflect the fields that
-  // are being updated.
-  return original_profile_ ? u"Update Address?" : u"Save Address?";
+  return l10n_util::GetStringUTF16(
+      original_profile_ ? IDS_AUTOFILL_UPDATE_ADDRESS_PROMPT_TITLE
+                        : IDS_AUTOFILL_SAVE_ADDRESS_PROMPT_TITLE);
 }
 
 const AutofillProfile&
@@ -72,13 +72,13 @@
 }
 
 void SaveUpdateAddressProfileBubbleControllerImpl::OnEditButtonClicked() {
-  HideBubble();
   EditAddressProfileDialogControllerImpl::CreateForWebContents(web_contents());
   EditAddressProfileDialogControllerImpl* controller =
       EditAddressProfileDialogControllerImpl::FromWebContents(web_contents());
   controller->OfferEdit(address_profile_,
                         /*is_update=*/original_profile_.has_value(),
                         std::move(address_profile_save_prompt_callback_));
+  HideBubble();
 }
 
 void SaveUpdateAddressProfileBubbleControllerImpl::OnBubbleClosed() {
@@ -98,6 +98,11 @@
   return !address_profile_save_prompt_callback_.is_null();
 }
 
+std::u16string
+SaveUpdateAddressProfileBubbleControllerImpl::GetPageActionIconTootip() const {
+  return GetWindowTitle();
+}
+
 AutofillBubbleBase*
 SaveUpdateAddressProfileBubbleControllerImpl::GetSaveBubbleView() const {
   return bubble_view();
diff --git a/chrome/browser/ui/autofill/save_update_address_profile_bubble_controller_impl.h b/chrome/browser/ui/autofill/save_update_address_profile_bubble_controller_impl.h
index 5e8693a..ef85394e 100644
--- a/chrome/browser/ui/autofill/save_update_address_profile_bubble_controller_impl.h
+++ b/chrome/browser/ui/autofill/save_update_address_profile_bubble_controller_impl.h
@@ -55,6 +55,7 @@
   // SaveAddressProfileIconController:
   void OnPageActionIconClicked() override;
   bool IsBubbleActive() const override;
+  std::u16string GetPageActionIconTootip() const override;
   AutofillBubbleBase* GetSaveBubbleView() const override;
 
  protected:
diff --git a/chrome/browser/ui/browser.h b/chrome/browser/ui/browser.h
index 71ce1e2..2481cee 100644
--- a/chrome/browser/ui/browser.h
+++ b/chrome/browser/ui/browser.h
@@ -200,6 +200,9 @@
     kErrorLoadingKiosk,
   };
 
+  // The default value for a browser's `restore_id` param.
+  static constexpr int kDefaultRestoreId = 0;
+
   // Callback that receives the result of a user being warned about closing a
   // browser window (for example, if closing the window would interrupt a
   // download). The parameter is whether the close should proceed.
@@ -250,7 +253,7 @@
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
     // The id from the restore data to restore the browser window.
-    int32_t restore_id = 0;
+    int32_t restore_id = kDefaultRestoreId;
 #endif
 
     bool is_focus_mode = false;
diff --git a/chrome/browser/ui/caption_bubble_controller.h b/chrome/browser/ui/caption_bubble_controller.h
index 183ffcc..645e754 100644
--- a/chrome/browser/ui/caption_bubble_controller.h
+++ b/chrome/browser/ui/caption_bubble_controller.h
@@ -13,7 +13,7 @@
 
 namespace captions {
 
-class CaptionHostImpl;
+class LiveCaptionSpeechRecognitionHost;
 
 ///////////////////////////////////////////////////////////////////////////////
 // Caption Bubble Controller
@@ -37,14 +37,16 @@
   // the transcription result was set on the caption bubble successfully.
   // Transcriptions will halt if this returns false.
   virtual bool OnTranscription(
-      CaptionHostImpl* caption_host_impl,
+      LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host,
       const media::mojom::SpeechRecognitionResultPtr& result) = 0;
 
   // Called when the speech service has an error.
-  virtual void OnError(CaptionHostImpl* caption_host_impl) = 0;
+  virtual void OnError(LiveCaptionSpeechRecognitionHost*
+                           live_caption_speech_recognition_host) = 0;
 
   // Called when the audio stream has ended.
-  virtual void OnAudioStreamEnd(CaptionHostImpl* caption_host_impl) = 0;
+  virtual void OnAudioStreamEnd(LiveCaptionSpeechRecognitionHost*
+                                    live_caption_speech_recognition_host) = 0;
 
   // Called when the caption style changes.
   virtual void UpdateCaptionStyle(
diff --git a/chrome/browser/ui/page_info/page_info_unittest.cc b/chrome/browser/ui/page_info/page_info_unittest.cc
index 1485a7c..f3fba4c 100644
--- a/chrome/browser/ui/page_info/page_info_unittest.cc
+++ b/chrome/browser/ui/page_info/page_info_unittest.cc
@@ -631,16 +631,11 @@
 
 // Define some dummy constants for Android-only resources.
 #if !defined(OS_ANDROID)
-#define IDR_PAGEINFO_WARNING_MINOR 0
 #define IDR_PAGEINFO_BAD 0
 #define IDR_PAGEINFO_GOOD 0
 #endif
 
 TEST_F(PageInfoTest, InsecureContent) {
-#if defined(OS_ANDROID)
-  base::test::ScopedFeatureList scoped_feature_list;
-  scoped_feature_list.InitAndDisableFeature(page_info::kPageInfoV2);
-#endif
   struct TestCase {
     security_state::SecurityLevel security_level;
     net::CertStatus cert_status;
@@ -661,7 +656,7 @@
        false /* ran_content_with_cert_errors */,
        false /* displayed_content_with_cert_errors */,
        PageInfo::SITE_CONNECTION_STATUS_INSECURE_PASSIVE_SUBRESOURCE,
-       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_WARNING_MINOR},
+       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_BAD},
       // Passive mixed content with a nonsecure form. The nonsecure form is the
       // more severe problem.
       {security_state::NONE, 0, false /* ran_mixed_content */,
@@ -669,21 +664,21 @@
        false /* ran_content_with_cert_errors */,
        false /* displayed_content_with_cert_errors */,
        PageInfo::SITE_CONNECTION_STATUS_INSECURE_FORM_ACTION,
-       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_WARNING_MINOR},
+       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_BAD},
       // Only nonsecure form.
       {security_state::NONE, 0, false /* ran_mixed_content */,
        false /* displayed_mixed_content */, true,
        false /* ran_content_with_cert_errors */,
        false /* displayed_content_with_cert_errors */,
        PageInfo::SITE_CONNECTION_STATUS_INSECURE_FORM_ACTION,
-       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_WARNING_MINOR},
+       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_BAD},
       // Passive mixed content with a cert error on the main resource.
       {security_state::DANGEROUS, net::CERT_STATUS_DATE_INVALID,
        false /* ran_mixed_content */, true /* displayed_mixed_content */, false,
        false /* ran_content_with_cert_errors */,
        false /* displayed_content_with_cert_errors */,
        PageInfo::SITE_CONNECTION_STATUS_INSECURE_PASSIVE_SUBRESOURCE,
-       PageInfo::SITE_IDENTITY_STATUS_ERROR, IDR_PAGEINFO_WARNING_MINOR},
+       PageInfo::SITE_IDENTITY_STATUS_ERROR, IDR_PAGEINFO_BAD},
       // Active and passive mixed content.
       {security_state::DANGEROUS, 0, true /* ran_mixed_content */,
        true /* displayed_mixed_content */, false,
@@ -727,7 +722,7 @@
        false /* ran_content_with_cert_errors */,
        true /* displayed_content_with_cert_errors */,
        PageInfo::SITE_CONNECTION_STATUS_INSECURE_PASSIVE_SUBRESOURCE,
-       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_WARNING_MINOR},
+       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_BAD},
       // Passive subresources with cert errors, with a cert error on the
       // main resource also. In this case, the subresources with
       // certificate errors are ignored: if the main resource had a cert
@@ -774,13 +769,7 @@
        true /* displayed_content_with_cert_errors */, false,
        false /* ran_mixed_content */, true /* displayed_mixed_content */,
        PageInfo::SITE_CONNECTION_STATUS_INSECURE_PASSIVE_SUBRESOURCE,
-       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_WARNING_MINOR},
-      // Passive mixed content and subresources with cert errors.
-      {security_state::NONE, 0, false /* ran_content_with_cert_errors */,
-       true /* displayed_content_with_cert_errors */, false,
-       false /* ran_mixed_content */, true /* displayed_mixed_content */,
-       PageInfo::SITE_CONNECTION_STATUS_INSECURE_PASSIVE_SUBRESOURCE,
-       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_WARNING_MINOR},
+       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_BAD},
       // Passive mixed content, a nonsecure form, and subresources with cert
       // errors.
       {security_state::NONE, 0, false /* ran_mixed_content */,
@@ -788,7 +777,7 @@
        false /* ran_content_with_cert_errors */,
        true /* displayed_content_with_cert_errors */,
        PageInfo::SITE_CONNECTION_STATUS_INSECURE_FORM_ACTION,
-       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_WARNING_MINOR},
+       PageInfo::SITE_IDENTITY_STATUS_CERT, IDR_PAGEINFO_BAD},
       // Passive mixed content and active subresources with cert errors.
       {security_state::DANGEROUS, 0, false /* ran_mixed_content */,
        true /* displayed_mixed_content */, false,
@@ -810,7 +799,7 @@
        true /* ran_content_with_cert_errors */,
        false /* displayed_content_with_cert_errors */,
        PageInfo::SITE_CONNECTION_STATUS_INSECURE_PASSIVE_SUBRESOURCE,
-       PageInfo::SITE_IDENTITY_STATUS_ERROR, IDR_PAGEINFO_WARNING_MINOR},
+       PageInfo::SITE_IDENTITY_STATUS_ERROR, IDR_PAGEINFO_BAD},
   };
 
   for (const auto& test : kTestCases) {
@@ -934,7 +923,7 @@
   EXPECT_EQ(PageInfo::SITE_IDENTITY_STATUS_DEPRECATED_SIGNATURE_ALGORITHM,
             page_info()->site_identity_status());
 #if defined(OS_ANDROID)
-  EXPECT_EQ(IDR_PAGEINFO_BAD_V2,
+  EXPECT_EQ(IDR_PAGEINFO_BAD,
             PageInfoUI::GetIdentityIconID(page_info()->site_identity_status()));
 #endif
 }
diff --git a/chrome/browser/ui/prefs/pref_watcher.cc b/chrome/browser/ui/prefs/pref_watcher.cc
index b633d60..bd42e9fe 100644
--- a/chrome/browser/ui/prefs/pref_watcher.cc
+++ b/chrome/browser/ui/prefs/pref_watcher.cc
@@ -15,6 +15,7 @@
 #include "chrome/common/pref_names.h"
 #include "components/keyed_service/content/browser_context_dependency_manager.h"
 #include "components/language/core/browser/pref_names.h"
+#include "components/live_caption/pref_names.h"
 #include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h"
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
diff --git a/chrome/browser/ui/views/accessibility/caption_bubble_controller_views.cc b/chrome/browser/ui/views/accessibility/caption_bubble_controller_views.cc
index acfcad9..5dabb0a 100644
--- a/chrome/browser/ui/views/accessibility/caption_bubble_controller_views.cc
+++ b/chrome/browser/ui/views/accessibility/caption_bubble_controller_views.cc
@@ -10,7 +10,7 @@
 #include "base/bind.h"
 #include "chrome/browser/accessibility/caption_controller.h"
 #include "chrome/browser/accessibility/caption_controller_factory.h"
-#include "chrome/browser/accessibility/caption_host_impl.h"
+#include "chrome/browser/accessibility/live_caption_speech_recognition_host.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_finder.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
@@ -46,11 +46,11 @@
 }
 
 bool CaptionBubbleControllerViews::OnTranscription(
-    CaptionHostImpl* caption_host_impl,
+    LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host,
     const media::mojom::SpeechRecognitionResultPtr& result) {
   if (!caption_bubble_)
     return false;
-  SetActiveModel(caption_host_impl);
+  SetActiveModel(live_caption_speech_recognition_host);
   if (active_model_->IsClosed())
     return false;
 
@@ -69,27 +69,28 @@
   return true;
 }
 
-void CaptionBubbleControllerViews::OnError(CaptionHostImpl* caption_host_impl) {
+void CaptionBubbleControllerViews::OnError(
+    LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host) {
   if (!caption_bubble_)
     return;
-  SetActiveModel(caption_host_impl);
+  SetActiveModel(live_caption_speech_recognition_host);
   if (active_model_->IsClosed())
     return;
   active_model_->OnError();
 }
 
 void CaptionBubbleControllerViews::OnAudioStreamEnd(
-    CaptionHostImpl* caption_host_impl) {
+    LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host) {
   if (!caption_bubble_)
     return;
 
   CaptionBubbleModel* caption_bubble_model =
-      caption_bubble_models_[caption_host_impl].get();
+      caption_bubble_models_[live_caption_speech_recognition_host].get();
   if (active_model_ == caption_bubble_model) {
     active_model_ = nullptr;
     caption_bubble_->SetModel(nullptr);
   }
-  caption_bubble_models_.erase(caption_host_impl);
+  caption_bubble_models_.erase(live_caption_speech_recognition_host);
 }
 
 void CaptionBubbleControllerViews::UpdateCaptionStyle(
@@ -98,9 +99,10 @@
 }
 
 void CaptionBubbleControllerViews::SetActiveModel(
-    CaptionHostImpl* caption_host_impl) {
-  if (!caption_bubble_models_.count(caption_host_impl)) {
-    content::WebContents* web_contents = caption_host_impl->GetWebContents();
+    LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host) {
+  if (!caption_bubble_models_.count(live_caption_speech_recognition_host)) {
+    content::WebContents* web_contents =
+        live_caption_speech_recognition_host->GetWebContents();
     views::Widget* context_widget =
         web_contents ? views::Widget::GetTopLevelWidgetForNativeView(
                            web_contents->GetNativeView())
@@ -109,7 +111,7 @@
     if (context_widget)
       context_bounds = context_widget->GetClientAreaBoundsInScreen();
     caption_bubble_models_.emplace(
-        caption_host_impl,
+        live_caption_speech_recognition_host,
         std::make_unique<CaptionBubbleModel>(
             context_bounds,
             base::BindRepeating(&CaptionBubbleControllerViews::ActivateContext,
@@ -117,7 +119,7 @@
   }
 
   CaptionBubbleModel* caption_bubble_model =
-      caption_bubble_models_[caption_host_impl].get();
+      caption_bubble_models_[live_caption_speech_recognition_host].get();
   if (active_model_ != caption_bubble_model) {
     active_model_ = caption_bubble_model;
     caption_bubble_->SetModel(active_model_);
diff --git a/chrome/browser/ui/views/accessibility/caption_bubble_controller_views.h b/chrome/browser/ui/views/accessibility/caption_bubble_controller_views.h
index 62e9239..061bb2a2 100644
--- a/chrome/browser/ui/views/accessibility/caption_bubble_controller_views.h
+++ b/chrome/browser/ui/views/accessibility/caption_bubble_controller_views.h
@@ -41,14 +41,16 @@
   // the transcription result was set on the caption bubble successfully.
   // Transcriptions will halt if this returns false.
   bool OnTranscription(
-      CaptionHostImpl* caption_host_impl,
+      LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host,
       const media::mojom::SpeechRecognitionResultPtr& result) override;
 
   // Called when the speech service has an error.
-  void OnError(CaptionHostImpl* caption_host_impl) override;
+  void OnError(LiveCaptionSpeechRecognitionHost*
+                   live_caption_speech_recognition_host) override;
 
   // Called when the audio stream has ended.
-  void OnAudioStreamEnd(CaptionHostImpl* caption_host_impl) override;
+  void OnAudioStreamEnd(LiveCaptionSpeechRecognitionHost*
+                            live_caption_speech_recognition_host) override;
 
   // Called when the caption style changes.
   void UpdateCaptionStyle(
@@ -64,7 +66,8 @@
   // Sets the active CaptionBubbleModel to the one corresponding to the given
   // media player id, and creates a new CaptionBubbleModel if one does not
   // already exist.
-  void SetActiveModel(CaptionHostImpl* caption_host_impl);
+  void SetActiveModel(
+      LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host);
 
   // A callback passed to the CaptionBubbleModel which is called when the
   // BackToTab button is clicked in the CaptionBubble.
@@ -82,7 +85,8 @@
   // A map of media player ids and their corresponding CaptionBubbleModel. New
   // entries are added to this map when a previously unseen media player id is
   // received.
-  std::unordered_map<CaptionHostImpl*, std::unique_ptr<CaptionBubbleModel>>
+  std::unordered_map<LiveCaptionSpeechRecognitionHost*,
+                     std::unique_ptr<CaptionBubbleModel>>
       caption_bubble_models_;
 };
 }  // namespace captions
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 45dadd3..20e9436 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
@@ -10,7 +10,7 @@
 #include "base/test/scoped_mock_time_message_loop_task_runner.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
-#include "chrome/browser/accessibility/caption_host_impl.h"
+#include "chrome/browser/accessibility/live_caption_speech_recognition_host.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_commands.h"
 #include "chrome/browser/ui/browser_tabstrip.h"
@@ -53,11 +53,12 @@
     return controller_.get();
   }
 
-  CaptionHostImpl* GetCaptionHostImpl() {
-    if (!caption_host_impl_)
-      caption_host_impl_ = std::make_unique<CaptionHostImpl>(
+  LiveCaptionSpeechRecognitionHost* GetLiveCaptionSpeechRecognitionHost() {
+    if (!live_caption_speech_recognition_host_)
+      live_caption_speech_recognition_host_ = std::make_unique<
+          LiveCaptionSpeechRecognitionHost>(
           browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame());
-    return caption_host_impl_.get();
+    return live_caption_speech_recognition_host_.get();
   }
 
   CaptionBubble* GetBubble() {
@@ -140,35 +141,38 @@
   }
 
   bool OnPartialTranscription(std::string text) {
-    return OnPartialTranscription(text, GetCaptionHostImpl());
+    return OnPartialTranscription(text, GetLiveCaptionSpeechRecognitionHost());
   }
 
-  bool OnPartialTranscription(std::string text,
-                              CaptionHostImpl* caption_host_impl) {
+  bool OnPartialTranscription(
+      std::string text,
+      LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host) {
     return GetController()->OnTranscription(
-        caption_host_impl,
+        live_caption_speech_recognition_host,
         media::mojom::SpeechRecognitionResult::New(text, false));
   }
 
   bool OnFinalTranscription(std::string text) {
-    return OnFinalTranscription(text, GetCaptionHostImpl());
+    return OnFinalTranscription(text, GetLiveCaptionSpeechRecognitionHost());
   }
 
-  bool OnFinalTranscription(std::string text,
-                            CaptionHostImpl* caption_host_impl) {
+  bool OnFinalTranscription(
+      std::string text,
+      LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host) {
     return GetController()->OnTranscription(
-        caption_host_impl,
+        live_caption_speech_recognition_host,
         media::mojom::SpeechRecognitionResult::New(text, true));
   }
 
-  void OnError() { OnError(GetCaptionHostImpl()); }
+  void OnError() { OnError(GetLiveCaptionSpeechRecognitionHost()); }
 
-  void OnError(CaptionHostImpl* caption_host_impl) {
-    GetController()->OnError(caption_host_impl);
+  void OnError(
+      LiveCaptionSpeechRecognitionHost* live_caption_speech_recognition_host) {
+    GetController()->OnError(live_caption_speech_recognition_host);
   }
 
   void OnAudioStreamEnd() {
-    GetController()->OnAudioStreamEnd(GetCaptionHostImpl());
+    GetController()->OnAudioStreamEnd(GetLiveCaptionSpeechRecognitionHost());
   }
 
   std::vector<ui::AXNodeData> GetAXLinesNodeData() {
@@ -199,7 +203,8 @@
 
  private:
   std::unique_ptr<CaptionBubbleControllerViews> controller_;
-  std::unique_ptr<CaptionHostImpl> caption_host_impl_;
+  std::unique_ptr<LiveCaptionSpeechRecognitionHost>
+      live_caption_speech_recognition_host_;
 };
 
 IN_PROC_BROWSER_TEST_F(CaptionBubbleControllerViewsTest, ShowsCaptionInBubble) {
@@ -342,7 +347,7 @@
   OnError();
 
   // The error should not be visible on a different media stream.
-  auto media_1 = std::make_unique<CaptionHostImpl>(
+  auto media_1 = std::make_unique<LiveCaptionSpeechRecognitionHost>(
       browser()->tab_strip_model()->GetActiveWebContents()->GetFocusedFrame());
   OnPartialTranscription("Elephants are vegetarians.", media_1.get());
   EXPECT_TRUE(GetTitle()->GetVisible());
@@ -767,8 +772,9 @@
   // This test has two medias.
   // Media 0 has the text "Polar bears are the largest carnivores on land".
   // Media 1 has the text "A snail can sleep for two years".
-  CaptionHostImpl* media_0 = GetCaptionHostImpl();
-  auto media_1 = std::make_unique<CaptionHostImpl>(
+  LiveCaptionSpeechRecognitionHost* media_0 =
+      GetLiveCaptionSpeechRecognitionHost();
+  auto media_1 = std::make_unique<LiveCaptionSpeechRecognitionHost>(
       browser()->tab_strip_model()->GetActiveWebContents()->GetFocusedFrame());
 
   // Send final transcription from media 0.
@@ -845,7 +851,7 @@
   EXPECT_EQ(7 * line_height, GetLabel()->GetBoundsInScreen().height());
 
   // Switch media. The bubble should remain expanded.
-  auto media_1 = std::make_unique<CaptionHostImpl>(
+  auto media_1 = std::make_unique<LiveCaptionSpeechRecognitionHost>(
       browser()->tab_strip_model()->GetActiveWebContents()->GetFocusedFrame());
   OnPartialTranscription("Nearly all ants are female.", media_1.get());
   EXPECT_TRUE(GetCollapseButton()->GetVisible());
@@ -924,8 +930,9 @@
 
 IN_PROC_BROWSER_TEST_F(CaptionBubbleControllerViewsTest,
                        AccessibleTextChangesWhenMediaChanges) {
-  CaptionHostImpl* media_0 = GetCaptionHostImpl();
-  auto media_1 = std::make_unique<CaptionHostImpl>(
+  LiveCaptionSpeechRecognitionHost* media_0 =
+      GetLiveCaptionSpeechRecognitionHost();
+  auto media_1 = std::make_unique<LiveCaptionSpeechRecognitionHost>(
       browser()->tab_strip_model()->GetActiveWebContents()->GetFocusedFrame());
 
   OnPartialTranscription("3 dogs survived the Titanic sinking.", media_0);
diff --git a/chrome/browser/ui/views/autofill/edit_address_profile_view.cc b/chrome/browser/ui/views/autofill/edit_address_profile_view.cc
index 86fbfd924..d3b8ab24 100644
--- a/chrome/browser/ui/views/autofill/edit_address_profile_view.cc
+++ b/chrome/browser/ui/views/autofill/edit_address_profile_view.cc
@@ -9,6 +9,8 @@
 #include "chrome/browser/ui/views/autofill/address_editor_view.h"
 #include "chrome/browser/ui/views/chrome_layout_provider.h"
 #include "components/autofill/core/common/autofill_features.h"
+#include "components/strings/grit/components_strings.h"
+#include "ui/base/l10n/l10n_util.h"
 #include "ui/views/layout/fill_layout.h"
 
 namespace autofill {
@@ -39,6 +41,9 @@
 
   SetTitle(controller_->GetWindowTitle());
   SetButtonLabel(ui::DIALOG_BUTTON_OK, controller_->GetOkButtonLabel());
+  SetButtonLabel(ui::DIALOG_BUTTON_CANCEL,
+                 l10n_util::GetStringUTF16(
+                     IDS_AUTOFILL_EDIT_ADDRESS_DIALOG_CANCEL_BUTTON_LABEL));
 }
 
 EditAddressProfileView::~EditAddressProfileView() = default;
diff --git a/chrome/browser/ui/views/autofill/save_address_profile_icon_view.cc b/chrome/browser/ui/views/autofill/save_address_profile_icon_view.cc
index ebad3aa..93b2b52 100644
--- a/chrome/browser/ui/views/autofill/save_address_profile_icon_view.cc
+++ b/chrome/browser/ui/views/autofill/save_address_profile_icon_view.cc
@@ -32,9 +32,6 @@
 }
 
 void SaveAddressProfileIconView::UpdateImpl() {
-  if (!GetWebContents())
-    return;
-
   SaveAddressProfileIconController* controller = GetController();
   bool command_enabled =
       SetCommandEnabled(controller && controller->IsBubbleActive());
@@ -43,8 +40,10 @@
 
 std::u16string SaveAddressProfileIconView::GetTextForTooltipAndAccessibleName()
     const {
-  // TODO(crbug.com/1167060): Update upon having final mocks.
-  return u"Save Address";
+  SaveAddressProfileIconController* controller = GetController();
+  if (!controller)
+    return std::u16string();
+  return controller->GetPageActionIconTootip();
 }
 
 void SaveAddressProfileIconView::OnExecuting(
diff --git a/chrome/browser/ui/views/autofill/save_address_profile_view.cc b/chrome/browser/ui/views/autofill/save_address_profile_view.cc
index f7d0472..1546720 100644
--- a/chrome/browser/ui/views/autofill/save_address_profile_view.cc
+++ b/chrome/browser/ui/views/autofill/save_address_profile_view.cc
@@ -17,7 +17,9 @@
 #include "components/autofill/core/browser/data_model/autofill_profile.h"
 #include "components/autofill/core/browser/field_types.h"
 #include "components/autofill/core/common/autofill_features.h"
+#include "components/strings/grit/components_strings.h"
 #include "components/vector_icons/vector_icons.h"
+#include "ui/base/l10n/l10n_util.h"
 #include "ui/base/models/simple_combobox_model.h"
 #include "ui/gfx/color_utils.h"
 #include "ui/views/controls/button/image_button.h"
@@ -222,6 +224,14 @@
       base::Unretained(controller_),
       AutofillClient::SaveAddressProfileOfferUserDecision::kDeclined));
 
+  SetTitle(controller_->GetWindowTitle());
+  SetButtonLabel(ui::DIALOG_BUTTON_OK,
+                 l10n_util::GetStringUTF16(
+                     IDS_AUTOFILL_SAVE_ADDRESS_PROMPT_OK_BUTTON_LABEL));
+  SetButtonLabel(ui::DIALOG_BUTTON_CANCEL,
+                 l10n_util::GetStringUTF16(
+                     IDS_AUTOFILL_SAVE_ADDRESS_PROMPT_CANCEL_BUTTON_LABEL));
+
   SetLayoutManager(std::make_unique<views::FlexLayout>())
       ->SetOrientation(views::LayoutOrientation::kHorizontal)
       .SetCrossAxisAlignment(views::LayoutAlignment::kStart)
@@ -235,14 +245,15 @@
           views::MinimumFlexSizeRule::kPreferredSnapToMinimum,
           views::MaximumFlexSizeRule::kUnbounded));
 
-  // TODO(crbug.com/1167060): Update icons upon having final mocks
   edit_button_ = AddChildView(views::CreateVectorImageButtonWithNativeTheme(
       base::BindRepeating(
           &SaveUpdateAddressProfileBubbleController::OnEditButtonClicked,
           base::Unretained(controller_)),
       vector_icons::kEditIcon, kIconSize));
-  // TODO(crbug.com/1167060): Use internationalized string.
-  edit_button_->SetAccessibleName(u"Edit Address");
+  edit_button_->SetAccessibleName(l10n_util::GetStringUTF16(
+      IDS_AUTOFILL_SAVE_ADDRESS_PROMPT_EDIT_BUTTON_TOOLTIP));
+  edit_button_->SetTooltipText(l10n_util::GetStringUTF16(
+      IDS_AUTOFILL_SAVE_ADDRESS_PROMPT_EDIT_BUTTON_TOOLTIP));
 
   address_components_view_
       ->SetLayoutManager(std::make_unique<views::FlexLayout>())
@@ -305,10 +316,6 @@
   return true;
 }
 
-std::u16string SaveAddressProfileView::GetWindowTitle() const {
-  return controller_->GetWindowTitle();
-}
-
 void SaveAddressProfileView::WindowClosing() {
   if (controller_) {
     controller_->OnBubbleClosed();
@@ -334,7 +341,6 @@
 }
 
 void SaveAddressProfileView::AddedToWidget() {
-  // TODO(crbug.com/1167060): Update upon having final mocks.
   // Set the header image.
   ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
   auto image_view = std::make_unique<ThemeTrackingNonAccessibleImageView>(
diff --git a/chrome/browser/ui/views/autofill/save_address_profile_view.h b/chrome/browser/ui/views/autofill/save_address_profile_view.h
index e4d6567..653ccdf 100644
--- a/chrome/browser/ui/views/autofill/save_address_profile_view.h
+++ b/chrome/browser/ui/views/autofill/save_address_profile_view.h
@@ -36,7 +36,6 @@
 
   // views::WidgetDelegate:
   bool ShouldShowCloseButton() const override;
-  std::u16string GetWindowTitle() const override;
   void WindowClosing() override;
 
   void Show(DisplayReason reason);
diff --git a/chrome/browser/ui/views/autofill/update_address_profile_view.cc b/chrome/browser/ui/views/autofill/update_address_profile_view.cc
index 238319d..f05e2fb 100644
--- a/chrome/browser/ui/views/autofill/update_address_profile_view.cc
+++ b/chrome/browser/ui/views/autofill/update_address_profile_view.cc
@@ -12,7 +12,9 @@
 #include "components/autofill/core/browser/autofill_address_util.h"
 #include "components/autofill/core/browser/field_types.h"
 #include "components/autofill/core/common/autofill_features.h"
+#include "components/strings/grit/components_strings.h"
 #include "components/vector_icons/vector_icons.h"
+#include "ui/base/l10n/l10n_util.h"
 #include "ui/gfx/paint_vector_icon.h"
 #include "ui/views/controls/button/image_button.h"
 #include "ui/views/controls/button/image_button_factory.h"
@@ -29,13 +31,6 @@
 constexpr int kIconSize = 16;
 constexpr int kValuesLabelWidth = 190;
 
-int AddressDetailsIconSize() {
-  // Use the line height of the body small text. This allows the icons to adapt
-  // if the user changes the font size.
-  return views::style::GetLineHeight(views::style::CONTEXT_LABEL,
-                                     views::style::STYLE_PRIMARY);
-}
-
 const gfx::VectorIcon& GetVectorIconForType(ServerFieldType type) {
   switch (type) {
     case NAME_FULL_WITH_HONORIFIC_PREFIX:
@@ -92,9 +87,8 @@
                     views::DISTANCE_RELATED_LABEL_HORIZONTAL)));
 
     auto icon_view = std::make_unique<views::ImageView>();
-    icon_view->SetImage(
-        ui::ImageModel::FromVectorIcon(GetVectorIconForType(diff_entry.type),
-                                       icon_color, AddressDetailsIconSize()));
+    icon_view->SetImage(ui::ImageModel::FromVectorIcon(
+        GetVectorIconForType(diff_entry.type), icon_color, kIconSize));
 
     value_row->AddChildView(std::move(icon_view));
     auto label_view =
@@ -117,11 +111,13 @@
   layout->StartRow(/*vertical_resize=*/views::GridLayout::kFixedSize,
                    kColumnSetId);
 
-  // TODO(crbug.com/1167060): Use internationalized string.
   if (show_row_label) {
     std::unique_ptr<views::Label> label(new views::Label(
-        are_new_values ? u"New" : u"Old", views::style::CONTEXT_LABEL,
-        views::style::STYLE_SECONDARY));
+        l10n_util::GetStringUTF16(
+            are_new_values
+                ? IDS_AUTOFILL_UPDATE_ADDRESS_PROMPT_NEW_VALUES_SECTION_LABEL
+                : IDS_AUTOFILL_UPDATE_ADDRESS_PROMPT_OLD_VALUES_SECTION_LABEL),
+        views::style::CONTEXT_LABEL, views::style::STYLE_SECONDARY));
     layout->AddView(std::move(label), /*col_span=*/1, /*row_span=*/1,
                     /*h_align=*/views::GridLayout::LEADING,
                     /*v_align=*/views::GridLayout::LEADING);
@@ -139,8 +135,11 @@
         views::CreateVectorImageButtonWithNativeTheme(
             std::move(edit_button_callback), vector_icons::kEditIcon,
             kIconSize);
-    // TODO(crbug.com/1167060): Use internationalized string.
-    edit_button->SetAccessibleName(u"Edit Address");
+
+    edit_button->SetAccessibleName(l10n_util::GetStringUTF16(
+        IDS_AUTOFILL_SAVE_ADDRESS_PROMPT_EDIT_BUTTON_TOOLTIP));
+    edit_button->SetTooltipText(l10n_util::GetStringUTF16(
+        IDS_AUTOFILL_SAVE_ADDRESS_PROMPT_EDIT_BUTTON_TOOLTIP));
     layout->AddView(std::move(edit_button), /*col_span=*/1, /*row_span=*/1,
                     /*h_align=*/views::GridLayout::LEADING,
                     /*v_align=*/views::GridLayout::LEADING);
@@ -188,6 +187,14 @@
       base::Unretained(controller_),
       AutofillClient::SaveAddressProfileOfferUserDecision::kDeclined));
 
+  SetTitle(controller_->GetWindowTitle());
+  SetButtonLabel(ui::DIALOG_BUTTON_OK,
+                 l10n_util::GetStringUTF16(
+                     IDS_AUTOFILL_UPDATE_ADDRESS_PROMPT_OK_BUTTON_LABEL));
+  SetButtonLabel(ui::DIALOG_BUTTON_CANCEL,
+                 l10n_util::GetStringUTF16(
+                     IDS_AUTOFILL_UPDATE_ADDRESS_PROMPT_CANCEL_BUTTON_LABEL));
+
   SetLayoutManager(std::make_unique<views::FlexLayout>())
       ->SetOrientation(views::LayoutOrientation::kVertical)
       .SetCrossAxisAlignment(views::LayoutAlignment::kStretch)
diff --git a/chrome/browser/ui/views/page_info/safety_tip_page_info_bubble_view.cc b/chrome/browser/ui/views/page_info/safety_tip_page_info_bubble_view.cc
index d8d7627..fbfb79a 100644
--- a/chrome/browser/ui/views/page_info/safety_tip_page_info_bubble_view.cc
+++ b/chrome/browser/ui/views/page_info/safety_tip_page_info_bubble_view.cc
@@ -187,7 +187,8 @@
       action_taken_ = SafetyTipInteraction::kDismissWithClose;
       break;
     case views::Widget::ClosedReason::kCancelButtonClicked:
-      NOTREACHED();
+      // I don't know why, but ESC sometimes generates kCancelButtonClicked.
+      action_taken_ = SafetyTipInteraction::kDismissWithEsc;
       break;
   }
   std::move(close_callback_).Run(action_taken_);
diff --git a/chrome/browser/ui/views/page_info/safety_tip_page_info_bubble_view_browsertest.cc b/chrome/browser/ui/views/page_info/safety_tip_page_info_bubble_view_browsertest.cc
index fb794bc..6852bd7 100644
--- a/chrome/browser/ui/views/page_info/safety_tip_page_info_bubble_view_browsertest.cc
+++ b/chrome/browser/ui/views/page_info/safety_tip_page_info_bubble_view_browsertest.cc
@@ -260,6 +260,8 @@
     switch (ui_status()) {
       case UIStatus::kDisabled:
         disabled_features.push_back(security_state::features::kSafetyTipUI);
+        disabled_features.push_back(
+            lookalikes::features::kDetectTargetEmbeddingLookalikes);
         break;
       case UIStatus::kEnabledWithDefaultFeatures:
         enabled_features.emplace_back(security_state::features::kSafetyTipUI,
@@ -271,8 +273,7 @@
             base::FieldTrialParams({{"suspicioussites", "true"},
                                     {"topsites", "false"},
                                     {"editdistance", "false"},
-                                    {"editdistance_siteengagement", "false"},
-                                    {"targetembedding", "false"}}));
+                                    {"editdistance_siteengagement", "false"}}));
         break;
       case UIStatus::kEnabledWithAllFeatures:
         enabled_features.emplace_back(
@@ -280,8 +281,7 @@
             base::FieldTrialParams({{"suspicioussites", "true"},
                                     {"topsites", "true"},
                                     {"editdistance", "true"},
-                                    {"editdistance_siteengagement", "true"},
-                                    {"targetembedding", "true"}}));
+                                    {"editdistance_siteengagement", "true"}}));
         enabled_features.emplace_back(
             lookalikes::features::kDetectTargetEmbeddingLookalikes,
             base::FieldTrialParams());
@@ -606,7 +606,7 @@
       browser(), security_state::SafetyTipStatus::kBadReputation, GURL()));
 
   // ...but suppressed by the allowlist.
-  reputation::SetSafetyTipAllowlistPatterns({"site1.com/"}, {});
+  reputation::SetSafetyTipAllowlistPatterns({"site1.com/"}, {}, {});
   NavigateToURL(browser(), kNavigatedUrl, WindowOpenDisposition::CURRENT_TAB);
   EXPECT_FALSE(IsUIShowing());
   ASSERT_NO_FATAL_FAILURE(CheckPageInfoDoesNotShowSafetyTipInfo(browser()));
@@ -874,7 +874,7 @@
   EXPECT_TRUE(IsUIShowingOrAllFeaturesEnabled());
 
   // ...but suppressed by the allowlist.
-  reputation::SetSafetyTipAllowlistPatterns({"xn--googl-fsa.sk/"}, {});
+  reputation::SetSafetyTipAllowlistPatterns({"xn--googl-fsa.sk/"}, {}, {});
   SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
   NavigateToURL(browser(), kNavigatedUrl, WindowOpenDisposition::CURRENT_TAB);
   EXPECT_FALSE(IsUIShowing());
@@ -891,7 +891,7 @@
   // This domain is one edit distance from one of a top 500 domain.
   const GURL kNavigatedUrl = GetURL("gooogle.com");
 
-  reputation::SetSafetyTipAllowlistPatterns({}, {"google\\.com"});
+  reputation::SetSafetyTipAllowlistPatterns({}, {"google\\.com"}, {});
   SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
 
   NavigateToURL(browser(), kNavigatedUrl, WindowOpenDisposition::CURRENT_TAB);
@@ -904,13 +904,36 @@
 // distance when enabled, and not otherwise.
 IN_PROC_BROWSER_TEST_P(SafetyTipPageInfoBubbleViewBrowserTest,
                        TriggersOnEditDistance) {
-  // This domain is an edit distance of one from the top 500.
+  // This domain is an edit distance from google.com.
   const GURL kNavigatedUrl = GetURL("goooglé.com");
+  const GURL kTargetUrl = GetURL("google.com");
   SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
+  SetEngagementScore(browser(), kTargetUrl, kHighEngagement);
   NavigateToURL(browser(), kNavigatedUrl, WindowOpenDisposition::CURRENT_TAB);
   EXPECT_EQ(IsUIShowing(), AreLookalikeWarningsEnabled());
 }
 
+// Tests that Safety Tips trigger on lookalike domains with tail embedding when
+// enabled, and not otherwise.
+IN_PROC_BROWSER_TEST_P(SafetyTipPageInfoBubbleViewBrowserTest,
+                       TriggersOnTailEmbedding) {
+  // Tail-embedding has the canonical domain at the very end of the lookalike.
+  const GURL kNavigatedUrl = GetURL("accounts-google.com");
+  SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
+  NavigateToURL(browser(), kNavigatedUrl, WindowOpenDisposition::CURRENT_TAB);
+  EXPECT_TRUE(IsUIShowingOrDisabled());
+}
+
+// Tests that Safety Tips don't trigger on lookalike domains with non-tail
+// target embedding.
+IN_PROC_BROWSER_TEST_P(SafetyTipPageInfoBubbleViewBrowserTest,
+                       DoesntTriggersOnGenericTargetEmbedding) {
+  const GURL kNavigatedUrl = GetURL("google.com.evil.com");
+  SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
+  NavigateToURL(browser(), kNavigatedUrl, WindowOpenDisposition::CURRENT_TAB);
+  EXPECT_FALSE(IsUIShowing());
+}
+
 // Tests that the SafetyTipShown histogram triggers correctly.
 // Flaky on all platforms: https://crbug.com/1139955
 IN_PROC_BROWSER_TEST_P(SafetyTipPageInfoBubbleViewBrowserTest,
diff --git a/chrome/browser/ui/views/tabs/tab_drag_controller.cc b/chrome/browser/ui/views/tabs/tab_drag_controller.cc
index 66eb538..5821f25d 100644
--- a/chrome/browser/ui/views/tabs/tab_drag_controller.cc
+++ b/chrome/browser/ui/views/tabs/tab_drag_controller.cc
@@ -2116,6 +2116,12 @@
   create_params.user_gesture = true;
   create_params.in_tab_dragging = true;
   create_params.initial_bounds = new_bounds;
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+  // Do not copy attached window's restore id as this will cause Full Restore to
+  // restore the newly created browser using the original browser's stored data.
+  // See crbug.com/1208923 for details.
+  create_params.restore_id = Browser::kDefaultRestoreId;
+#endif
   // Do not copy attached window's show state as the attached window might be a
   // maximized or fullscreen window and we do not want the newly created browser
   // window is a maximized or fullscreen window since it will prevent window
diff --git a/chrome/browser/ui/views/web_apps/web_app_protocol_handler_intent_picker_dialog_view.cc b/chrome/browser/ui/views/web_apps/web_app_protocol_handler_intent_picker_dialog_view.cc
index 07567da..d9c8f77 100644
--- a/chrome/browser/ui/views/web_apps/web_app_protocol_handler_intent_picker_dialog_view.cc
+++ b/chrome/browser/ui/views/web_apps/web_app_protocol_handler_intent_picker_dialog_view.cc
@@ -86,11 +86,11 @@
       keep_alive_(std::move(keep_alive)),
       close_callback_(std::move(close_callback)) {
   SetDefaultButton(ui::DIALOG_BUTTON_CANCEL);
-  SetModalType(ui::MODAL_TYPE_WINDOW);
+  SetModalType(ui::MODAL_TYPE_NONE);
   std::u16string title = l10n_util::GetStringUTF16(
       IDS_PROTOCOL_HANDLER_INTENT_PICKER_SINGLE_TITLE);
   SetTitle(title);
-  SetShowCloseButton(false);
+  SetShowCloseButton(true);
 
   SetButtonLabel(ui::DIALOG_BUTTON_OK,
                  l10n_util::GetStringUTF16(
diff --git a/chrome/browser/ui/webid/account_selection_view.h b/chrome/browser/ui/webid/account_selection_view.h
index 70f9aa6..1851d53 100644
--- a/chrome/browser/ui/webid/account_selection_view.h
+++ b/chrome/browser/ui/webid/account_selection_view.h
@@ -37,10 +37,12 @@
   virtual ~AccountSelectionView() = default;
 
   // Instructs the view to show the provided |accounts| to the user.
-  // |url| is the  current origin.
+  // |rp_url| is the current origin and |idp_url| is the IdP origin.
   // After user interaction either OnAccountSelected() or OnDismiss() gets
   // invoked.
-  virtual void Show(const GURL& url, base::span<const Account> accounts) = 0;
+  virtual void Show(const GURL& rp_url,
+                    const GURL& idp_url,
+                    base::span<const Account> accounts) = 0;
 
  protected:
   Delegate* delegate_ = nullptr;
diff --git a/chrome/browser/ui/webid/identity_dialog_controller.cc b/chrome/browser/ui/webid/identity_dialog_controller.cc
index c04163c..e52290a 100644
--- a/chrome/browser/ui/webid/identity_dialog_controller.cc
+++ b/chrome/browser/ui/webid/identity_dialog_controller.cc
@@ -95,20 +95,22 @@
 void IdentityDialogController::ShowAccountsDialog(
     content::WebContents* rp_web_contents,
     content::WebContents* idp_web_contents,
-    const GURL& idp_signin_url,
+    const GURL& idp_url,
     AccountList accounts,
     AccountSelectionCallback on_selected) {
+  // IDP scheme is expected to always be `https://`.
+  CHECK(idp_url.SchemeIs(url::kHttpsScheme));
 #if !defined(OS_ANDROID)
   std::move(on_selected).Run(accounts[0].sub);
 #else
   rp_web_contents_ = rp_web_contents;
   on_account_selection_ = std::move(on_selected);
-  const GURL& url = rp_web_contents_->GetLastCommittedURL();
+  const GURL& rp_url = rp_web_contents_->GetLastCommittedURL();
 
   if (!account_view_)
     account_view_ = AccountSelectionView::Create(this);
 
-  account_view_->Show(url, accounts);
+  account_view_->Show(rp_url, idp_url, accounts);
 #endif
 }
 
diff --git a/chrome/browser/ui/webid/identity_dialog_controller.h b/chrome/browser/ui/webid/identity_dialog_controller.h
index e67d30e..64d6c66 100644
--- a/chrome/browser/ui/webid/identity_dialog_controller.h
+++ b/chrome/browser/ui/webid/identity_dialog_controller.h
@@ -48,7 +48,7 @@
 
   void ShowAccountsDialog(content::WebContents* rp_web_contents,
                           content::WebContents* idp_web_contents,
-                          const GURL& idp_signin_url,
+                          const GURL& idp_url,
                           AccountList accounts,
                           AccountSelectionCallback on_selected) override;
 
diff --git a/chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc b/chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc
index bd74ce6..bf86688 100644
--- a/chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc
+++ b/chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc
@@ -824,7 +824,7 @@
       !profile->IsOffTheRecord()) {
     return &NewWebUI<nearby_share::NearbyShareDialogUI>;
   }
-  if (url.host_piece() == chrome::kChromeUIProjectorHost)
+  if (url.host_piece() == chrome::kChromeUIProjectorSelfieCamHost)
     return &NewWebUI<chromeos::ProjectorUI>;
   if (url.host_piece() == chrome::kChromeUISetTimeHost)
     return &NewWebUI<chromeos::SetTimeUI>;
diff --git a/chrome/browser/ui/webui/chromeos/cellular_setup/cellular_setup_localized_strings_provider.cc b/chrome/browser/ui/webui/chromeos/cellular_setup/cellular_setup_localized_strings_provider.cc
index 10310760..2073bbf 100644
--- a/chrome/browser/ui/webui/chromeos/cellular_setup/cellular_setup_localized_strings_provider.cc
+++ b/chrome/browser/ui/webui/chromeos/cellular_setup/cellular_setup_localized_strings_provider.cc
@@ -11,6 +11,7 @@
 #include "base/values.h"
 #include "chrome/browser/ui/webui/webui_util.h"
 #include "chrome/grit/generated_resources.h"
+#include "chrome/grit/oobe_resources.h"
 #include "components/login/localized_values_builder.h"
 #include "components/strings/grit/components_strings.h"
 #include "content/public/browser/web_ui_data_source.h"
@@ -83,13 +84,20 @@
     {"confirmationCodeError",
      IDS_CELLULAR_SETUP_ESIM_PAGE_CONFIRMATION_CODE_ERROR},
     {"confirmationCodeLoading",
-     IDS_CELLULAR_SETUP_ESIM_PAGE_CONFIRMATION_CODE_LOADING}};  // namespace
+     IDS_CELLULAR_SETUP_ESIM_PAGE_CONFIRMATION_CODE_LOADING},
+    {"verifyingActivationCode",
+     IDS_CELLULAR_SETUP_ESIM_PAGE_VERIFYING_ACTIVATION_CODE}};
 
 struct NamedBoolean {
   const char* name;
   bool value;
 };
 
+struct NamedResourceId {
+  const char* name;
+  int value;
+};
+
 const std::vector<const NamedBoolean>& GetBooleanValues() {
   static const base::NoDestructor<std::vector<const NamedBoolean>> named_bools(
       {{"updatedCellularActivationUi",
@@ -100,6 +108,12 @@
   return *named_bools;
 }
 
+const std::vector<const NamedResourceId>& GetResourceIdValues() {
+  static const base::NoDestructor<std::vector<const NamedResourceId>>
+      named_resource_ids({{"spinner.json", IDR_LOGIN_SPINNER_ANIMATION}});
+  return *named_resource_ids;
+}
+
 }  //  namespace
 
 void AddLocalizedStrings(content::WebUIDataSource* html_source) {
@@ -114,11 +128,17 @@
 void AddNonStringLoadTimeData(content::WebUIDataSource* html_source) {
   for (const auto& entry : GetBooleanValues())
     html_source->AddBoolean(entry.name, entry.value);
+
+  for (const auto& entry : GetResourceIdValues())
+    html_source->AddResourcePath(entry.name, entry.value);
 }
 
 void AddNonStringLoadTimeDataToDict(base::DictionaryValue* dict) {
   for (const auto& entry : GetBooleanValues())
     dict->SetBoolean(entry.name, entry.value);
+
+  for (const auto& entry : GetResourceIdValues())
+    dict->SetInteger(entry.name, entry.value);
 }
 
 }  // namespace cellular_setup
diff --git a/chrome/browser/ui/webui/chromeos/projector/projector_ui.cc b/chrome/browser/ui/webui/chromeos/projector/projector_ui.cc
index 110edb2..429862a 100644
--- a/chrome/browser/ui/webui/chromeos/projector/projector_ui.cc
+++ b/chrome/browser/ui/webui/chromeos/projector/projector_ui.cc
@@ -6,7 +6,7 @@
 
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/webui/webui_util.h"
-#include "chrome/common/url_constants.h"
+#include "chrome/common/webui_url_constants.h"
 #include "chromeos/projector/grit/projector_resources.h"
 #include "chromeos/projector/grit/projector_resources_map.h"
 #include "content/public/browser/web_ui_data_source.h"
@@ -17,7 +17,7 @@
 
 content::WebUIDataSource* CreateProjectorHTMLSource() {
   content::WebUIDataSource* source =
-      content::WebUIDataSource::Create(chrome::kChromeUIProjectorHost);
+      content::WebUIDataSource::Create(chrome::kChromeUIProjectorSelfieCamHost);
 
   webui::SetupWebUIDataSource(
       source, base::make_span(kProjectorResources, kProjectorResourcesSize),
@@ -27,11 +27,14 @@
 
 }  // namespace
 
-ProjectorUI::ProjectorUI(content::WebUI* web_ui) : WebUIController(web_ui) {
+ProjectorUI::ProjectorUI(content::WebUI* web_ui)
+    : MojoBubbleWebUIController(web_ui) {
   Profile* profile = Profile::FromWebUI(web_ui);
   content::WebUIDataSource::Add(profile, CreateProjectorHTMLSource());
 }
 
 ProjectorUI::~ProjectorUI() = default;
 
+WEB_UI_CONTROLLER_TYPE_IMPL(ProjectorUI)
+
 }  // namespace chromeos
diff --git a/chrome/browser/ui/webui/chromeos/projector/projector_ui.h b/chrome/browser/ui/webui/chromeos/projector/projector_ui.h
index 367d28f..283860f8 100644
--- a/chrome/browser/ui/webui/chromeos/projector/projector_ui.h
+++ b/chrome/browser/ui/webui/chromeos/projector/projector_ui.h
@@ -6,16 +6,20 @@
 #define CHROME_BROWSER_UI_WEBUI_CHROMEOS_PROJECTOR_PROJECTOR_UI_H_
 
 #include "content/public/browser/web_ui_controller.h"
+#include "ui/webui/mojo_bubble_web_ui_controller.h"
 
 namespace chromeos {
 
 // The implementation for the Projector Selfie Cam WebUI.
-class ProjectorUI : public content::WebUIController {
+class ProjectorUI : public ui::MojoBubbleWebUIController {
  public:
   explicit ProjectorUI(content::WebUI* web_ui);
   ~ProjectorUI() override;
   ProjectorUI(const ProjectorUI&) = delete;
   ProjectorUI& operator=(const ProjectorUI&) = delete;
+
+ private:
+  WEB_UI_CONTROLLER_TYPE_DECL();
 };
 
 }  // namespace chromeos
diff --git a/chrome/browser/ui/webui/chromeos/projector/selfie_cam_bubble_manager.cc b/chrome/browser/ui/webui/chromeos/projector/selfie_cam_bubble_manager.cc
new file mode 100644
index 0000000..597174a
--- /dev/null
+++ b/chrome/browser/ui/webui/chromeos/projector/selfie_cam_bubble_manager.cc
@@ -0,0 +1,207 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/webui/chromeos/projector/selfie_cam_bubble_manager.h"
+
+#include <memory>
+
+#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/views/bubble/webui_bubble_dialog_view.h"
+#include "chrome/browser/ui/webui/chromeos/projector/projector_ui.h"
+#include "chrome/common/webui_url_constants.h"
+#include "chrome/grit/generated_resources.h"
+#include "ui/base/hit_test.h"
+#include "ui/gfx/color_palette.h"
+#include "ui/gfx/geometry/insets.h"
+#include "ui/gfx/geometry/point.h"
+#include "ui/gfx/geometry/size.h"
+#include "ui/views/bubble/bubble_border.h"
+#include "ui/views/bubble/bubble_frame_view.h"
+#include "ui/views/controls/native/native_view_host.h"
+#include "ui/views/controls/webview/webview.h"
+
+namespace chromeos {
+
+namespace {
+
+constexpr int kCornerRadiusDip = 80;
+
+constexpr gfx::Size kPreferredSize(2 * kCornerRadiusDip, 2 * kCornerRadiusDip);
+
+// Makes the selfie cam draggable.
+class SelfieCamBubbleFrameView : public views::BubbleFrameView {
+ public:
+  SelfieCamBubbleFrameView()
+      : views::BubbleFrameView(gfx::Insets(), gfx::Insets()) {
+    auto border = std::make_unique<views::BubbleBorder>(
+        views::BubbleBorder::FLOAT, views::BubbleBorder::DIALOG_SHADOW,
+        gfx::kPlaceholderColor);
+    // Needed to make the selfie cam round.
+    border->SetCornerRadius(kCornerRadiusDip);
+    views::BubbleFrameView::SetBubbleBorder(std::move(border));
+  }
+
+  ~SelfieCamBubbleFrameView() override = default;
+  SelfieCamBubbleFrameView(const SelfieCamBubbleFrameView&) = delete;
+  SelfieCamBubbleFrameView& operator=(const SelfieCamBubbleFrameView&) = delete;
+
+  // Needed to make the selfie cam draggable everywhere within its bounds.
+  int NonClientHitTest(const gfx::Point& point) override {
+    // Outside of the window bounds, do nothing.
+    if (!bounds().Contains(point))
+      return HTNOWHERE;
+
+    // Ensure it's within the BubbleFrameView. This takes into account the
+    // rounded corners and drop shadow of the BubbleBorder.
+    int hit = views::BubbleFrameView::NonClientHitTest(point);
+
+    // After BubbleFrameView::NonClientHitTest processes the bubble-specific
+    // hits such as the rounded corners, it checks hits to the bubble's client
+    // view. Any hits to ClientFrameView::NonClientHitTest return HTCLIENT or
+    // HTNOWHERE. Override these to return HTCAPTION in order to make the
+    // entire widget draggable.
+    return (hit == HTCLIENT || hit == HTNOWHERE) ? HTCAPTION : hit;
+  }
+};
+
+// Dialog that displays the selfie cam for including the user's face in
+// Projector screen recordings.
+class SelfieCamBubbleDialogView : public WebUIBubbleDialogView {
+ public:
+  SelfieCamBubbleDialogView(
+      std::unique_ptr<BubbleContentsWrapper> contents_wrapper)
+      : WebUIBubbleDialogView(/*anchor_view=*/nullptr, contents_wrapper.get()),
+        contents_wrapper_(std::move(contents_wrapper)) {
+    set_has_parent(false);
+    set_close_on_deactivate(false);
+  }
+  ~SelfieCamBubbleDialogView() override = default;
+
+  // views::BubbleDialogDelegateView:
+  // Opens the selfie cam in the middle of the screen initially.
+  // TODO(crbug/1199396): Consider if the selfie cam should appear somewhere
+  // else by default initially, such as the bottom right of the screen.
+  gfx::Rect GetBubbleBounds() override {
+    // Bubble bounds are what the computed bubble bounds would be, taking into
+    // account the current bubble size.
+    gfx::Rect bubble_bounds =
+        views::BubbleDialogDelegateView::GetBubbleBounds();
+    // Widget bounds are where the bubble currently is in space.
+    gfx::Rect widget_bounds = GetWidget()->GetWindowBoundsInScreen();
+    // Use the widget x and y to keep the bubble oriented at its current
+    // location, and use the bubble width and height to set the correct bubble
+    // size.
+    return gfx::Rect(widget_bounds.x(), widget_bounds.y(),
+                     bubble_bounds.width(), bubble_bounds.height());
+  }
+
+  // views::BubbleDialogDelegateView:
+  void OnBeforeBubbleWidgetInit(views::Widget::InitParams* params,
+                                views::Widget* widget) const override {
+    params->type = views::Widget::InitParams::TYPE_WINDOW;
+    // Keeps the selfie cam always on top.
+    params->z_order = ui::ZOrderLevel::kFloatingWindow;
+    params->visible_on_all_workspaces = true;
+  }
+
+  // WidgetDelegate:
+  std::unique_ptr<views::NonClientFrameView> CreateNonClientFrameView(
+      views::Widget* widget) override {
+    return std::make_unique<SelfieCamBubbleFrameView>();
+  }
+
+  // Disallows closing the selfie cam through pressing the escape key because
+  // the toggle button gets out of sync with the model state. The only way to
+  // close the selfie cam should be through the toggle off button.
+  bool OnCloseRequested(views::Widget::ClosedReason close_reason) override {
+    // Pressing escape maps to kCancelButtonClicked instead of kEscKeyPressed.
+    return close_reason != views::Widget::ClosedReason::kCancelButtonClicked;
+  }
+
+  // views::View:
+  gfx::Size CalculatePreferredSize() const override { return kPreferredSize; }
+
+ private:
+  std::unique_ptr<BubbleContentsWrapper> contents_wrapper_;
+};
+
+// Renders the WebUI contents and asks for camera permission so that
+// we don't need to prompt the user.
+class SelfieCamBubbleContentsWrapper
+    : public BubbleContentsWrapperT<ProjectorUI> {
+ public:
+  SelfieCamBubbleContentsWrapper(const GURL& webui_url,
+                                 content::BrowserContext* browser_context,
+                                 int task_manager_string_id)
+      : BubbleContentsWrapperT(webui_url,
+                               browser_context,
+                               task_manager_string_id) {}
+
+  // content::WebContentsDelegate:
+  void RequestMediaAccessPermission(
+      content::WebContents* web_contents,
+      const content::MediaStreamRequest& request,
+      content::MediaResponseCallback callback) override {
+    MediaCaptureDevicesDispatcher::GetInstance()->ProcessMediaAccessRequest(
+        web_contents, request, std::move(callback), /*extension=*/nullptr);
+  }
+
+  bool CheckMediaAccessPermission(content::RenderFrameHost* render_frame_host,
+                                  const GURL& security_origin,
+                                  blink::mojom::MediaStreamType type) override {
+    return MediaCaptureDevicesDispatcher::GetInstance()
+        ->CheckMediaAccessPermission(render_frame_host, security_origin, type);
+  }
+};
+
+}  // namespace
+
+SelfieCamBubbleManager::SelfieCamBubbleManager() = default;
+SelfieCamBubbleManager::~SelfieCamBubbleManager() = default;
+
+void SelfieCamBubbleManager::Show(Profile* profile) {
+  if (IsVisible())
+    return;
+
+  auto contents_wrapper = std::make_unique<SelfieCamBubbleContentsWrapper>(
+      GURL(chrome::kChromeUIProjectorSelfieCamURL), profile,
+      IDS_SELFIE_CAM_TITLE);
+  // Need to reload the web contents here because the view isn't visible unless
+  // ShowUI is called from the JS side.  By reloading, we trigger the JS to
+  // eventually call ShowUI().
+  contents_wrapper->ReloadWebContents();
+
+  auto bubble_view =
+      std::make_unique<SelfieCamBubbleDialogView>(std::move(contents_wrapper));
+
+  bubble_view_ = bubble_view->GetWeakPtr();
+  auto* bubble_widget =
+      views::BubbleDialogDelegateView::CreateBubble(std::move(bubble_view));
+  // Needed to set the window.innerWidth and window.innerHeight to the preferred
+  // size in JavaScript so we can determine the correct camera resolution.
+  bubble_view_->web_view()->EnableSizingFromWebContents(
+      /*min_size=*/kPreferredSize, /*max_size=*/kPreferredSize);
+  // Needed to make the selfie cam round.
+  bubble_view_->web_view()->holder()->SetCornerRadii(
+      gfx::RoundedCornersF(kCornerRadiusDip));
+  // Needed to make the selfie cam draggable everywhere within its bounds.
+  bubble_view_->web_view()->holder()->SetHitTestTopInset(
+      bubble_view_->height());
+  bubble_widget->Show();
+}
+
+void SelfieCamBubbleManager::Close() {
+  if (!IsVisible())
+    return;
+
+  DCHECK(bubble_view_->GetWidget());
+  bubble_view_->GetWidget()->CloseNow();
+}
+
+bool SelfieCamBubbleManager::IsVisible() const {
+  return bubble_view_ != nullptr;
+}
+
+}  // namespace chromeos
diff --git a/chrome/browser/ui/webui/chromeos/projector/selfie_cam_bubble_manager.h b/chrome/browser/ui/webui/chromeos/projector/selfie_cam_bubble_manager.h
new file mode 100644
index 0000000..9e7852d6
--- /dev/null
+++ b/chrome/browser/ui/webui/chromeos/projector/selfie_cam_bubble_manager.h
@@ -0,0 +1,33 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_WEBUI_CHROMEOS_PROJECTOR_SELFIE_CAM_BUBBLE_MANAGER_H_
+#define CHROME_BROWSER_UI_WEBUI_CHROMEOS_PROJECTOR_SELFIE_CAM_BUBBLE_MANAGER_H_
+
+#include "base/memory/weak_ptr.h"
+
+class Profile;
+class WebUIBubbleDialogView;
+
+namespace chromeos {
+
+// Handles the creation and destruction of the selfie cam WebUI bubble.
+class SelfieCamBubbleManager {
+ public:
+  SelfieCamBubbleManager();
+  SelfieCamBubbleManager(const SelfieCamBubbleManager&) = delete;
+  SelfieCamBubbleManager& operator=(const SelfieCamBubbleManager&) = delete;
+  ~SelfieCamBubbleManager();
+
+  void Show(Profile* profile);
+  void Close();
+  bool IsVisible() const;
+
+ private:
+  base::WeakPtr<WebUIBubbleDialogView> bubble_view_;
+};
+
+}  // namespace chromeos
+
+#endif  // CHROME_BROWSER_UI_WEBUI_CHROMEOS_PROJECTOR_SELFIE_CAM_BUBBLE_MANAGER_H_
diff --git a/chrome/browser/ui/webui/print_preview/local_printer_handler_chromeos_unittest.cc b/chrome/browser/ui/webui/print_preview/local_printer_handler_chromeos_unittest.cc
index b57b84fc..65d94ae 100644
--- a/chrome/browser/ui/webui/print_preview/local_printer_handler_chromeos_unittest.cc
+++ b/chrome/browser/ui/webui/print_preview/local_printer_handler_chromeos_unittest.cc
@@ -19,6 +19,7 @@
 #include "base/values.h"
 #include "chrome/browser/chromeos/printing/test_cups_printers_manager.h"
 #include "chrome/browser/chromeos/printing/test_printer_configurer.h"
+#include "chrome/browser/printing/print_backend_service_manager.h"
 #include "chrome/browser/printing/print_backend_service_test_impl.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/common/pref_names.h"
@@ -142,24 +143,70 @@
 
 }  // namespace
 
-class LocalPrinterHandlerChromeosTest : public testing::Test {
+// Base testing class for `LocalPrinterHandlerChromeos`.  Contains the base
+// logic to allow for using either a local task runner or a service to make
+// print backend calls, and to possibly enable fallback when using a service.
+// Tests to trigger those different paths can be done by overloading
+// `UseService()` and `SupportFallback()`.
+class LocalPrinterHandlerChromeosTestBase : public testing::Test {
  public:
-  LocalPrinterHandlerChromeosTest() = default;
-  LocalPrinterHandlerChromeosTest(const LocalPrinterHandlerChromeosTest&) =
-      delete;
-  LocalPrinterHandlerChromeosTest& operator=(
-      const LocalPrinterHandlerChromeosTest&) = delete;
-  ~LocalPrinterHandlerChromeosTest() override = default;
+  LocalPrinterHandlerChromeosTestBase() = default;
+  LocalPrinterHandlerChromeosTestBase(
+      const LocalPrinterHandlerChromeosTestBase&) = delete;
+  LocalPrinterHandlerChromeosTestBase& operator=(
+      const LocalPrinterHandlerChromeosTestBase&) = delete;
+  ~LocalPrinterHandlerChromeosTestBase() override = default;
+
+  TestPrintBackend* sandboxed_print_backend() {
+    return sandboxed_test_backend_.get();
+  }
+  TestPrintBackend* unsandboxed_print_backend() {
+    return unsandboxed_test_backend_.get();
+  }
+
+  // Indicate if calls to print backend should be made using a service instead
+  // of a local task runner.
+  virtual bool UseService() = 0;
+
+  // Indicate if fallback support for access-denied errors should be included
+  // when using a service for print backend calls.
+  virtual bool SupportFallback() = 0;
 
   void SetUp() override {
-    test_backend_ = base::MakeRefCounted<TestPrintBackend>();
-    PrintBackend::SetPrintBackendForTesting(test_backend_.get());
+    // Choose between running with local test runner or via a service.
+    feature_list_.InitWithFeatureState(features::kEnableOopPrintDrivers,
+                                       UseService());
+
+    sandboxed_test_backend_ = base::MakeRefCounted<TestPrintBackend>();
     ppd_provider_ = base::MakeRefCounted<FakePpdProvider>();
     local_printer_handler_ = LocalPrinterHandlerChromeos::CreateForTesting(
         &profile_, nullptr, &printers_manager_,
         std::make_unique<chromeos::TestPrinterConfigurer>(), ppd_provider_);
+
+    if (UseService()) {
+      sandboxed_print_backend_service_ =
+          PrintBackendServiceTestImpl::LaunchForTesting(sandboxed_test_remote_,
+                                                        sandboxed_test_backend_,
+                                                        /*sandboxed=*/true);
+
+      if (SupportFallback()) {
+        unsandboxed_test_backend_ = base::MakeRefCounted<TestPrintBackend>();
+
+        unsandboxed_print_backend_service_ =
+            PrintBackendServiceTestImpl::LaunchForTesting(
+                unsandboxed_test_remote_, unsandboxed_test_backend_,
+                /*sandboxed=*/false);
+      }
+    } else {
+      // Use of task runners will call `PrintBackend::CreateInstance()`, which
+      // needs a test backend registered for it to use.
+      PrintBackend::SetPrintBackendForTesting(sandboxed_test_backend_.get());
+    }
   }
 
+  void TearDown() override { PrintBackendServiceManager::ResetForTesting(); }
+
+ protected:
   void AddPrinter(const std::string& id,
                   const std::string& display_name,
                   const std::string& description,
@@ -170,11 +217,23 @@
     auto basic_info = std::make_unique<PrinterBasicInfo>(
         id, display_name, description, /*printer_status=*/0, is_default,
         PrinterBasicInfoOptions{});
+
+    if (SupportFallback()) {
+      // Need to populate same values into a second print backend.
+      // For fallback they will always be treated as valid.
+      auto caps_unsandboxed =
+          std::make_unique<PrinterSemanticCapsAndDefaults>(*caps);
+      auto basic_info_unsandboxed =
+          std::make_unique<PrinterBasicInfo>(*basic_info);
+      unsandboxed_print_backend()->AddValidPrinter(
+          id, std::move(caps_unsandboxed), std::move(basic_info_unsandboxed));
+    }
+
     if (requires_elevated_permissions) {
-      test_backend_->AddAccessDeniedPrinter(id);
+      sandboxed_print_backend()->AddAccessDeniedPrinter(id);
     } else {
-      test_backend_->AddValidPrinter(id, std::move(caps),
-                                     std::move(basic_info));
+      sandboxed_print_backend()->AddValidPrinter(id, std::move(caps),
+                                                 std::move(basic_info));
     }
   }
 
@@ -182,8 +241,6 @@
 
   TestingProfile& profile() { return profile_; }
 
-  scoped_refptr<TestPrintBackend> test_backend() { return test_backend_; }
-
   chromeos::TestCupsPrintersManager& printers_manager() {
     return printers_manager_;
   }
@@ -197,17 +254,43 @@
   content::BrowserTaskEnvironment task_environment_;
   // Must outlive `printers_manager_`.
   TestingProfile profile_;
-  scoped_refptr<TestPrintBackend> test_backend_;
+  scoped_refptr<TestPrintBackend> sandboxed_test_backend_;
+  scoped_refptr<TestPrintBackend> unsandboxed_test_backend_;
   chromeos::TestCupsPrintersManager printers_manager_;
   scoped_refptr<FakePpdProvider> ppd_provider_;
   std::unique_ptr<LocalPrinterHandlerChromeos> local_printer_handler_;
+
+  // Support for testing via a service instead of with a local task runner.
+  base::test::ScopedFeatureList feature_list_;
+  mojo::Remote<mojom::PrintBackendService> sandboxed_test_remote_;
+  mojo::Remote<mojom::PrintBackendService> unsandboxed_test_remote_;
+  std::unique_ptr<PrintBackendServiceTestImpl> sandboxed_print_backend_service_;
+  std::unique_ptr<PrintBackendServiceTestImpl>
+      unsandboxed_print_backend_service_;
 };
 
-// `LocalPrinterHandlerChromeosProcessScopeTest` performs test which make use
-// of the print backend and can utilize that in different process scopes, be
-// that in-process via a local task runner or out-of-process through a service.
+// Testing class to cover `LocalPrinterHandlerChromeos` handling using a local
+// task runner.
+class LocalPrinterHandlerChromeosTest
+    : public LocalPrinterHandlerChromeosTestBase {
+ public:
+  LocalPrinterHandlerChromeosTest() = default;
+  LocalPrinterHandlerChromeosTest(const LocalPrinterHandlerChromeosTest&) =
+      delete;
+  LocalPrinterHandlerChromeosTest& operator=(
+      const LocalPrinterHandlerChromeosTest&) = delete;
+  ~LocalPrinterHandlerChromeosTest() override = default;
+
+  bool UseService() override { return false; }
+  bool SupportFallback() override { return false; }
+};
+
+// Testing class to cover `LocalPrinterHandlerChromeos` handling using either a
+// local task runner or a service.  Makes no attempt to cover fallback when
+// using a service, which is handled separately by
+// `LocalPrinterHandlerChromeosFallbackTest`
 class LocalPrinterHandlerChromeosProcessScopeTest
-    : public LocalPrinterHandlerChromeosTest,
+    : public LocalPrinterHandlerChromeosTestBase,
       public testing::WithParamInterface<bool> {
  public:
   LocalPrinterHandlerChromeosProcessScopeTest() = default;
@@ -217,22 +300,24 @@
       const LocalPrinterHandlerChromeosProcessScopeTest&) = delete;
   ~LocalPrinterHandlerChromeosProcessScopeTest() override = default;
 
-  void SetUp() override {
-    // Choose between running with local test runner or via a service.
-    LocalPrinterHandlerChromeosTest::SetUp();
-    const bool use_backend_service = GetParam();
-    if (use_backend_service) {
-      feature_list_.InitAndEnableFeature(features::kEnableOopPrintDrivers);
-      print_backend_service_ = PrintBackendServiceTestImpl::LaunchForTesting(
-          test_remote_, test_backend());
-    }
-  }
+  bool UseService() override { return GetParam(); }
+  bool SupportFallback() override { return false; }
+};
 
- private:
-  // Support for testing via a service instead of with a local task runner.
-  base::test::ScopedFeatureList feature_list_;
-  mojo::Remote<mojom::PrintBackendService> test_remote_;
-  std::unique_ptr<PrintBackendServiceTestImpl> print_backend_service_;
+// Testing class to cover `LocalPrinterHandlerChromeos` handling using only a
+// service and when fallback could yield different results.
+class LocalPrinterHandlerChromeosFallbackTest
+    : public LocalPrinterHandlerChromeosTestBase {
+ public:
+  LocalPrinterHandlerChromeosFallbackTest() = default;
+  LocalPrinterHandlerChromeosFallbackTest(
+      const LocalPrinterHandlerChromeosFallbackTest&) = delete;
+  LocalPrinterHandlerChromeosFallbackTest& operator=(
+      const LocalPrinterHandlerChromeosFallbackTest&) = delete;
+  ~LocalPrinterHandlerChromeosFallbackTest() override = default;
+
+  bool UseService() override { return true; }
+  bool SupportFallback() override { return true; }
 };
 
 INSTANTIATE_TEST_SUITE_P(All,
@@ -315,15 +400,14 @@
   AddPrinter("printer1", "saved", "description1", /*is_default=*/true,
              /*requires_elevated_permissions=*/false);
 
-  base::Value fetched_caps;
+  base::Value fetched_caps("dummy");
   local_printer_handler()->StartGetCapability(
       "printer1", base::BindOnce(&RecordGetCapability, std::ref(fetched_caps)));
 
   RunUntilIdle();
 
-  ASSERT_TRUE(fetched_caps.is_dict());
-  EXPECT_TRUE(fetched_caps.FindKey(kSettingCapabilities));
-  EXPECT_TRUE(fetched_caps.FindKey(kPrinter));
+  EXPECT_TRUE(fetched_caps.FindDictKey(kSettingCapabilities));
+  EXPECT_TRUE(fetched_caps.FindDictKey(kPrinter));
 }
 
 // Test that printers which have not yet been installed are installed with
@@ -340,15 +424,14 @@
   AddPrinter("printer1", "discovered", "description1", /*is_default=*/true,
              /*requires_elevated_permissions=*/false);
 
-  base::Value fetched_caps;
+  base::Value fetched_caps("dummy");
   local_printer_handler()->StartGetCapability(
       "printer1", base::BindOnce(&RecordGetCapability, std::ref(fetched_caps)));
 
   RunUntilIdle();
 
-  ASSERT_TRUE(fetched_caps.is_dict());
-  EXPECT_TRUE(fetched_caps.FindKey(kSettingCapabilities));
-  EXPECT_TRUE(fetched_caps.FindKey(kPrinter));
+  EXPECT_TRUE(fetched_caps.FindDictKey(kSettingCapabilities));
+  EXPECT_TRUE(fetched_caps.FindDictKey(kPrinter));
 }
 
 // In this test we expect the `StartGetCapability` to bail early because the
@@ -379,19 +462,49 @@
   AddPrinter("printer1", "saved", "description1", /*is_default=*/true,
              /*requires_elevated_permissions=*/true);
 
-  base::Value fetched_caps;
+  base::Value fetched_caps("dummy");
   local_printer_handler()->StartGetCapability(
       "printer1", base::BindOnce(&RecordGetCapability, std::ref(fetched_caps)));
 
   RunUntilIdle();
 
-  ASSERT_TRUE(fetched_caps.is_dict());
-  const base::Value* settings = fetched_caps.FindKey(kSettingCapabilities);
+  const base::Value* settings = fetched_caps.FindDictKey(kSettingCapabilities);
   ASSERT_TRUE(settings);
   ASSERT_TRUE(settings->is_dict());
   EXPECT_TRUE(settings->DictEmpty());
 }
 
+TEST_F(LocalPrinterHandlerChromeosFallbackTest,
+       StartGetCapabilityElevatedPermissionsSucceeds) {
+  Printer saved_printer =
+      CreateTestPrinter("printer1", "saved", "description1");
+  printers_manager().AddPrinter(saved_printer, PrinterClass::kSaved);
+  printers_manager().InstallPrinter("printer1");
+
+  // Add printer capabilities to `test_backend_`.
+  AddPrinter("printer1", "saved", "description1", /*is_default=*/true,
+             /*requires_elevated_permissions=*/true);
+
+  // Note that printer does not initially show as requiring elevated privileges.
+  EXPECT_FALSE(PrintBackendServiceManager::GetInstance()
+                   .PrinterDriverRequiresElevatedPrivilege("printer1"));
+
+  base::Value fetched_caps("dummy");
+  local_printer_handler()->StartGetCapability(
+      "printer1", base::BindOnce(&RecordGetCapability, std::ref(fetched_caps)));
+
+  RunUntilIdle();
+
+  // Getting capabilities should succeed when fallback is supported.
+  const base::Value* settings = fetched_caps.FindDictKey(kSettingCapabilities);
+  ASSERT_TRUE(settings);
+  EXPECT_TRUE(settings->FindDictKey(kPrinter));
+
+  // Verify that this printer now shows up as requiring elevated privileges.
+  EXPECT_TRUE(PrintBackendServiceManager::GetInstance()
+                  .PrinterDriverRequiresElevatedPrivilege("printer1"));
+}
+
 TEST_F(LocalPrinterHandlerChromeosTest, GetNativePrinterPolicies) {
   sync_preferences::TestingPrefServiceSyncable* prefs =
       profile().GetTestingPrefService();
diff --git a/chrome/browser/ui/webui/print_preview/local_printer_handler_default.cc b/chrome/browser/ui/webui/print_preview/local_printer_handler_default.cc
index f79aac5..2602467 100644
--- a/chrome/browser/ui/webui/print_preview/local_printer_handler_default.cc
+++ b/chrome/browser/ui/webui/print_preview/local_printer_handler_default.cc
@@ -89,12 +89,37 @@
 
 void OnDidFetchCapabilities(
     const std::string& device_name,
+    bool elevated_privileges,
     bool has_secure_protocol,
     PrinterHandler::GetCapabilityCallback callback,
     mojom::PrinterCapsAndInfoResultPtr printer_caps_and_info) {
   if (printer_caps_and_info->is_result_code()) {
     LOG(WARNING) << "Failure fetching printer capabilities for " << device_name
                  << " - error " << printer_caps_and_info->get_result_code();
+
+    // If we failed because of access denied then we could retry at an elevated
+    // privilege (if not already elevated).
+    if (printer_caps_and_info->get_result_code() ==
+            mojom::ResultCode::kAccessDenied &&
+        !elevated_privileges) {
+      // Register that this printer requires elevated privileges.
+      PrintBackendServiceManager& service_mgr =
+          PrintBackendServiceManager::GetInstance();
+      service_mgr.SetPrinterDriverRequiresElevatedPrivilege(device_name);
+
+      // Retry the operation which should now happen at a higher privilege
+      // level.
+      auto& service = service_mgr.GetService(
+          g_browser_process->GetApplicationLocale(), device_name);
+      service->FetchCapabilities(
+          device_name,
+          base::BindOnce(&OnDidFetchCapabilities, device_name,
+                         /*elevated_privileges=*/true, has_secure_protocol,
+                         std::move(callback)));
+      return;
+    }
+
+    // Unable to fallback, call back without data.
     std::move(callback).Run(base::Value());
     return;
   }
@@ -237,12 +262,16 @@
 
   if (base::FeatureList::IsEnabled(features::kEnableOopPrintDrivers)) {
     VLOG(1) << "Getting printer capabilities via service for " << device_name;
-    auto& service = PrintBackendServiceManager::GetInstance().GetService(
+    PrintBackendServiceManager& service_mgr =
+        PrintBackendServiceManager::GetInstance();
+    auto& service = service_mgr.GetService(
         g_browser_process->GetApplicationLocale(), device_name);
     service->FetchCapabilities(
         device_name,
-        base::BindOnce(&OnDidFetchCapabilities, device_name,
-                       /*has_secure_protocol=*/false, std::move(cb)));
+        base::BindOnce(
+            &OnDidFetchCapabilities, device_name,
+            service_mgr.PrinterDriverRequiresElevatedPrivilege(device_name),
+            /*has_secure_protocol=*/false, std::move(cb)));
   } else {
     VLOG(1) << "Getting printer capabilities in-process for " << device_name;
     base::PostTaskAndReplyWithResult(
diff --git a/chrome/browser/ui/webui/print_preview/local_printer_handler_default_unittest.cc b/chrome/browser/ui/webui/print_preview/local_printer_handler_default_unittest.cc
index 39657c2..6a941c6e 100644
--- a/chrome/browser/ui/webui/print_preview/local_printer_handler_default_unittest.cc
+++ b/chrome/browser/ui/webui/print_preview/local_printer_handler_default_unittest.cc
@@ -13,6 +13,7 @@
 #include "base/memory/scoped_refptr.h"
 #include "base/strings/string_piece.h"
 #include "base/values.h"
+#include "chrome/browser/printing/print_backend_service_manager.h"
 #include "chrome/browser/printing/print_backend_service_test_impl.h"
 #include "chrome/common/printing/printer_capabilities.h"
 #include "chrome/test/base/testing_profile.h"
@@ -65,38 +66,71 @@
 
 }  // namespace
 
-class LocalPrinterHandlerDefaultTest : public testing::TestWithParam<bool> {
+// Base testing class for `LocalPrinterHandlerDefault`.  Contains the base
+// logic to allow for using either a local task runner or a service to make
+// print backend calls, and to possibly enable fallback when using a service.
+// Tests to trigger those different paths can be done by overloading
+// `UseService()` and `SupportFallback()`.
+class LocalPrinterHandlerDefaultTestBase : public testing::Test {
  public:
-  LocalPrinterHandlerDefaultTest() = default;
-  LocalPrinterHandlerDefaultTest(const LocalPrinterHandlerDefaultTest&) =
-      delete;
-  LocalPrinterHandlerDefaultTest& operator=(
-      const LocalPrinterHandlerDefaultTest&) = delete;
-  ~LocalPrinterHandlerDefaultTest() override = default;
+  LocalPrinterHandlerDefaultTestBase() = default;
+  LocalPrinterHandlerDefaultTestBase(
+      const LocalPrinterHandlerDefaultTestBase&) = delete;
+  LocalPrinterHandlerDefaultTestBase& operator=(
+      const LocalPrinterHandlerDefaultTestBase&) = delete;
+  ~LocalPrinterHandlerDefaultTestBase() override = default;
 
-  TestPrintBackend* print_backend() { return test_backend_.get(); }
+  TestPrintBackend* sandboxed_print_backend() {
+    return sandboxed_test_backend_.get();
+  }
+  TestPrintBackend* unsandboxed_print_backend() {
+    return unsandboxed_test_backend_.get();
+  }
+
+  // Indicate if calls to print backend should be made using a service instead
+  // of a local task runner.
+  virtual bool UseService() = 0;
+
+  // Indicate if fallback support for access-denied errors should be included
+  // when using a service for print backend calls.
+  virtual bool SupportFallback() = 0;
 
   void SetUp() override {
     // Choose between running with local test runner or via a service.
-    const bool use_backend_service = GetParam();
     feature_list_.InitWithFeatureState(features::kEnableOopPrintDrivers,
-                                       use_backend_service);
+                                       UseService());
 
     TestingProfile::Builder builder;
     profile_ = builder.Build();
     initiator_ = content::WebContents::Create(
         content::WebContents::CreateParams(profile_.get()));
-    test_backend_ = base::MakeRefCounted<TestPrintBackend>();
-    PrintBackend::SetPrintBackendForTesting(test_backend_.get());
+    sandboxed_test_backend_ = base::MakeRefCounted<TestPrintBackend>();
+
     local_printer_handler_ =
         std::make_unique<LocalPrinterHandlerDefault>(initiator_.get());
 
-    if (use_backend_service) {
-      print_backend_service_ = PrintBackendServiceTestImpl::LaunchForTesting(
-          test_remote_, test_backend_);
+    if (UseService()) {
+      sandboxed_print_backend_service_ =
+          PrintBackendServiceTestImpl::LaunchForTesting(sandboxed_test_remote_,
+                                                        sandboxed_test_backend_,
+                                                        /*sandboxed=*/true);
+      if (SupportFallback()) {
+        unsandboxed_test_backend_ = base::MakeRefCounted<TestPrintBackend>();
+
+        unsandboxed_print_backend_service_ =
+            PrintBackendServiceTestImpl::LaunchForTesting(
+                unsandboxed_test_remote_, unsandboxed_test_backend_,
+                /*sandboxed=*/false);
+      }
+    } else {
+      // Use of task runners will call `PrintBackend::CreateInstance()`, which
+      // needs a test backend registered for it to use.
+      PrintBackend::SetPrintBackendForTesting(sandboxed_test_backend_.get());
     }
   }
 
+  void TearDown() override { PrintBackendServiceManager::ResetForTesting(); }
+
   void AddPrinter(const std::string& id,
                   const std::string& display_name,
                   const std::string& description,
@@ -109,11 +143,22 @@
         id, display_name, description,
         /*printer_status=*/0, is_default, PrinterBasicInfoOptions{});
 
+    if (SupportFallback()) {
+      // Need to populate same values into a second print backend.
+      // For fallback they will always be treated as valid.
+      auto caps_unsandboxed =
+          std::make_unique<PrinterSemanticCapsAndDefaults>(*caps);
+      auto basic_info_unsandboxed =
+          std::make_unique<PrinterBasicInfo>(*basic_info);
+      unsandboxed_print_backend()->AddValidPrinter(
+          id, std::move(caps_unsandboxed), std::move(basic_info_unsandboxed));
+    }
+
     if (requires_elevated_permissions) {
-      print_backend()->AddAccessDeniedPrinter(id);
+      sandboxed_print_backend()->AddAccessDeniedPrinter(id);
     } else {
-      print_backend()->AddValidPrinter(id, std::move(caps),
-                                       std::move(basic_info));
+      sandboxed_print_backend()->AddValidPrinter(id, std::move(caps),
+                                                 std::move(basic_info));
     }
   }
 
@@ -128,19 +173,60 @@
   content::BrowserTaskEnvironment task_environment_;
   std::unique_ptr<TestingProfile> profile_;
   std::unique_ptr<content::WebContents> initiator_;
-  scoped_refptr<TestPrintBackend> test_backend_;
+  scoped_refptr<TestPrintBackend> sandboxed_test_backend_;
+  scoped_refptr<TestPrintBackend> unsandboxed_test_backend_;
   std::unique_ptr<LocalPrinterHandlerDefault> local_printer_handler_;
 
   // Support for testing via a service instead of with a local task runner.
   base::test::ScopedFeatureList feature_list_;
-  mojo::Remote<mojom::PrintBackendService> test_remote_;
-  std::unique_ptr<PrintBackendServiceTestImpl> print_backend_service_;
+  mojo::Remote<mojom::PrintBackendService> sandboxed_test_remote_;
+  mojo::Remote<mojom::PrintBackendService> unsandboxed_test_remote_;
+  std::unique_ptr<PrintBackendServiceTestImpl> sandboxed_print_backend_service_;
+  std::unique_ptr<PrintBackendServiceTestImpl>
+      unsandboxed_print_backend_service_;
 };
 
-INSTANTIATE_TEST_SUITE_P(All, LocalPrinterHandlerDefaultTest, testing::Bool());
+// Testing class to cover `LocalPrinterHandlerDefault` handling using either a
+// local task runner or a service.  Makes no attempt to cover fallback when
+// using a service, which is handled separately by
+// `LocalPrinterHandlerDefaultTestFallback`
+class LocalPrinterHandlerDefaultTestProcess
+    : public LocalPrinterHandlerDefaultTestBase,
+      public testing::WithParamInterface<bool> {
+ public:
+  LocalPrinterHandlerDefaultTestProcess() = default;
+  LocalPrinterHandlerDefaultTestProcess(
+      const LocalPrinterHandlerDefaultTestProcess&) = delete;
+  LocalPrinterHandlerDefaultTestProcess& operator=(
+      const LocalPrinterHandlerDefaultTestProcess&) = delete;
+  ~LocalPrinterHandlerDefaultTestProcess() override = default;
+
+  bool UseService() override { return GetParam(); }
+  bool SupportFallback() override { return false; }
+};
+
+// Testing class to cover `LocalPrinterHandlerDefault` handling using only a
+// service, and to check different behavior for whether fallback is enabled.
+class LocalPrinterHandlerDefaultTestFallback
+    : public LocalPrinterHandlerDefaultTestBase {
+ public:
+  LocalPrinterHandlerDefaultTestFallback() = default;
+  LocalPrinterHandlerDefaultTestFallback(
+      const LocalPrinterHandlerDefaultTestFallback&) = delete;
+  LocalPrinterHandlerDefaultTestFallback& operator=(
+      const LocalPrinterHandlerDefaultTestFallback&) = delete;
+  ~LocalPrinterHandlerDefaultTestFallback() override = default;
+
+  bool UseService() override { return true; }
+  bool SupportFallback() override { return true; }
+};
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         LocalPrinterHandlerDefaultTestProcess,
+                         testing::Bool());
 
 // Tests that getting default printer is successful.
-TEST_P(LocalPrinterHandlerDefaultTest, GetDefaultPrinter) {
+TEST_P(LocalPrinterHandlerDefaultTestProcess, GetDefaultPrinter) {
   AddPrinter("printer1", "default1", "description1", /*is_default=*/true,
              /*requires_elevated_permissions=*/false);
   AddPrinter("printer2", "non-default2", "description2", /*is_default=*/false,
@@ -159,7 +245,7 @@
 
 // Tests that getting default printer gives empty string when no printers are
 // installed.
-TEST_P(LocalPrinterHandlerDefaultTest, GetDefaultPrinterNoneInstalled) {
+TEST_P(LocalPrinterHandlerDefaultTestProcess, GetDefaultPrinterNoneInstalled) {
   std::string default_printer = "dummy";
   local_printer_handler()->GetDefaultPrinter(
       base::BindOnce(&RecordGetDefaultPrinter, std::ref(default_printer)));
@@ -169,7 +255,7 @@
   EXPECT_TRUE(default_printer.empty());
 }
 
-TEST_P(LocalPrinterHandlerDefaultTest, GetPrinters) {
+TEST_P(LocalPrinterHandlerDefaultTestProcess, GetPrinters) {
   AddPrinter("printer1", "default1", "description1", /*is_default=*/true,
              /*requires_elevated_permissions=*/false);
   AddPrinter("printer2", "non-default2", "description2", /*is_default=*/false,
@@ -221,7 +307,7 @@
   EXPECT_EQ(*printers, expected_printers);
 }
 
-TEST_P(LocalPrinterHandlerDefaultTest, GetPrintersNoneRegistered) {
+TEST_P(LocalPrinterHandlerDefaultTestProcess, GetPrintersNoneRegistered) {
   size_t call_count = 0;
   std::unique_ptr<base::ListValue> printers;
   bool is_done = false;
@@ -241,24 +327,24 @@
 
 // Tests that fetching capabilities for an existing installed printer is
 // successful.
-TEST_P(LocalPrinterHandlerDefaultTest, StartGetCapabilityValidPrinter) {
+TEST_P(LocalPrinterHandlerDefaultTestProcess, StartGetCapabilityValidPrinter) {
   AddPrinter("printer1", "default1", "description1", /*is_default=*/true,
              /*requires_elevated_permissions=*/false);
 
-  base::Value fetched_caps;
+  base::Value fetched_caps("dummy");
   local_printer_handler()->StartGetCapability(
       "printer1", base::BindOnce(&RecordGetCapability, std::ref(fetched_caps)));
 
   RunUntilIdle();
 
-  ASSERT_TRUE(fetched_caps.is_dict());
-  EXPECT_TRUE(fetched_caps.FindKey(kSettingCapabilities));
-  EXPECT_TRUE(fetched_caps.FindKey(kPrinter));
+  EXPECT_TRUE(fetched_caps.FindDictKey(kSettingCapabilities));
+  EXPECT_TRUE(fetched_caps.FindDictKey(kPrinter));
 }
 
 // Tests that fetching capabilities bails early when the provided printer
 // can't be found.
-TEST_P(LocalPrinterHandlerDefaultTest, StartGetCapabilityInvalidPrinter) {
+TEST_P(LocalPrinterHandlerDefaultTestProcess,
+       StartGetCapabilityInvalidPrinter) {
   base::Value fetched_caps("dummy");
   local_printer_handler()->StartGetCapability(
       /*destination_id=*/"invalid printer",
@@ -271,7 +357,7 @@
 
 // Test that installed printers to which the user does not have permission to
 // access will fail to get any capabilities.
-TEST_P(LocalPrinterHandlerDefaultTest, StartGetCapabilityAccessDenied) {
+TEST_P(LocalPrinterHandlerDefaultTestProcess, StartGetCapabilityAccessDenied) {
   AddPrinter("printer1", "default1", "description1", /*is_default=*/true,
              /*requires_elevated_permissions=*/true);
 
@@ -285,4 +371,30 @@
   EXPECT_TRUE(fetched_caps.is_none());
 }
 
+// Tests that fetching capabilities can eventually succeed with fallback
+// processing when a printer requires elevated permissions.
+TEST_F(LocalPrinterHandlerDefaultTestFallback,
+       StartGetCapabilityElevatedPermissionsSucceeds) {
+  AddPrinter("printer1", "default1", "description1", /*is_default=*/true,
+             /*requires_elevated_permissions=*/true);
+
+  // Note that printer does not initially show as requiring elevated privileges.
+  EXPECT_FALSE(PrintBackendServiceManager::GetInstance()
+                   .PrinterDriverRequiresElevatedPrivilege("printer1"));
+
+  base::Value fetched_caps("dummy");
+  local_printer_handler()->StartGetCapability(
+      /*destination_id=*/"printer1",
+      base::BindOnce(&RecordGetCapability, std::ref(fetched_caps)));
+
+  RunUntilIdle();
+
+  EXPECT_TRUE(fetched_caps.FindDictKey(kSettingCapabilities));
+  EXPECT_TRUE(fetched_caps.FindDictKey(kPrinter));
+
+  // Verify that this printer now shows up as requiring elevated privileges.
+  EXPECT_TRUE(PrintBackendServiceManager::GetInstance()
+                  .PrinterDriverRequiresElevatedPrivilege("printer1"));
+}
+
 }  // namespace printing
diff --git a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.cc b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.cc
index cc9e4ba..962027d8 100644
--- a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.cc
@@ -36,14 +36,7 @@
 }  // namespace
 
 AccessibilityHandler::AccessibilityHandler(Profile* profile)
-    : profile_(profile) {
-  if (::switches::IsExperimentalAccessibilityDictationOfflineEnabled()) {
-    if (speech::SodaInstaller::GetInstance()->IsSodaInstalled())
-      OnSodaInstalled();
-    else
-      speech::SodaInstaller::GetInstance()->AddObserver(this);
-  }
-}
+    : profile_(profile) {}
 
 AccessibilityHandler::~AccessibilityHandler() {
   if (a11y_nav_buttons_toggle_metrics_reporter_timer_.IsRunning())
@@ -120,6 +113,8 @@
   FireWebUIListener(
       "initial-data-ready",
       base::Value(AccessibilityManager::Get()->GetStartupSoundEnabled()));
+
+  MaybeAddSodaInstallerObserver();
 }
 
 void AccessibilityHandler::HandleShowChromeVoxTutorial(
@@ -138,6 +133,15 @@
       chrome::FindBrowserWithWebContents(web_ui()->GetWebContents()));
 }
 
+void AccessibilityHandler::MaybeAddSodaInstallerObserver() {
+  if (::switches::IsExperimentalAccessibilityDictationOfflineEnabled()) {
+    if (speech::SodaInstaller::GetInstance()->IsSodaInstalled())
+      OnSodaInstalled();
+    else
+      speech::SodaInstaller::GetInstance()->AddObserver(this);
+  }
+}
+
 // SodaInstaller::Observer:
 void AccessibilityHandler::OnSodaInstalled() {
   speech::SodaInstaller::GetInstance()->RemoveObserver(this);
diff --git a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.h b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.h
index a0bef8eb..f778cfe3 100644
--- a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.h
+++ b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler.h
@@ -47,6 +47,8 @@
 
   void OpenExtensionOptionsPage(const char extension_id[]);
 
+  void MaybeAddSodaInstallerObserver();
+
   // SodaInstaller::Observer:
   void OnSodaInstalled() override;
   void OnSodaLanguagePackInstalled(
diff --git a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler_browsertest.cc b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler_browsertest.cc
index 8180941..2b958a3 100644
--- a/chrome/browser/ui/webui/settings/chromeos/accessibility_handler_browsertest.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/accessibility_handler_browsertest.cc
@@ -74,6 +74,8 @@
     return false;
   }
 
+  void AddSodaInstallerObserver() { handler_->MaybeAddSodaInstallerObserver(); }
+
   void OnSodaInstalled() { handler_->OnSodaInstalled(); }
 
   void OnSodaProgress(int progress) { handler_->OnSodaProgress(progress); }
@@ -123,6 +125,7 @@
 // the correct listener when SODA is installed.
 IN_PROC_BROWSER_TEST_F(AccessibilityHandlerTest, OnSodaInstalledNotification) {
   AssertWebUICalls(0);
+  AddSodaInstallerObserver();
   speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
   AssertWebUICalls(1);
   ASSERT_TRUE(WasWebUIListenerCalledWithStringArgument(
diff --git a/chrome/browser/web_applications/extensions/externally_managed_app_install_task_unittest.cc b/chrome/browser/web_applications/extensions/externally_managed_app_install_task_unittest.cc
index 0fea9ef1..8df08c03 100644
--- a/chrome/browser/web_applications/extensions/externally_managed_app_install_task_unittest.cc
+++ b/chrome/browser/web_applications/extensions/externally_managed_app_install_task_unittest.cc
@@ -1040,8 +1040,6 @@
   const GURL kWebAppUrl("https://foo.example");
   ExternalInstallOptions options(kWebAppUrl, DisplayMode::kStandalone,
                                  ExternalInstallSource::kSystemInstalled);
-  options.add_to_desktop = false;
-  options.add_to_quick_launch_bar = false;
   options.only_use_app_info_factory = true;
   options.app_info_factory = base::BindLambdaForTesting([&kWebAppUrl]() {
     auto info = std::make_unique<WebApplicationInfo>();
@@ -1051,9 +1049,6 @@
     return info;
   });
 
-  os_integration_manager()->SetNextCreateShortcutsResult(
-      finalizer()->GetAppIdForUrl(options.install_url), true);
-
   ExternallyManagedAppInstallTask task(
       profile(), /*url_loader=*/nullptr, registrar(), os_integration_manager(),
       ui_manager(), finalizer(), install_manager(), std::move(options));
@@ -1077,12 +1072,10 @@
 
         EXPECT_EQ(app_id.value(), id.value());
 
-        // Installing with an App Info does call into OS Integration Manager.
-        absl::optional<InstallOsHooksOptions> os_hooks_options =
-            os_integration_manager()->get_last_install_options();
-        ASSERT_TRUE(os_hooks_options);
-        EXPECT_FALSE(os_hooks_options->add_to_desktop);
-        EXPECT_FALSE(os_hooks_options->add_to_quick_launch_bar);
+        // Installing with an App Info doesn't call into OS Integration Manager.
+        // This might be an issue for default apps.
+        EXPECT_FALSE(
+            os_integration_manager()->get_last_install_options().has_value());
 
         EXPECT_EQ(0u, finalizer()->num_reparent_tab_calls());
 
diff --git a/chrome/browser/web_applications/web_app_install_task.cc b/chrome/browser/web_applications/web_app_install_task.cc
index 56688267..aa04ee8 100644
--- a/chrome/browser/web_applications/web_app_install_task.cc
+++ b/chrome/browser/web_applications/web_app_install_task.cc
@@ -239,7 +239,6 @@
 
   install_source_ = install_source;
   background_installation_ = true;
-  from_info_ = true;
 
   RecordInstallEvent();
 
@@ -249,22 +248,8 @@
 
   UpdateFinalizerClientData(install_params_, &options);
 
-  // If no install params have been set, default to not adding any shortcuts,
-  // overriding the default used in the install-from-manifest case, which is
-  // to add shortcuts.
-  if (!install_params_) {
-    install_params_ = InstallManager::InstallParams();
-    install_params_->add_to_applications_menu = false;
-    install_params_->add_to_desktop = false;
-    install_params_->add_to_quick_launch_bar = false;
-  }
-
-  install_callback_ = std::move(callback);
-  WebApplicationInfo web_application_info_copy = *web_application_info;
-  install_finalizer_->FinalizeInstall(
-      web_application_info_copy, options,
-      base::BindOnce(&WebAppInstallTask::OnInstallFinalizedCreateOsHooks,
-                     GetWeakPtr(), std::move(web_application_info)));
+  install_finalizer_->FinalizeInstall(*web_application_info, options,
+                                      std::move(callback));
 }
 
 void WebAppInstallTask::InstallWebAppWithParams(
@@ -398,9 +383,8 @@
 bool WebAppInstallTask::ShouldStopInstall() const {
   // Install should stop early if WebContents is being destroyed.
   // WebAppInstallTask::WebContentsDestroyed will get called eventually and
-  // the callback will be invoked at that point. Installs from info don't have a
-  // web contents, so this should always return false in that case.
-  return !from_info_ && (!web_contents() || web_contents()->IsBeingDestroyed());
+  // the callback will be invoked at that point.
+  return !web_contents() || web_contents()->IsBeingDestroyed();
 }
 
 void WebAppInstallTask::OnWebAppUrlLoadedGetWebApplicationInfo(
@@ -820,10 +804,10 @@
 
   install_finalizer_->FinalizeInstall(
       web_app_info_copy, finalize_options,
-      base::BindOnce(&WebAppInstallTask::OnInstallFinalizedCreateOsHooks,
+      base::BindOnce(&WebAppInstallTask::OnInstallFinalizedCreateShortcuts,
                      GetWeakPtr(), std::move(web_app_info)));
 
-  // Check that the finalizer hasn't called OnInstallFinalizedCreateOsHooks
+  // Check that the finalizer hasn't called OnInstallFinalizedCreateShortcuts
   // synchronously:
   DCHECK(install_callback_);
 }
@@ -836,7 +820,7 @@
   CallInstallCallback(app_id, code);
 }
 
-void WebAppInstallTask::OnInstallFinalizedCreateOsHooks(
+void WebAppInstallTask::OnInstallFinalizedCreateShortcuts(
     std::unique_ptr<WebApplicationInfo> web_app_info,
     const AppId& app_id,
     InstallResultCode code) {
@@ -863,8 +847,7 @@
   }
 
   // Only record the AppBanner stats for locally installed apps.
-  if (web_contents())
-    RecordAppBanner(web_contents(), web_app_info->start_url);
+  RecordAppBanner(web_contents(), web_app_info->start_url);
 
   InstallOsHooksOptions options;
 
diff --git a/chrome/browser/web_applications/web_app_install_task.h b/chrome/browser/web_applications/web_app_install_task.h
index ba5ba8c..fdd1b3c 100644
--- a/chrome/browser/web_applications/web_app_install_task.h
+++ b/chrome/browser/web_applications/web_app_install_task.h
@@ -242,7 +242,7 @@
                          bool user_accepted,
                          std::unique_ptr<WebApplicationInfo> web_app_info);
   void OnInstallFinalized(const AppId& app_id, InstallResultCode code);
-  void OnInstallFinalizedCreateOsHooks(
+  void OnInstallFinalizedCreateShortcuts(
       std::unique_ptr<WebApplicationInfo> web_app_info,
       const AppId& app_id,
       InstallResultCode code);
@@ -265,10 +265,6 @@
   absl::optional<AppId> expected_app_id_;
   bool background_installation_ = false;
 
-  // True when the install is initiated from InstallWebAppFromInfo. In this
-  // case, there will be no WebContents.
-  bool from_info_ = false;
-
   // The mechanism via which the app creation was triggered, will stay as
   // kNoInstallSource for updates.
   static constexpr webapps::WebappInstallSource kNoInstallSource =
@@ -285,6 +281,7 @@
   AppRegistrar* registrar_;
 
   base::WeakPtrFactory<WebAppInstallTask> weak_ptr_factory_{this};
+
 };
 
 }  // namespace web_app
diff --git a/chrome/browser/web_applications/web_app_install_task_unittest.cc b/chrome/browser/web_applications/web_app_install_task_unittest.cc
index ca7ec06..12b4bc6 100644
--- a/chrome/browser/web_applications/web_app_install_task_unittest.cc
+++ b/chrome/browser/web_applications/web_app_install_task_unittest.cc
@@ -837,6 +837,32 @@
       test_os_integration_manager().num_add_app_to_quick_launch_bar_calls());
 }
 
+TEST_F(WebAppInstallTaskTest, InstallWebAppFromManifest_Success) {
+  const GURL url = GURL("https://example.com/path");
+  const AppId app_id = GenerateAppIdFromURL(url);
+
+  auto manifest = std::make_unique<blink::Manifest>();
+  manifest->start_url = url;
+  manifest->short_name = u"Server Name";
+
+  data_retriever_->SetManifest(std::move(manifest), /*is_installable=*/true);
+
+  base::RunLoop run_loop;
+
+  install_task_->InstallWebAppFromManifest(
+      web_contents(), /*bypass_service_worker_check=*/false,
+      webapps::WebappInstallSource::MENU_BROWSER_TAB,
+      base::BindOnce(test::TestAcceptDialogCallback),
+      base::BindLambdaForTesting(
+          [&](const AppId& installed_app_id, InstallResultCode code) {
+            EXPECT_EQ(InstallResultCode::kSuccessNewInstall, code);
+            EXPECT_EQ(app_id, installed_app_id);
+            run_loop.Quit();
+          }));
+
+  run_loop.Run();
+}
+
 TEST_F(WebAppInstallTaskTest, InstallWebAppFromInfo_Success) {
   SetInstallFinalizerForTesting();
 
@@ -866,48 +892,6 @@
           }));
 
   run_loop.Run();
-
-  // With no install params set, OS hooks are run but no shortcuts are created.
-  EXPECT_EQ(0u, test_os_integration_manager().num_create_shortcuts_calls());
-  EXPECT_EQ(1u, test_os_integration_manager().num_create_file_handlers_calls());
-}
-
-TEST_F(WebAppInstallTaskTest, InstallWebAppFromInfo_WithInstallParams) {
-  SetInstallFinalizerForTesting();
-
-  const GURL url = GURL("https://example.com/path");
-  const AppId app_id = GenerateAppIdFromURL(url);
-
-  auto web_app_info = std::make_unique<WebApplicationInfo>();
-  web_app_info->start_url = url;
-  web_app_info->open_as_window = true;
-  web_app_info->title = u"App Name";
-
-  // Set install params that add shortcuts.
-  install_task_->SetInstallParams(InstallManager::InstallParams());
-
-  base::RunLoop run_loop;
-
-  install_task_->InstallWebAppFromInfo(
-      std::move(web_app_info), ForInstallableSite::kYes,
-      webapps::WebappInstallSource::MENU_BROWSER_TAB,
-      base::BindLambdaForTesting(
-          [&](const AppId& installed_app_id, InstallResultCode code) {
-            EXPECT_EQ(InstallResultCode::kSuccessNewInstall, code);
-            EXPECT_EQ(app_id, installed_app_id);
-
-            std::unique_ptr<WebApplicationInfo> final_web_app_info =
-                test_install_finalizer().web_app_info();
-            EXPECT_TRUE(final_web_app_info->open_as_window);
-
-            run_loop.Quit();
-          }));
-
-  run_loop.Run();
-
-  // OS hooks are run and shortcuts are created.
-  EXPECT_EQ(1u, test_os_integration_manager().num_create_shortcuts_calls());
-  EXPECT_EQ(1u, test_os_integration_manager().num_create_file_handlers_calls());
 }
 
 TEST_F(WebAppInstallTaskTest, InstallWebAppFromInfo_GenerateIcons) {
diff --git a/chrome/browser/webauthn/chrome_authenticator_request_delegate.cc b/chrome/browser/webauthn/chrome_authenticator_request_delegate.cc
index 40360f5..9c7d24b1 100644
--- a/chrome/browser/webauthn/chrome_authenticator_request_delegate.cc
+++ b/chrome/browser/webauthn/chrome_authenticator_request_delegate.cc
@@ -29,6 +29,7 @@
 #include "chrome/browser/ui/webauthn/authenticator_request_dialog.h"
 #include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
 #include "chrome/common/chrome_switches.h"
+#include "chrome/common/chrome_version.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/grit/generated_resources.h"
 #include "components/device_event_log/device_event_log.h"
@@ -234,20 +235,22 @@
 ChromeWebAuthenticationDelegate::TouchIdAuthenticatorConfig
 ChromeWebAuthenticationDelegate::TouchIdAuthenticatorConfigForProfile(
     Profile* profile) {
-  constexpr char kTouchIdKeychainAccessGroup[] =
-      "EQHXZ8M8AV.com.google.Chrome.webauthn";
-  PrefService* prefs = profile->GetPrefs();
+  constexpr char kKeychainAccessGroup[] =
+      MAC_TEAM_IDENTIFIER_STRING "." MAC_BUNDLE_IDENTIFIER_STRING ".webauthn";
+
   std::string metadata_secret =
-      prefs->GetString(kWebAuthnTouchIdMetadataSecretPrefName);
+      profile->GetPrefs()->GetString(kWebAuthnTouchIdMetadataSecretPrefName);
   if (metadata_secret.empty() ||
       !base::Base64Decode(metadata_secret, &metadata_secret)) {
     metadata_secret = device::fido::mac::GenerateCredentialMetadataSecret();
-    prefs->SetString(
+    profile->GetPrefs()->SetString(
         kWebAuthnTouchIdMetadataSecretPrefName,
         base::Base64Encode(base::as_bytes(base::make_span(metadata_secret))));
   }
-  return TouchIdAuthenticatorConfig{kTouchIdKeychainAccessGroup,
-                                    std::move(metadata_secret)};
+
+  return TouchIdAuthenticatorConfig{
+      .keychain_access_group = kKeychainAccessGroup,
+      .metadata_secret = std::move(metadata_secret)};
 }
 
 absl::optional<ChromeWebAuthenticationDelegate::TouchIdAuthenticatorConfig>
diff --git a/chrome/browser/window_placement/window_placement_permission_context.cc b/chrome/browser/window_placement/window_placement_permission_context.cc
index faf480e..f0623ec5b 100644
--- a/chrome/browser/window_placement/window_placement_permission_context.cc
+++ b/chrome/browser/window_placement/window_placement_permission_context.cc
@@ -15,7 +15,7 @@
     : permissions::PermissionContextBase(
           browser_context,
           ContentSettingsType::WINDOW_PLACEMENT,
-          blink::mojom::PermissionsPolicyFeature::kNotFound) {}
+          blink::mojom::PermissionsPolicyFeature::kWindowPlacement) {}
 
 WindowPlacementPermissionContext::~WindowPlacementPermissionContext() = default;
 
diff --git a/chrome/browser/window_placement/window_placement_permission_context_browsertest.cc b/chrome/browser/window_placement/window_placement_permission_context_browsertest.cc
index 9faa15ff..c119c1be 100644
--- a/chrome/browser/window_placement/window_placement_permission_context_browsertest.cc
+++ b/chrome/browser/window_placement/window_placement_permission_context_browsertest.cc
@@ -12,6 +12,8 @@
 #include "content/public/browser/web_contents.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/test/browser_test.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/default_handlers.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
 
 namespace {
@@ -31,12 +33,35 @@
         switches::kEnableBlinkFeatures, "WindowPlacement");
     InProcessBrowserTest::SetUpCommandLine(command_line);
   }
+
+  void SetUpOnMainThread() override {
+    // Support multiple sites on the test server.
+    host_resolver()->AddRule("*", "127.0.0.1");
+
+    // Window placement features are only available on secure contexts, and so
+    // we need to create an HTTPS test server here to serve those pages rather
+    // than using the default embedded_test_server().
+    https_test_server_ = std::make_unique<net::EmbeddedTestServer>(
+        net::EmbeddedTestServer::TYPE_HTTPS);
+    // Support sites like a.test, b.test, c.test etc
+    https_test_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
+    https_test_server_->ServeFilesFromSourceDirectory("chrome/test/data");
+    net::test_server::RegisterDefaultHandlers(https_test_server_.get());
+    content::SetupCrossSiteRedirector(https_test_server_.get());
+    ASSERT_TRUE(https_test_server_->Start());
+  }
+
+  net::EmbeddedTestServer* https_test_server() {
+    return https_test_server_.get();
+  }
+
+ protected:
+  std::unique_ptr<net::EmbeddedTestServer> https_test_server_;
 };
 
 // Tests user activation after dimissing and denying the permission request.
 IN_PROC_BROWSER_TEST_F(WindowPlacementPermissionContextTest, DismissAndDeny) {
-  ASSERT_TRUE(embedded_test_server()->Start());
-  const GURL url(embedded_test_server()->GetURL("/empty.html"));
+  const GURL url(https_test_server()->GetURL("a.test", "/empty.html"));
   EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
   auto* tab = browser()->tab_strip_model()->GetActiveWebContents();
   EXPECT_FALSE(tab->GetMainFrame()->HasTransientUserActivation());
@@ -61,8 +86,7 @@
 
 // Tests user activation after accepting the permission request.
 IN_PROC_BROWSER_TEST_F(WindowPlacementPermissionContextTest, Accept) {
-  ASSERT_TRUE(embedded_test_server()->Start());
-  const GURL url(embedded_test_server()->GetURL("/empty.html"));
+  const GURL url(https_test_server()->GetURL("a.test", "/empty.html"));
   EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
   auto* tab = browser()->tab_strip_model()->GetActiveWebContents();
   EXPECT_FALSE(tab->GetMainFrame()->HasTransientUserActivation());
@@ -78,4 +102,85 @@
   EXPECT_TRUE(tab->GetMainFrame()->HasTransientUserActivation());
 }
 
+IN_PROC_BROWSER_TEST_F(WindowPlacementPermissionContextTest,
+                       IFrameSameOriginAllow) {
+  const GURL url(https_test_server()->GetURL("a.test", "/iframe.html"));
+  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+  auto* tab = browser()->tab_strip_model()->GetActiveWebContents();
+
+  content::RenderFrameHost* child = ChildFrameAt(tab->GetMainFrame(), 0);
+  ASSERT_TRUE(child);
+  EXPECT_FALSE(tab->GetMainFrame()->HasTransientUserActivation());
+  EXPECT_FALSE(child->GetMainFrame()->HasTransientUserActivation());
+
+  permissions::PermissionRequestManager* permission_request_manager =
+      permissions::PermissionRequestManager::FromWebContents(tab);
+
+  permission_request_manager->set_auto_response_for_test(
+      permissions::PermissionRequestManager::ACCEPT_ALL);
+  EXPECT_EQ("granted", EvalJs(child, kGetScreens,
+                              content::EXECUTE_SCRIPT_NO_USER_GESTURE));
+  EXPECT_TRUE(tab->GetMainFrame()->HasTransientUserActivation());
+  EXPECT_TRUE(child->GetMainFrame()->HasTransientUserActivation());
+}
+
+IN_PROC_BROWSER_TEST_F(WindowPlacementPermissionContextTest,
+                       IFrameCrossOriginDeny) {
+  const GURL url(https_test_server()->GetURL("a.test", "/iframe.html"));
+  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+  auto* tab = browser()->tab_strip_model()->GetActiveWebContents();
+
+  GURL subframe_url(https_test_server()->GetURL("b.test", "/title1.html"));
+  content::NavigateIframeToURL(tab, /*iframe_id=*/"test", subframe_url);
+
+  content::RenderFrameHost* child = ChildFrameAt(tab->GetMainFrame(), 0);
+  ASSERT_TRUE(child);
+  EXPECT_FALSE(tab->GetMainFrame()->HasTransientUserActivation());
+  EXPECT_FALSE(child->GetMainFrame()->HasTransientUserActivation());
+
+  permissions::PermissionRequestManager* permission_request_manager =
+      permissions::PermissionRequestManager::FromWebContents(tab);
+
+  // PermissionRequestManager will accept any window placement permission
+  // dialogs that appear. However, the window-placement permission is not
+  // explicitly allowed on the iframe, so requests made by the child frame will
+  // be automatically denied before a prompt might be issued
+  permission_request_manager->set_auto_response_for_test(
+      permissions::PermissionRequestManager::ACCEPT_ALL);
+  EXPECT_EQ("denied", EvalJs(child, kGetScreens,
+                             content::EXECUTE_SCRIPT_NO_USER_GESTURE));
+  EXPECT_FALSE(tab->GetMainFrame()->HasTransientUserActivation());
+  EXPECT_FALSE(child->GetMainFrame()->HasTransientUserActivation());
+}
+
+IN_PROC_BROWSER_TEST_F(WindowPlacementPermissionContextTest,
+                       IFrameCrossOriginExplicitAllow) {
+  const GURL url(https_test_server()->GetURL("a.test", "/iframe.html"));
+  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+  auto* tab = browser()->tab_strip_model()->GetActiveWebContents();
+
+  // See https://w3c.github.io/webappsec-permissions-policy/ for more
+  // information on permissions policies and allowing cross-origin iframes
+  // to have particular permissions.
+  //
+  // TODO(enne): This code causes a user activation, so can't check that below
+  // like other tests.  Figure out why this is and try to clear it / address it.
+  EXPECT_TRUE(ExecJs(tab, R"(const frame = document.getElementById('test');
+    frame.setAttribute('allow', 'window-placement');)"));
+
+  GURL subframe_url(https_test_server()->GetURL("b.test", "/title1.html"));
+  content::NavigateIframeToURL(tab, /*iframe_id=*/"test", subframe_url);
+
+  content::RenderFrameHost* child = ChildFrameAt(tab->GetMainFrame(), 0);
+  ASSERT_TRUE(child);
+
+  permissions::PermissionRequestManager* permission_request_manager =
+      permissions::PermissionRequestManager::FromWebContents(tab);
+
+  permission_request_manager->set_auto_response_for_test(
+      permissions::PermissionRequestManager::ACCEPT_ALL);
+  EXPECT_EQ("granted", EvalJs(child, kGetScreens,
+                              content::EXECUTE_SCRIPT_NO_USER_GESTURE));
+}
+
 }  // namespace
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 82ba0b0f..b80b30b6 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-master-1621349817-1d50b69a5e4482ba6511550a3d6135cafd061111.profdata
+chrome-win32-master-1621382314-880123e5c4c4f5ae58f6b0ec6d73e103020a9443.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 986af74..ea18550 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-master-1621349817-519d8eae9c154f5a52e78d5c4f4b361cf53da7a0.profdata
+chrome-win64-master-1621382314-37d89922e41e14aa748bc033bf556a6b727eb5d1.profdata
diff --git a/chrome/chrome_cleaner/test/cleaner_test.cc b/chrome/chrome_cleaner/test/cleaner_test.cc
index 0b02d8f..8d0121c 100644
--- a/chrome/chrome_cleaner/test/cleaner_test.cc
+++ b/chrome/chrome_cleaner/test/cleaner_test.cc
@@ -603,6 +603,12 @@
   void SetUp() override {
     CleanerTestBase::SetUp();
 
+    // TODO(crbug.com/1210601): All uses of MockChromePromptResponder are
+    // failing on Windows 7. Disable this test suite until the problem can be
+    // investigated.
+    if (base::win::GetVersion() < base::win::Version::WIN8)
+      GTEST_SKIP() << "Skipping on Win7: crbug.com/1210601";
+
     command_line_ =
         BuildCommandLine(kCleanerExecutable, ExecutionMode::kScanning);
     chrome_cleaner::ChromePromptPipeHandles pipe_handles =
diff --git a/chrome/chrome_paks.gni b/chrome/chrome_paks.gni
index 1acffc9..941ee46 100644
--- a/chrome/chrome_paks.gni
+++ b/chrome/chrome_paks.gni
@@ -7,6 +7,7 @@
 import("//chrome/browser/buildflags.gni")
 import("//chrome/common/features.gni")
 import("//extensions/buildflags/buildflags.gni")
+import("//pdf/features.gni")
 import("//ui/base/ui_features.gni")
 import("chrome_repack_locales.gni")
 
@@ -296,7 +297,7 @@
         "//extensions:extensions_resources",
       ]
     }
-    if (enable_plugins) {
+    if (enable_pdf) {
       sources += [ "$root_gen_dir/chrome/pdf_resources.pak" ]
       deps += [ "//chrome/browser/resources/pdf:resources" ]
     }
diff --git a/chrome/common/BUILD.gn b/chrome/common/BUILD.gn
index 9738c237..195f082 100644
--- a/chrome/common/BUILD.gn
+++ b/chrome/common/BUILD.gn
@@ -246,6 +246,7 @@
     "//build:chromeos_buildflags",
     "//components/crash/core/app",
     "//components/google/core/common",
+    "//components/live_caption:constants",
     "//components/metrics:call_stack_profile_builder",
     "//components/no_state_prefetch/common",
     "//components/no_state_prefetch/common:mojo_bindings",
diff --git a/chrome/common/DEPS b/chrome/common/DEPS
index 699d95885..897ccf7 100644
--- a/chrome/common/DEPS
+++ b/chrome/common/DEPS
@@ -22,6 +22,7 @@
   "+components/flags_ui/flags_ui_switches.h",
   "+components/gcm_driver",
   "+components/google/core/common",
+  "+components/live_caption/pref_names.h",
   "+components/metrics/client_info.h",
   "+components/metrics/metadata_recorder.h",
   "+components/metrics/metrics_pref_names.h",
diff --git a/chrome/common/chrome_version.h.in b/chrome/common/chrome_version.h.in
index f4d88ff..0b2944f 100644
--- a/chrome/common/chrome_version.h.in
+++ b/chrome/common/chrome_version.h.in
@@ -22,3 +22,8 @@
 #define PRODUCT_SHORTNAME_STRING "@PRODUCT_SHORTNAME@"
 #define COPYRIGHT_STRING "@COPYRIGHT@"
 #define OFFICIAL_BUILD_STRING "@OFFICIAL_BUILD@"
+
+// Distribution Information
+
+#define MAC_BUNDLE_IDENTIFIER_STRING "@MAC_BUNDLE_ID@"
+#define MAC_TEAM_IDENTIFIER_STRING "@MAC_TEAM_ID@"
diff --git a/chrome/common/extensions/api/accessibility_private.json b/chrome/common/extensions/api/accessibility_private.json
index 28ce8df7..7f80235 100644
--- a/chrome/common/extensions/api/accessibility_private.json
+++ b/chrome/common/extensions/api/accessibility_private.json
@@ -80,7 +80,7 @@
         "id": "SwitchAccessMenuAction",
         "type": "string",
         "enum": [ "copy", "cut", "decrement", "dictation", "endTextSelection", "increment", "itemScan", "jumpToBeginningOfText", "jumpToEndOfText", "keyboard", "leftClick", "moveBackwardOneCharOfText", "moveBackwardOneWordOfText", "moveCursor", "moveDownOneLineOfText", "moveForwardOneCharOfText", "moveForwardOneWordOfText", "moveUpOneLineOfText", "paste", "pointScan", "rightClick", "scrollDown", "scrollLeft", "scrollRight", "scrollUp", "select", "settings", "startTextSelection" ],
-        "description": "Available actions to be shown in the Switch Access menu. Must be kept in sync with the strings in ash/system/accessibility/switch_access_menu_view.cc"
+        "description": "Available actions to be shown in the Switch Access menu. Must be kept in sync with the strings in ash/system/accessibility/switch_access/switch_access_menu_view.cc"
       },
       {
         "id": "SyntheticKeyboardEventType",
diff --git a/chrome/common/extensions/api/autotest_private.idl b/chrome/common/extensions/api/autotest_private.idl
index 9c74214..05dd159 100644
--- a/chrome/common/extensions/api/autotest_private.idl
+++ b/chrome/common/extensions/api/autotest_private.idl
@@ -1036,8 +1036,8 @@
 
     // Create mouse events to move a mouse cursor to the location. This can
     // cause a dragging if a button is pressed. It starts from the last mouse
-    // location. It does not support the move or drag across display boundaries.
-    // |location|: the target location (in display's coordinate).
+    // location.
+    // |location|: the target location (in screen coordinate).
     // |duration_in_ms|: the duration (in milliseconds) for the mouse movement.
     //    The mouse will move linearly. 0 means moving immediately.
     // |callback|: called after the mouse move finishes.
diff --git a/chrome/common/pref_names.cc b/chrome/common/pref_names.cc
index 438a68e31..c1b22016 100644
--- a/chrome/common/pref_names.cc
+++ b/chrome/common/pref_names.cc
@@ -1185,34 +1185,6 @@
 const char kDefaultBrowserSettingEnabled[] =
     "browser.default_browser_setting_enabled";
 
-// String indicating the size of the captions text as a percentage.
-const char kAccessibilityCaptionsTextSize[] =
-    "accessibility.captions.text_size";
-
-// String indicating the font of the captions text.
-const char kAccessibilityCaptionsTextFont[] =
-    "accessibility.captions.text_font";
-
-// Comma-separated string indicating the RGB values of the captions text color.
-const char kAccessibilityCaptionsTextColor[] =
-    "accessibility.captions.text_color";
-
-// Integer indicating the opacity of the captions text from 0 - 100.
-const char kAccessibilityCaptionsTextOpacity[] =
-    "accessibility.captions.text_opacity";
-
-// Comma-separated string indicating the RGB values of the background color.
-const char kAccessibilityCaptionsBackgroundColor[] =
-    "accessibility.captions.background_color";
-
-// CSS string indicating the shadow of the captions text.
-const char kAccessibilityCaptionsTextShadow[] =
-    "accessibility.captions.text_shadow";
-
-// Integer indicating the opacity of the captions text background from 0 - 100.
-const char kAccessibilityCaptionsBackgroundOpacity[] =
-    "accessibility.captions.background_opacity";
-
 // Boolean that indicates whether chrome://accessibility should show the
 // internal accessibility tree.
 const char kShowInternalAccessibilityTree[] =
@@ -3221,6 +3193,8 @@
 // Boolean pref indicating whether user has enabled rule-based discount in cart
 // module.
 const char kCartDiscountEnabled[] = "cart_discount_enabled";
+// Map pref recording the discounts used by users.
+const char kCartUsedDiscounts[] = "cart_used_discounts";
 #endif
 
 #if defined(OS_ANDROID)
diff --git a/chrome/common/pref_names.h b/chrome/common/pref_names.h
index d314fc4d..11a1620 100644
--- a/chrome/common/pref_names.h
+++ b/chrome/common/pref_names.h
@@ -186,13 +186,6 @@
 #if !BUILDFLAG(IS_CHROMEOS_ASH)
 extern const char kAccessibilityFocusHighlightEnabled[];
 #endif
-extern const char kAccessibilityCaptionsTextSize[];
-extern const char kAccessibilityCaptionsTextFont[];
-extern const char kAccessibilityCaptionsTextColor[];
-extern const char kAccessibilityCaptionsTextOpacity[];
-extern const char kAccessibilityCaptionsBackgroundColor[];
-extern const char kAccessibilityCaptionsTextShadow[];
-extern const char kAccessibilityCaptionsBackgroundOpacity[];
 #if !defined(OS_ANDROID)
 extern const char kLiveCaptionEnabled[];
 extern const char kLiveCaptionLanguageCode[];
@@ -1134,6 +1127,7 @@
 extern const char kCartModuleWelcomeSurfaceShownTimes[];
 extern const char kCartDiscountAcknowledged[];
 extern const char kCartDiscountEnabled[];
+extern const char kCartUsedDiscounts[];
 #endif
 
 #if defined(OS_ANDROID)
diff --git a/chrome/common/pref_names_util.cc b/chrome/common/pref_names_util.cc
index 98d62b0..9dbb829 100644
--- a/chrome/common/pref_names_util.cc
+++ b/chrome/common/pref_names_util.cc
@@ -10,6 +10,7 @@
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
 #include "chrome/common/pref_names.h"
+#include "components/live_caption/pref_names.h"
 #include "components/prefs/pref_service.h"
 #include "ui/native_theme/native_theme.h"
 
diff --git a/chrome/common/webui_url_constants.cc b/chrome/common/webui_url_constants.cc
index 46f8728..93e8eba 100644
--- a/chrome/common/webui_url_constants.cc
+++ b/chrome/common/webui_url_constants.cc
@@ -284,7 +284,8 @@
 const char kChromeUIPasswordChangeUrl[] = "chrome://password-change";
 const char kChromeUIPrintManagementUrl[] = "chrome://print-management";
 const char kChromeUIPowerHost[] = "power";
-const char kChromeUIProjectorHost[] = "projector";
+const char kChromeUIProjectorSelfieCamHost[] = "projector-selfie-cam";
+const char kChromeUIProjectorSelfieCamURL[] = "chrome://projector-selfie-cam/";
 const char kChromeUIScanningAppURL[] = "chrome://scanning";
 const char kChromeUIScreenlockIconHost[] = "screenlock-icon";
 const char kChromeUIScreenlockIconURL[] = "chrome://screenlock-icon/";
diff --git a/chrome/common/webui_url_constants.h b/chrome/common/webui_url_constants.h
index cbe9b3e..e2ef915 100644
--- a/chrome/common/webui_url_constants.h
+++ b/chrome/common/webui_url_constants.h
@@ -273,7 +273,8 @@
 extern const char kChromeUIPasswordChangeUrl[];
 extern const char kChromeUIPrintManagementUrl[];
 extern const char kChromeUIPowerHost[];
-extern const char kChromeUIProjectorHost[];
+extern const char kChromeUIProjectorSelfieCamHost[];
+extern const char kChromeUIProjectorSelfieCamURL[];
 extern const char kChromeUIScanningAppURL[];
 extern const char kChromeUIScreenlockIconHost[];
 extern const char kChromeUIScreenlockIconURL[];
diff --git a/chrome/renderer/BUILD.gn b/chrome/renderer/BUILD.gn
index 70c976d2..4dca660 100644
--- a/chrome/renderer/BUILD.gn
+++ b/chrome/renderer/BUILD.gn
@@ -11,6 +11,7 @@
 import("//components/spellcheck/spellcheck_build_features.gni")
 import("//extensions/buildflags/buildflags.gni")
 import("//media/media_options.gni")
+import("//pdf/features.gni")
 import("//ppapi/buildflags/buildflags.gni")
 import("//testing/libfuzzer/fuzzer_test.gni")
 import("//third_party/widevine/cdm/widevine.gni")
@@ -258,7 +259,6 @@
       "plugins/chrome_plugin_placeholder.h",
     ]
     deps += [
-      "//components/pdf/renderer",
       "//components/strings",
       "//media:media_buildflags",
       "//ppapi/host",
@@ -272,6 +272,10 @@
     }
   }
 
+  if (enable_pdf) {
+    deps += [ "//components/pdf/renderer" ]
+  }
+
   if (is_chromeos_ash) {
     deps += [ "//ash/constants" ]
   }
diff --git a/chrome/renderer/cart/DEPS b/chrome/renderer/cart/DEPS
index f2952a21..5a4b108 100644
--- a/chrome/renderer/cart/DEPS
+++ b/chrome/renderer/cart/DEPS
@@ -3,6 +3,7 @@
   "+chrome/browser/signin",
   "+chrome/browser",
   "+components/signin/public/identity_manager",
+  "+components/prefs",
 ]
 
 specific_include_rules = {
diff --git a/chrome/renderer/cart/commerce_hint_agent_browsertest.cc b/chrome/renderer/cart/commerce_hint_agent_browsertest.cc
index bfe85a0..25c1b68 100644
--- a/chrome/renderer/cart/commerce_hint_agent_browsertest.cc
+++ b/chrome/renderer/cart/commerce_hint_agent_browsertest.cc
@@ -12,9 +12,11 @@
 #include "chrome/browser/profiles/profile_manager.h"
 #include "chrome/browser/signin/identity_manager_factory.h"
 #include "chrome/common/chrome_features.h"
+#include "chrome/common/pref_names.h"
 #include "chrome/test/base/chrome_test_utils.h"
 #include "components/network_session_configurator/common/network_switches.h"
 #include "components/optimization_guide/core/optimization_guide_features.h"
+#include "components/prefs/pref_service.h"
 #include "components/search/ntp_features.h"
 #include "components/signin/public/identity_manager/identity_manager.h"
 #include "components/signin/public/identity_manager/identity_test_utils.h"
@@ -529,7 +531,9 @@
   void SetUpInProcessBrowserTestFixture() override {
     scoped_feature_list_.InitWithFeaturesAndParameters(
         {{ntp_features::kNtpChromeCartModule,
-          {{"partner-merchant-pattern", "(guitarcenter.com)"},
+          {{ntp_features::kNtpChromeCartModuleAbandonedCartDiscountParam,
+            "true"},
+           {"partner-merchant-pattern", "(guitarcenter.com)"},
            {"product-skip-pattern", "(^|\\W)(?i)(skipped)(\\W|$)"}}}},
         {optimization_guide::features::kOptimizationHints});
   }
@@ -589,4 +593,20 @@
   WaitForProductCount(expected_carts);
 }
 
+IN_PROC_BROWSER_TEST_F(CommerceHintProductInfoTest,
+                       RBDPartnerCartURLNotOverwrite) {
+  Profile* profile =
+      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
+  profile->GetPrefs()->SetBoolean(prefs::kCartDiscountEnabled, true);
+  EXPECT_TRUE(service_->IsCartDiscountEnabled());
+
+  NavigateToURL("https://www.guitarcenter.com/");
+  SendXHR("/add-to-cart", "product: 123");
+
+  WaitForCartCount(kExpectedExampleFallbackCart);
+  NavigateToURL("https://www.guitarcenter.com/cart.html");
+
+  WaitForCartCount(kExpectedExampleFallbackCart);
+}
+
 }  // namespace
diff --git a/chrome/renderer/pepper/chrome_renderer_pepper_host_factory.cc b/chrome/renderer/pepper/chrome_renderer_pepper_host_factory.cc
index 7820a55..60cfd89 100644
--- a/chrome/renderer/pepper/chrome_renderer_pepper_host_factory.cc
+++ b/chrome/renderer/pepper/chrome_renderer_pepper_host_factory.cc
@@ -10,6 +10,7 @@
 #include "chrome/renderer/pepper/pepper_uma_host.h"
 #include "components/pdf/renderer/pepper_pdf_host.h"
 #include "content/public/renderer/renderer_ppapi_host.h"
+#include "pdf/buildflags.h"
 #include "ppapi/host/ppapi_host.h"
 #include "ppapi/host/resource_host.h"
 #include "ppapi/proxy/ppapi_message_utils.h"
@@ -67,6 +68,7 @@
     }
   }
 
+#if BUILDFLAG(ENABLE_PDF)
   if (host_->GetPpapiHost()->permissions().HasPermission(
           ppapi::PERMISSION_PDF)) {
     switch (message.type()) {
@@ -75,6 +77,7 @@
       }
     }
   }
+#endif
 
   // Permissions for the following interfaces will be checked at the
   // time of the corresponding instance's method calls.  Currently these
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 0a39e03..5bb2775 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -1084,9 +1084,9 @@
       "../browser/accessibility/accessibility_labels_service_browsertest.cc",
       "../browser/accessibility/browser_accessibility_state_browsertest.cc",
       "../browser/accessibility/caption_controller_browsertest.cc",
-      "../browser/accessibility/caption_host_impl_browsertest.cc",
       "../browser/accessibility/image_annotation_browsertest.cc",
       "../browser/accessibility/interstitial_accessibility_browsertest.cc",
+      "../browser/accessibility/live_caption_speech_recognition_host_browsertest.cc",
       "../browser/apps/guest_view/app_view_browsertest.cc",
       "../browser/apps/guest_view/web_view_browsertest.cc",
       "../browser/apps/platform_apps/app_browsertest.cc",
@@ -3003,6 +3003,7 @@
         "../browser/ui/ash/keyboard/keyboard_end_to_end_browsertest.cc",
         "../browser/ui/ash/multi_user/test_multi_user_window_manager.cc",
         "../browser/ui/ash/multi_user/test_multi_user_window_manager.h",
+        "../browser/ui/ash/projector/projector_client_impl_browsertest.cc",
         "../browser/ui/ash/recording_service_browsertest.cc",
         "../browser/ui/ash/screen_orientation_delegate_chromeos_browsertest.cc",
         "../browser/ui/ash/sharesheet/sharesheet_bubble_view_browsertest.cc",
diff --git a/chrome/test/data/webui/cr_elements/cr_toolbar_focus_tests.js b/chrome/test/data/webui/cr_elements/cr_toolbar_focus_tests.js
index 082f6e5..3267de0 100644
--- a/chrome/test/data/webui/cr_elements/cr_toolbar_focus_tests.js
+++ b/chrome/test/data/webui/cr_elements/cr_toolbar_focus_tests.js
@@ -5,7 +5,7 @@
 /** @fileoverview Suite of tests for cr-toolbar. */
 
 // clang-format off
-import 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
+import {CrToolbarElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js';
 import {assertEquals, assertFalse, assertTrue} from '../chai_assert.js';
 // clang-format on
 
diff --git a/chrome/test/data/webui/cr_elements/cr_toolbar_search_field_tests.js b/chrome/test/data/webui/cr_elements/cr_toolbar_search_field_tests.js
index f8f0d686..b51f7ac98 100644
--- a/chrome/test/data/webui/cr_elements/cr_toolbar_search_field_tests.js
+++ b/chrome/test/data/webui/cr_elements/cr_toolbar_search_field_tests.js
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 
 // clang-format off
-import 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js';
+import {CrToolbarSearchFieldElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js';
 
 import {pressAndReleaseKeyOn} from 'chrome://resources/polymer/v3_0/iron-test-helpers/mock-interactions.js';
 import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
@@ -20,7 +20,7 @@
 
   /** @param {string} term */
   function simulateSearch(term) {
-    field.$$('#searchInput').value = term;
+    field.shadowRoot.querySelector('#searchInput').value = term;
     field.onSearchTermInput();
     field.onSearchTermSearch();
   }
@@ -59,10 +59,11 @@
     assertFalse(field.showingSearch);
     field.click();
     assertTrue(field.showingSearch);
-    const searchInput = /** @type {!HTMLElement} */ (field.$$('#searchInput'));
+    const searchInput = /** @type {!HTMLElement} */ (
+        field.shadowRoot.querySelector('#searchInput'));
     assertEquals(searchInput, field.root.activeElement);
 
-    field.$$('#searchInput').blur();
+    field.shadowRoot.querySelector('#searchInput').blur();
     assertFalse(field.showingSearch);
 
     field.click();
@@ -79,12 +80,14 @@
     flush();
     assertTrue(field.hasSearchText);
 
-    const clearSearch = field.$$('#clearSearch');
+    const clearSearch = field.shadowRoot.querySelector('#clearSearch');
     clearSearch.focus();
     clearSearch.click();
     assertTrue(field.showingSearch);
     assertEquals('', field.getValue());
-    assertEquals(field.$$('#searchInput'), field.root.activeElement);
+    assertEquals(
+        field.shadowRoot.querySelector('#searchInput'),
+        field.root.activeElement);
     assertFalse(field.hasSearchText);
     assertFalse(field.spinnerActive);
   });
@@ -95,7 +98,7 @@
     flush();
     assertEquals('query1', field.getValue());
 
-    field.$$('#clearSearch').click();
+    field.shadowRoot.querySelector('#clearSearch').click();
     assertTrue(field.showingSearch);
     assertEquals('', field.getValue());
 
@@ -193,7 +196,7 @@
   test('blur does not close field when a search is active', function() {
     field.click();
     simulateSearch('test');
-    field.$$('#searchInput').blur();
+    field.shadowRoot.querySelector('#searchInput').blur();
 
     assertTrue(field.showingSearch);
   });
@@ -207,13 +210,13 @@
     assertTrue(field.hasSearchText);
     flush();
 
-    const clearSearch = field.$$('#clearSearch');
+    const clearSearch = field.shadowRoot.querySelector('#clearSearch');
     assertFalse(clearSearch.hidden);
     assertTrue(field.showingSearch);
   });
 
   test('closes when value is cleared while unfocused', function() {
-    field.$$('#searchInput').focus();
+    field.shadowRoot.querySelector('#searchInput').focus();
     simulateSearch('test');
     flush();
 
@@ -224,7 +227,7 @@
 
     // Does close the field if it is blurred before being cleared.
     simulateSearch('test');
-    field.$$('#searchInput').blur();
+    field.shadowRoot.querySelector('#searchInput').blur();
     field.setValue('');
     assertFalse(field.showingSearch);
   });
diff --git a/chrome/test/data/webui/downloads/toolbar_tests.js b/chrome/test/data/webui/downloads/toolbar_tests.js
index b2ff8ab..f4caa70d 100644
--- a/chrome/test/data/webui/downloads/toolbar_tests.js
+++ b/chrome/test/data/webui/downloads/toolbar_tests.js
@@ -38,13 +38,15 @@
   });
 
   test('search starts spinner', function() {
-    toolbar.$.toolbar.fire('search-changed', 'a');
+    toolbar.$.toolbar.dispatchEvent(new CustomEvent(
+        'search-changed', {composed: true, bubbles: true, detail: 'a'}));
     assertTrue(toolbar.spinnerActive);
 
     // Pretend the manager got results and set this to false.
     toolbar.spinnerActive = false;
 
-    toolbar.$.toolbar.fire('search-changed', 'a ');  // Same term plus a space.
+    toolbar.$.toolbar.dispatchEvent(new CustomEvent(
+        'search-changed', {composed: true, bubbles: true, detail: 'a '}));
     assertFalse(toolbar.spinnerActive);
   });
 
diff --git a/chrome/test/data/webui/extensions/manager_test.js b/chrome/test/data/webui/extensions/manager_test.js
index 17955e3..cf8ac98 100644
--- a/chrome/test/data/webui/extensions/manager_test.js
+++ b/chrome/test/data/webui/extensions/manager_test.js
@@ -82,7 +82,10 @@
   });
 
   test(assert(extension_manager_tests.TestNames.ChangePages), function() {
-    manager.$$('extensions-toolbar').$$('cr-toolbar').$$('#menuButton').click();
+    manager.$$('extensions-toolbar')
+        .$$('cr-toolbar')
+        .shadowRoot.querySelector('#menuButton')
+        .click();
     flush();
 
     // We start on the item list.
diff --git a/chrome/test/data/webui/history/history_drawer_test.js b/chrome/test/data/webui/history/history_drawer_test.js
index cd659d0b9..e8e0bbfb 100644
--- a/chrome/test/data/webui/history/history_drawer_test.js
+++ b/chrome/test/data/webui/history/history_drawer_test.js
@@ -33,7 +33,9 @@
       // opened.
       assertFalse(!!drawerSideBar);
 
-      const menuButton = app.$.toolbar.$['main-toolbar'].$$('#menuButton');
+      const menuButton =
+          app.$.toolbar.$['main-toolbar'].shadowRoot.querySelector(
+              '#menuButton');
       assertTrue(!!menuButton);
 
       menuButton.click();
diff --git a/chrome/test/data/webui/history/history_toolbar_test.js b/chrome/test/data/webui/history/history_toolbar_test.js
index 41a1ac2..b0b9776e7 100644
--- a/chrome/test/data/webui/history/history_toolbar_test.js
+++ b/chrome/test/data/webui/history/history_toolbar_test.js
@@ -65,7 +65,8 @@
     testService.setQueryResult(
         {info: createHistoryInfo('Test'), value: TEST_HISTORY_RESULTS});
     toolbar.shadowRoot.querySelector('cr-toolbar')
-        .fire('search-changed', 'Test');
+        .dispatchEvent(new CustomEvent(
+            'search-changed', {bubbles: true, composed: true, detail: 'Test'}));
     return testService.whenCalled('queryHistory').then(query => {
       assertEquals('Test', query);
     });
@@ -79,7 +80,9 @@
       value: TEST_HISTORY_RESULTS,
     });
     toolbar.shadowRoot.querySelector('cr-toolbar')
-        .fire('search-changed', 'Test2');
+        .dispatchEvent(new CustomEvent(
+            'search-changed',
+            {bubbles: true, composed: true, detail: 'Test2'}));
     return testService.whenCalled('queryHistory')
         .then(flushTasks)
         .then(() => {
diff --git a/chrome/test/data/webui/settings/chromeos/cellular_networks_list_test.js b/chrome/test/data/webui/settings/chromeos/cellular_networks_list_test.js
index 90bf8d27..b9adc52 100644
--- a/chrome/test/data/webui/settings/chromeos/cellular_networks_list_test.js
+++ b/chrome/test/data/webui/settings/chromeos/cellular_networks_list_test.js
@@ -427,13 +427,16 @@
       inhibitReason: mojom.InhibitReason.kNotInhibited
     };
     addESimSlot();
-
+    cellularNetworkList.canShowSpinner = true;
     await flushAsync();
 
     const inhibitedSubtext = cellularNetworkList.$$('#inhibitedSubtext');
-    const inhibitedSpinner = cellularNetworkList.$$('#inhibitedSpinner');
+    const getInhibitedSpinner = () => {
+      return cellularNetworkList.$$('#inhibitedSpinner');
+    };
     assertTrue(inhibitedSubtext.hidden);
-    assertFalse(inhibitedSpinner.active);
+    assertTrue(!!getInhibitedSpinner());
+    assertFalse(getInhibitedSpinner().active);
 
     cellularNetworkList.cellularDeviceState = {
       type: mojom.NetworkType.kCellular,
@@ -443,6 +446,11 @@
     addESimSlot();
     await flushAsync();
     assertFalse(inhibitedSubtext.hidden);
-    assertTrue(inhibitedSpinner.active);
+    assertTrue(getInhibitedSpinner().active);
+
+    // Do not show inihibited spinner if cellular setup dialog is open.
+    cellularNetworkList.canShowSpinner = false;
+    await flushAsync();
+    assertFalse(!!getInhibitedSpinner());
   });
 });
diff --git a/chrome/test/data/webui/settings/settings_ui_tests.js b/chrome/test/data/webui/settings/settings_ui_tests.js
index 5469a55..bec8908 100644
--- a/chrome/test/data/webui/settings/settings_ui_tests.js
+++ b/chrome/test/data/webui/settings/settings_ui_tests.js
@@ -4,7 +4,7 @@
 
 // clang-format off
 import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
-import {CrSettingsPrefs, Router, routes} from 'chrome://settings/settings.js';
+import {CrSettingsPrefs, CrToolbarElement, CrToolbarSearchFieldElement, Router, routes} from 'chrome://settings/settings.js';
 
 import {assertEquals, assertFalse, assertTrue} from '../chai_assert.js';
 import {eventToPromise} from '../test_util.m.js';
diff --git a/chrome/updater/win/setup/setup.cc b/chrome/updater/win/setup/setup.cc
index 258dc211..b688945 100644
--- a/chrome/updater/win/setup/setup.cc
+++ b/chrome/updater/win/setup/setup.cc
@@ -176,6 +176,11 @@
   AddComInterfacesWorkItems(key, versioned_dir->Append(kUpdaterExe),
                             install_list.get());
 
+  if (scope == UpdaterScope::kSystem) {
+    AddComServiceWorkItems(versioned_dir->Append(kUpdaterExe),
+                           install_list.get());
+  }
+
   base::CommandLine run_updater_wake_command(
       versioned_dir->Append(kUpdaterExe));
   run_updater_wake_command.AppendSwitch(kWakeSwitch);
diff --git a/chromecast/browser/accessibility/touch_exploration_manager.cc b/chromecast/browser/accessibility/touch_exploration_manager.cc
index a7b12f9b..a66ca9cd 100644
--- a/chromecast/browser/accessibility/touch_exploration_manager.cc
+++ b/chromecast/browser/accessibility/touch_exploration_manager.cc
@@ -65,11 +65,10 @@
   // AccessibilityController::HandleAccessibilityGestore.)
   extensions::EventRouter* event_router = extensions::EventRouter::Get(
       shell::CastBrowserProcess::GetInstance()->browser_context());
-  std::unique_ptr<base::ListValue> event_args =
-      std::make_unique<base::ListValue>();
-  event_args->AppendString(ui::ToString(gesture));
-  event_args->AppendInteger(location.x());
-  event_args->AppendInteger(location.y());
+  std::vector<base::Value> event_args;
+  event_args.push_back(base::Value(ui::ToString(gesture)));
+  event_args.push_back(base::Value(location.x()));
+  event_args.push_back(base::Value(location.y()));
   std::unique_ptr<extensions::Event> event(new extensions::Event(
       extensions::events::ACCESSIBILITY_PRIVATE_ON_ACCESSIBILITY_GESTURE,
       extensions::cast::api::accessibility_private::OnAccessibilityGesture::
diff --git a/chromecast/browser/extensions/cast_extensions_browser_client.cc b/chromecast/browser/extensions/cast_extensions_browser_client.cc
index 7847aed..110fb91 100644
--- a/chromecast/browser/extensions/cast_extensions_browser_client.cc
+++ b/chromecast/browser/extensions/cast_extensions_browser_client.cc
@@ -237,7 +237,7 @@
   // Currently ignoring the dispatch_to_off_the_record_profiles attribute
   // as it is not necessary at the time
   std::unique_ptr<Event> event(
-      new Event(histogram_value, event_name, std::move(args)));
+      new Event(histogram_value, event_name, args->TakeList()));
   EventRouter::Get(browser_context_)->BroadcastEvent(std::move(event));
 }
 
diff --git a/chromecast/common/BUILD.gn b/chromecast/common/BUILD.gn
index 311e639..a5bb5e02 100644
--- a/chromecast/common/BUILD.gn
+++ b/chromecast/common/BUILD.gn
@@ -84,6 +84,7 @@
     "//chromecast/base:cast_version",
     "//chromecast/common/media",
     "//chromecast/common/mojom",
+    "//components/cast/common:constants",
     "//components/cdm/common:common",
     "//content/public/common",
     "//media:media_buildflags",
diff --git a/chromecast/common/DEPS b/chromecast/common/DEPS
index 3f1ef85..63ba4c4 100644
--- a/chromecast/common/DEPS
+++ b/chromecast/common/DEPS
@@ -1,5 +1,6 @@
 include_rules = [
   "+components/cdm/common",
+  "+components/cast/common",
   "+components/services/heap_profiling/public/cpp",
   "+components/url_matcher",
   "+components/version_info",
diff --git a/chromecast/common/cast_content_client.cc b/chromecast/common/cast_content_client.cc
index 05277ab..ea10192 100644
--- a/chromecast/common/cast_content_client.cc
+++ b/chromecast/common/cast_content_client.cc
@@ -19,6 +19,7 @@
 #include "chromecast/base/cast_paths.h"
 #include "chromecast/base/version.h"
 #include "chromecast/chromecast_buildflags.h"
+#include "components/cast/common/constants.h"
 #include "content/public/common/cdm_info.h"
 #include "content/public/common/user_agent.h"
 #include "media/base/media_switches.h"
@@ -173,8 +174,8 @@
           .c_str()
 #endif
       );
-  return content::BuildUserAgentFromOSAndProduct(os_info, product) +
-      " CrKey/" CAST_BUILD_REVISION;
+  return content::BuildUserAgentFromOSAndProduct(os_info, product) + " CrKey/" +
+         kFrozenCrKeyValue;
 }
 
 CastContentClient::~CastContentClient() {
diff --git a/chromeos/CHROMEOS_LKGM b/chromeos/CHROMEOS_LKGM
index 2e83fff..ad0a2c2 100644
--- a/chromeos/CHROMEOS_LKGM
+++ b/chromeos/CHROMEOS_LKGM
@@ -1 +1 @@
-13976.0.0
\ No newline at end of file
+13977.0.0
\ No newline at end of file
diff --git a/components/BUILD.gn b/components/BUILD.gn
index ee06de8..1b00597c0 100644
--- a/components/BUILD.gn
+++ b/components/BUILD.gn
@@ -11,7 +11,7 @@
 import("//components/ui_devtools/devtools.gni")
 import("//extensions/buildflags/buildflags.gni")
 import("//media/media_options.gni")
-import("//ppapi/buildflags/buildflags.gni")
+import("//pdf/features.gni")
 import("//printing/buildflags/buildflags.gni")
 import("//rlz/buildflags/buildflags.gni")
 import("//testing/test.gni")
@@ -517,7 +517,7 @@
     ]
   }
 
-  if (enable_plugins) {
+  if (enable_pdf) {
     deps += [ "//components/pdf/renderer:unit_tests" ]
   }
 
@@ -712,7 +712,7 @@
       data += [ "$root_out_dir/Content Shell.app/" ]
     }
 
-    if (enable_plugins) {
+    if (enable_pdf) {
       sources += [
         "pdf/browser/pdf_web_contents_helper_browsertest.cc",
         "pdf/renderer/pdf_accessibility_tree_browsertest.cc",
diff --git a/components/arc/session/connection_holder.h b/components/arc/session/connection_holder.h
index 542b19fb..19f33fe 100644
--- a/components/arc/session/connection_holder.h
+++ b/components/arc/session/connection_holder.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_ARC_SESSION_CONNECTION_HOLDER_H_
 
 #include <memory>
-#include <string>
 #include <type_traits>
 #include <utility>
 
diff --git a/components/assist_ranker/ranker_model_loader.h b/components/assist_ranker/ranker_model_loader.h
index 392828f..e55fa9d9 100644
--- a/components/assist_ranker/ranker_model_loader.h
+++ b/components/assist_ranker/ranker_model_loader.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_ASSIST_RANKER_RANKER_MODEL_LOADER_H_
 
 #include <memory>
-#include <string>
 
 #include "base/callback.h"
 #include "components/assist_ranker/ranker_model.h"
diff --git a/components/autofill/content/browser/webauthn/internal_authenticator_impl.h b/components/autofill/content/browser/webauthn/internal_authenticator_impl.h
index f8509782..9811f10 100644
--- a/components/autofill/content/browser/webauthn/internal_authenticator_impl.h
+++ b/components/autofill/content/browser/webauthn/internal_authenticator_impl.h
@@ -8,7 +8,6 @@
 #include <stdint.h>
 
 #include <memory>
-#include <string>
 
 #include "base/macros.h"
 #include "components/autofill/core/browser/payments/internal_authenticator.h"
diff --git a/components/autofill/core/browser/autofill_save_update_address_profile_delegate_ios.cc b/components/autofill/core/browser/autofill_save_update_address_profile_delegate_ios.cc
index 5536cc4..a4baf9f 100644
--- a/components/autofill/core/browser/autofill_save_update_address_profile_delegate_ios.cc
+++ b/components/autofill/core/browser/autofill_save_update_address_profile_delegate_ios.cc
@@ -98,6 +98,18 @@
       *GetProfile(), *GetOriginalProfile(), locale_);
 }
 
+bool AutofillSaveUpdateAddressProfileDelegateIOS::EditAccepted() {
+  RunSaveAddressProfilePromptCallback(
+      AutofillClient::SaveAddressProfileOfferUserDecision::kEditAccepted);
+  return true;
+}
+
+void AutofillSaveUpdateAddressProfileDelegateIOS::SetProfileRawInfo(
+    const ServerFieldType& type,
+    const std::u16string& data) {
+  profile_.SetRawInfo(type, data);
+}
+
 bool AutofillSaveUpdateAddressProfileDelegateIOS::Accept() {
   RunSaveAddressProfilePromptCallback(
       AutofillClient::SaveAddressProfileOfferUserDecision::kAccepted);
diff --git a/components/autofill/core/browser/autofill_save_update_address_profile_delegate_ios.h b/components/autofill/core/browser/autofill_save_update_address_profile_delegate_ios.h
index 35d9ad5..a360c41b0 100644
--- a/components/autofill/core/browser/autofill_save_update_address_profile_delegate_ios.h
+++ b/components/autofill/core/browser/autofill_save_update_address_profile_delegate_ios.h
@@ -59,6 +59,14 @@
   base::flat_map<ServerFieldType, std::pair<std::u16string, std::u16string>>
   GetProfileDiff() const;
 
+  // Calls |RunSaveAddressProfilePromptCallback| with the kEditAccepted|
+  // decision.
+  virtual bool EditAccepted();
+
+  // Updates |profile_| |type| value to |data|.
+  void SetProfileRawInfo(const ServerFieldType& type,
+                         const std::u16string& data);
+
   const autofill::AutofillProfile* GetProfile() const;
   const autofill::AutofillProfile* GetOriginalProfile() const;
   void set_modal_is_shown_to_true() { modal_is_shown_ = true; }
diff --git a/components/autofill/core/browser/browser_autofill_manager.cc b/components/autofill/core/browser/browser_autofill_manager.cc
index 8d3f0462..7f970d9 100644
--- a/components/autofill/core/browser/browser_autofill_manager.cc
+++ b/components/autofill/core/browser/browser_autofill_manager.cc
@@ -1479,7 +1479,8 @@
 
   FillCreditCardForm(credit_card_query_id_, credit_card_form_,
                      credit_card_field_, *credit_card, cvc);
-  if (credit_card->record_type() == CreditCard::FULL_SERVER_CARD) {
+  if (credit_card->record_type() == CreditCard::FULL_SERVER_CARD ||
+      credit_card->record_type() == CreditCard::VIRTUAL_CARD) {
     credit_card_access_manager_->CacheUnmaskedCardInfo(*credit_card, cvc);
   }
 }
diff --git a/components/autofill/core/browser/form_parsing/autofill_scanner.h b/components/autofill/core/browser/form_parsing/autofill_scanner.h
index 53179b4..52d90906 100644
--- a/components/autofill/core/browser/form_parsing/autofill_scanner.h
+++ b/components/autofill/core/browser/form_parsing/autofill_scanner.h
@@ -8,7 +8,6 @@
 #include <stddef.h>
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/autofill/core/browser/payments/autofill_wallet_model_type_controller.h b/components/autofill/core/browser/payments/autofill_wallet_model_type_controller.h
index 8a016e0..d1ba2b5b 100644
--- a/components/autofill/core/browser/payments/autofill_wallet_model_type_controller.h
+++ b/components/autofill/core/browser/payments/autofill_wallet_model_type_controller.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_AUTOFILL_CORE_BROWSER_PAYMENTS_AUTOFILL_WALLET_MODEL_TYPE_CONTROLLER_H_
 
 #include <memory>
-#include <string>
 
 #include "base/macros.h"
 #include "components/prefs/pref_change_registrar.h"
diff --git a/components/autofill/core/browser/payments/credit_card_access_manager.cc b/components/autofill/core/browser/payments/credit_card_access_manager.cc
index 9176197..30226283 100644
--- a/components/autofill/core/browser/payments/credit_card_access_manager.cc
+++ b/components/autofill/core/browser/payments/credit_card_access_manager.cc
@@ -45,6 +45,9 @@
 // Time to wait between multiple calls to GetUnmaskDetails().
 constexpr int64_t kDelayForGetUnmaskDetails = 3 * 60 * 1000;  // 3 min
 
+// Suffix for server IDs in the cache indicating that a card is a virtual card.
+const char kVirtualCardIdentifier[] = "_vcn";
+
 // Used for asynchronously waiting for |event| to be signaled.
 bool WaitForEvent(base::WaitableEvent* event) {
   event->declare_only_used_while_idle();
@@ -112,6 +115,20 @@
   return unmasked_card_cache_.empty();
 }
 
+std::vector<const CachedServerCardInfo*>
+CreditCardAccessManager::GetCachedUnmaskedCards() const {
+  std::vector<const CachedServerCardInfo*> unmasked_cards;
+  for (auto const& iter : unmasked_card_cache_) {
+    unmasked_cards.push_back(&iter.second);
+  }
+  return unmasked_cards;
+}
+
+bool CreditCardAccessManager::IsCardPresentInUnmaskedCache(
+    const std::string& server_id) const {
+  return unmasked_card_cache_.find(server_id) != unmasked_card_cache_.end();
+}
+
 bool CreditCardAccessManager::ServerCardsAvailable() {
   for (const CreditCard* credit_card : GetCreditCardsToSuggest()) {
     if (!IsLocalCard(credit_card))
@@ -259,14 +276,19 @@
   }
 
   // If card has been previously unmasked, use cached data.
+  std::string identifier = card->record_type() == CreditCard::VIRTUAL_CARD
+                               ? card->server_id() + kVirtualCardIdentifier
+                               : card->server_id();
   std::unordered_map<std::string, CachedServerCardInfo>::iterator it =
-      unmasked_card_cache_.find(card->server_id());
+      unmasked_card_cache_.find(identifier);
   if (it != unmasked_card_cache_.end()) {  // key is in cache
     accessor->OnCreditCardFetched(/*did_succeed=*/true,
                                   /*credit_card=*/&it->second.card,
                                   /*cvc=*/it->second.cvc);
-    base::UmaHistogramCounts1000("Autofill.UsedCachedServerCard",
-                                 ++it->second.cache_uses);
+    std::string metrics_name = card->record_type() == CreditCard::VIRTUAL_CARD
+                                   ? "Autofill.UsedCachedVirtualCard"
+                                   : "Autofill.UsedCachedServerCard";
+    base::UmaHistogramCounts1000(metrics_name, ++it->second.cache_uses);
     return;
   }
 
@@ -370,9 +392,13 @@
 
 void CreditCardAccessManager::CacheUnmaskedCardInfo(const CreditCard& card,
                                                     const std::u16string& cvc) {
-  DCHECK_EQ(card.record_type(), CreditCard::FULL_SERVER_CARD);
-  CachedServerCardInfo card_info = {card, cvc, 0};
-  unmasked_card_cache_[card.server_id()] = card_info;
+  DCHECK(card.record_type() == CreditCard::FULL_SERVER_CARD ||
+         card.record_type() == CreditCard::VIRTUAL_CARD);
+  std::string identifier = card.record_type() == CreditCard::VIRTUAL_CARD
+                               ? card.server_id() + kVirtualCardIdentifier
+                               : card.server_id();
+  CachedServerCardInfo card_info = {card, cvc, /*cache_uses=*/0};
+  unmasked_card_cache_[identifier] = card_info;
 }
 
 UnmaskAuthFlowType CreditCardAccessManager::GetAuthenticationType(
diff --git a/components/autofill/core/browser/payments/credit_card_access_manager.h b/components/autofill/core/browser/payments/credit_card_access_manager.h
index ca6add9..bd86db5 100644
--- a/components/autofill/core/browser/payments/credit_card_access_manager.h
+++ b/components/autofill/core/browser/payments/credit_card_access_manager.h
@@ -131,6 +131,14 @@
   // TODO(crbug/1069929): Add browsertests for this.
   void CacheUnmaskedCardInfo(const CreditCard& card, const std::u16string& cvc);
 
+  // Return the info for the server cards present in the
+  // |unamsked_cards_cache_|.
+  std::vector<const CachedServerCardInfo*> GetCachedUnmaskedCards() const;
+
+  // Returns true if a |unmasked_cards_cache| contains an entry for the card
+  // with |server_id|.
+  bool IsCardPresentInUnmaskedCache(const std::string& server_id) const;
+
   CreditCardCVCAuthenticator* GetOrCreateCVCAuthenticator();
 
 #if !defined(OS_IOS)
diff --git a/components/autofill/core/browser/payments/credit_card_access_manager_unittest.cc b/components/autofill/core/browser/payments/credit_card_access_manager_unittest.cc
index 7163b7e..b83c1b5 100644
--- a/components/autofill/core/browser/payments/credit_card_access_manager_unittest.cc
+++ b/components/autofill/core/browser/payments/credit_card_access_manager_unittest.cc
@@ -80,9 +80,13 @@
 namespace {
 
 const char kTestGUID[] = "00000000-0000-0000-0000-000000000001";
+const char kTestGUID2[] = "00000000-0000-0000-0000-000000000002";
 const char kTestNumber[] = "4234567890123456";  // Visa
+const char kTestNumber2[] = "5454545454545454";
 const char16_t kTestNumber16[] = u"4234567890123456";
 const char16_t kTestCvc16[] = u"123";
+const char kTestServerId[] = "server_id_1";
+const char kTestServerId2[] = "server_id_2";
 
 #if !defined(OS_IOS)
 const char kTestCvc[] = "123";
@@ -218,7 +222,8 @@
 
   void CreateServerCard(std::string guid,
                         std::string number = std::string(),
-                        bool masked = true) {
+                        bool masked = true,
+                        std::string server_id = std::string()) {
     CreditCard server_card = CreditCard();
     test::SetCreditCardInfo(&server_card, "Elvis Presley", number.c_str(),
                             test::NextMonth().c_str(), test::NextYear().c_str(),
@@ -226,7 +231,7 @@
     server_card.set_guid(guid);
     server_card.set_record_type(masked ? CreditCard::MASKED_SERVER_CARD
                                        : CreditCard::FULL_SERVER_CARD);
-
+    server_card.set_server_id(server_id);
     personal_data_manager_.AddServerCreditCard(server_card);
   }
 
@@ -1855,10 +1860,58 @@
   credit_card_access_manager_->FetchCreditCard(masked_card,
                                                accessor_->GetWeakPtr());
   histogram_tester.ExpectBucketCount("Autofill.UsedCachedServerCard", 1, 1);
-
   credit_card_access_manager_->FetchCreditCard(masked_card,
                                                accessor_->GetWeakPtr());
   histogram_tester.ExpectBucketCount("Autofill.UsedCachedServerCard", 2, 1);
+
+  // Create a virtual card.
+  CreditCard virtual_card = CreditCard();
+  test::SetCreditCardInfo(&virtual_card, "Elvis Presley", kTestNumber,
+                          test::NextMonth().c_str(), test::NextYear().c_str(),
+                          "1");
+  virtual_card.set_record_type(CreditCard::VIRTUAL_CARD);
+  credit_card_access_manager_->CacheUnmaskedCardInfo(virtual_card, kTestCvc16);
+
+  // Mocks that user selects the virtual card option of the masked card.
+  masked_card->set_record_type(CreditCard::VIRTUAL_CARD);
+  credit_card_access_manager_->FetchCreditCard(masked_card,
+                                               accessor_->GetWeakPtr());
+
+  histogram_tester.ExpectBucketCount("Autofill.UsedCachedVirtualCard", 1, 1);
+}
+
+TEST_F(CreditCardAccessManagerTest, GetCachedUnmaskedCards) {
+  // Assert that there are no cards cached initially.
+  EXPECT_EQ(0U, credit_card_access_manager_->GetCachedUnmaskedCards().size());
+
+  CreateServerCard(kTestGUID, kTestNumber, /*masked=*/false, kTestServerId);
+  CreateServerCard(kTestGUID2, kTestNumber2, /*masked=*/true, kTestServerId2);
+  // Add a card to the cache.
+  CreditCard* unmasked_card =
+      credit_card_access_manager_->GetCreditCard(kTestGUID);
+  credit_card_access_manager_->CacheUnmaskedCardInfo(*unmasked_card,
+                                                     kTestCvc16);
+
+  // Verify that only the card added to the cache is returned.
+  ASSERT_EQ(1U, credit_card_access_manager_->GetCachedUnmaskedCards().size());
+  EXPECT_EQ(*unmasked_card,
+            credit_card_access_manager_->GetCachedUnmaskedCards()[0]->card);
+}
+
+TEST_F(CreditCardAccessManagerTest, IsCardPresentInUnmaskedCache) {
+  CreateServerCard(kTestGUID, kTestNumber, /*masked=*/false, kTestServerId);
+  CreateServerCard(kTestGUID2, kTestNumber2, /*masked=*/true, kTestServerId2);
+  // Add a card to the cache.
+  CreditCard* unmasked_card =
+      credit_card_access_manager_->GetCreditCard(kTestGUID);
+  credit_card_access_manager_->CacheUnmaskedCardInfo(*unmasked_card,
+                                                     kTestCvc16);
+
+  // Verify that only one card is present in the cache.
+  EXPECT_TRUE(credit_card_access_manager_->IsCardPresentInUnmaskedCache(
+      unmasked_card->server_id()));
+  EXPECT_FALSE(credit_card_access_manager_->IsCardPresentInUnmaskedCache(
+      credit_card_access_manager_->GetCreditCard(kTestGUID2)->server_id()));
 }
 
 }  // namespace autofill
diff --git a/components/autofill/core/browser/strike_database_integrator_base.cc b/components/autofill/core/browser/strike_database_integrator_base.cc
index b7109648..6d63e2ee 100644
--- a/components/autofill/core/browser/strike_database_integrator_base.cc
+++ b/components/autofill/core/browser/strike_database_integrator_base.cc
@@ -140,6 +140,10 @@
   }
   std::vector<std::string> expired_keys;
   for (auto entry : strike_database_->GetStrikeCache()) {
+    // Only consider keys from the current strike database integrator.
+    if (strike_database_->GetPrefixFromKey(entry.first) != GetProjectPrefix()) {
+      continue;
+    }
     if (GetEntryAge(entry.second) > GetExpiryTimeDelta().value()) {
       if (strike_database_->GetStrikes(entry.first) > 0) {
         expired_keys.push_back(entry.first);
diff --git a/components/autofill/core/browser/strike_database_integrator_base.h b/components/autofill/core/browser/strike_database_integrator_base.h
index 81e9b5b..af0c194 100644
--- a/components/autofill/core/browser/strike_database_integrator_base.h
+++ b/components/autofill/core/browser/strike_database_integrator_base.h
@@ -95,19 +95,21 @@
   FRIEND_TEST_ALL_PREFIXES(ChromeBrowsingDataRemoverDelegateTest,
                            StrikeDatabaseEmptyOnAutofillRemoveEverything);
   FRIEND_TEST_ALL_PREFIXES(StrikeDatabaseIntegratorTestStrikeDatabaseTest,
-                           NonExpiringStrikesDoNotExpire);
-  FRIEND_TEST_ALL_PREFIXES(StrikeDatabaseIntegratorTestStrikeDatabaseTest,
-                           RemoveExpiredStrikesTest);
+                           ClearStrikesForKeys);
   FRIEND_TEST_ALL_PREFIXES(StrikeDatabaseIntegratorTestStrikeDatabaseTest,
                            GetKeyForStrikeDatabaseIntegratorUniqueIdTest);
   FRIEND_TEST_ALL_PREFIXES(StrikeDatabaseIntegratorTestStrikeDatabaseTest,
-                           RemoveExpiredStrikesUniqueIdTest);
+                           IdFromKey);
+  FRIEND_TEST_ALL_PREFIXES(StrikeDatabaseIntegratorTestStrikeDatabaseTest,
+                           NonExpiringStrikesDoNotExpire);
+  FRIEND_TEST_ALL_PREFIXES(StrikeDatabaseIntegratorTestStrikeDatabaseTest,
+                           RemoveExpiredStrikesOnlyConsidersCurrentIntegrator);
+  FRIEND_TEST_ALL_PREFIXES(StrikeDatabaseIntegratorTestStrikeDatabaseTest,
+                           RemoveExpiredStrikesTest);
   FRIEND_TEST_ALL_PREFIXES(StrikeDatabaseIntegratorTestStrikeDatabaseTest,
                            RemoveExpiredStrikesTestLogsUMA);
   FRIEND_TEST_ALL_PREFIXES(StrikeDatabaseIntegratorTestStrikeDatabaseTest,
-                           IdFromKey);
-  FRIEND_TEST_ALL_PREFIXES(StrikeDatabaseIntegratorTestStrikeDatabaseTest,
-                           ClearStrikesForKeys);
+                           RemoveExpiredStrikesUniqueIdTest);
   friend class SaveCardInfobarEGTestHelper;
   friend class StrikeDatabaseTest;
   friend class StrikeDatabaseTester;
diff --git a/components/autofill/core/browser/strike_database_integrator_test_strike_database.cc b/components/autofill/core/browser/strike_database_integrator_test_strike_database.cc
index 80dd5d33..a3e5b90 100644
--- a/components/autofill/core/browser/strike_database_integrator_test_strike_database.cc
+++ b/components/autofill/core/browser/strike_database_integrator_test_strike_database.cc
@@ -8,7 +8,6 @@
 
 namespace autofill {
 
-const char kProjectPrefix[] = "StrikeDatabaseIntegratorTest";
 const int kMaxStrikesLimit = 6;
 
 StrikeDatabaseIntegratorTestStrikeDatabase::
@@ -26,11 +25,21 @@
 }
 
 StrikeDatabaseIntegratorTestStrikeDatabase::
+    StrikeDatabaseIntegratorTestStrikeDatabase(
+        StrikeDatabase* strike_database,
+        absl::optional<base::TimeDelta> expiry_time_delta,
+        std::string& project_prefix)
+    : StrikeDatabaseIntegratorTestStrikeDatabase(strike_database,
+                                                 expiry_time_delta) {
+  project_prefix_ = project_prefix;
+}
+
+StrikeDatabaseIntegratorTestStrikeDatabase::
     ~StrikeDatabaseIntegratorTestStrikeDatabase() = default;
 
 std::string StrikeDatabaseIntegratorTestStrikeDatabase::GetProjectPrefix()
     const {
-  return kProjectPrefix;
+  return project_prefix_;
 }
 
 int StrikeDatabaseIntegratorTestStrikeDatabase::GetMaxStrikesLimit() const {
diff --git a/components/autofill/core/browser/strike_database_integrator_test_strike_database.h b/components/autofill/core/browser/strike_database_integrator_test_strike_database.h
index 1c132af..33053e76 100644
--- a/components/autofill/core/browser/strike_database_integrator_test_strike_database.h
+++ b/components/autofill/core/browser/strike_database_integrator_test_strike_database.h
@@ -23,6 +23,12 @@
       absl::optional<base::TimeDelta> expiry_time_delta);
   explicit StrikeDatabaseIntegratorTestStrikeDatabase(
       StrikeDatabase* strike_database);
+  // This constructor initializes the TestStrikeDatabase with a non-default
+  // project prefix.
+  StrikeDatabaseIntegratorTestStrikeDatabase(
+      StrikeDatabase* strike_database,
+      absl::optional<base::TimeDelta> expiry_time_delta,
+      std::string& project_prefix);
   ~StrikeDatabaseIntegratorTestStrikeDatabase() override;
 
   absl::optional<size_t> GetMaximumEntries() const override;
@@ -42,6 +48,7 @@
 
   absl::optional<size_t> maximum_entries_ = 10;
   absl::optional<size_t> maximum_entries_after_cleanup_ = 5;
+  std::string project_prefix_ = "StrikeDatabaseIntegratorTest";
 };
 
 }  // namespace autofill
diff --git a/components/autofill/core/browser/strike_database_integrator_test_strike_database_unittest.cc b/components/autofill/core/browser/strike_database_integrator_test_strike_database_unittest.cc
index 8cb5f99..5182966 100644
--- a/components/autofill/core/browser/strike_database_integrator_test_strike_database_unittest.cc
+++ b/components/autofill/core/browser/strike_database_integrator_test_strike_database_unittest.cc
@@ -206,6 +206,38 @@
       11, 1);
 }
 
+// This test verifies correctness of http://crbug/1206176.
+TEST_F(StrikeDatabaseIntegratorTestStrikeDatabaseTest,
+       RemoveExpiredStrikesOnlyConsidersCurrentIntegrator) {
+  autofill::TestAutofillClock test_clock;
+  test_clock.SetNow(AutofillClock::Now());
+  // Create a second test integrator, but with a different project prefix name,
+  // and whose strikes explicitly do not expire.
+  std::string other_project_prefix = "DifferentProjectPrefix";
+  std::unique_ptr<StrikeDatabaseIntegratorTestStrikeDatabase>
+      other_strike_database =
+          std::make_unique<StrikeDatabaseIntegratorTestStrikeDatabase>(
+              strike_database_service_.get(),
+              /*expiry_time_micros=*/absl::nullopt, other_project_prefix);
+
+  // Add a strike to both integrators.
+  strike_database_->AddStrike();
+  EXPECT_EQ(1, strike_database_->GetStrikes());
+  other_strike_database->AddStrike();
+  EXPECT_EQ(1, other_strike_database->GetStrikes());
+
+  // Advance clock to past expiry time for |strike_database_|.
+  test_clock.Advance(strike_database_->GetExpiryTimeDelta().value() +
+                     base::TimeDelta::FromMicroseconds(1));
+
+  // Attempt to expire strikes. Only |strike_database_|'s keys should be
+  // affected.
+  strike_database_->RemoveExpiredStrikes();
+  other_strike_database->RemoveExpiredStrikes();
+  EXPECT_EQ(0, strike_database_->GetStrikes());
+  EXPECT_EQ(1, other_strike_database->GetStrikes());
+}
+
 TEST_F(StrikeDatabaseIntegratorTestStrikeDatabaseTest,
        GetKeyForStrikeDatabaseIntegratorUniqueIdTest) {
   strike_database_->SetUniqueIdsRequired(true);
diff --git a/components/autofill/core/common/autofill_payments_features.cc b/components/autofill/core/common/autofill_payments_features.cc
index 340ca9b..bc82b4f 100644
--- a/components/autofill/core/common/autofill_payments_features.cc
+++ b/components/autofill/core/common/autofill_payments_features.cc
@@ -131,6 +131,12 @@
 const base::Feature kAutofillSaveCardInfobarEditSupport{
     "AutofillSaveCardInfobarEditSupport", base::FEATURE_ENABLED_BY_DEFAULT};
 
+// When enabled, the entire PAN and the CVC details of the unmasked cached card
+// will be shown in the manual filling view.
+const base::Feature kAutofillShowUnmaskedCachedCardInManualFillingView{
+    "AutofillShowUnmaskedCachedCardInManualFillingView",
+    base::FEATURE_DISABLED_BY_DEFAULT};
+
 // When enabled, suggestions with offers will be shown at the top.
 const base::Feature kAutofillSortSuggestionsBasedOnOfferPresence{
     "AutofillSortSuggestionsBasedOnOfferPresence",
diff --git a/components/autofill/core/common/autofill_payments_features.h b/components/autofill/core/common/autofill_payments_features.h
index e8bbfb93..e21a996a 100644
--- a/components/autofill/core/common/autofill_payments_features.h
+++ b/components/autofill/core/common/autofill_payments_features.h
@@ -36,6 +36,7 @@
 extern const base::Feature kAutofillParseMerchantPromoCodeFields;
 extern const base::Feature kAutofillSaveCardDismissOnNavigation;
 extern const base::Feature kAutofillSaveCardInfobarEditSupport;
+extern const base::Feature kAutofillShowUnmaskedCachedCardInManualFillingView;
 extern const base::Feature kAutofillSortSuggestionsBasedOnOfferPresence;
 extern const base::Feature kAutofillSuggestVirtualCardsOnlyOnFullFormDetection;
 extern const base::Feature kAutofillSuppressCreditCardSaveForAssistant;
diff --git a/components/autofill_assistant/browser/actions/stopwatch.h b/components/autofill_assistant/browser/actions/stopwatch.h
index cd399cc..64f7fd2a 100644
--- a/components/autofill_assistant/browser/actions/stopwatch.h
+++ b/components/autofill_assistant/browser/actions/stopwatch.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_ACTIONS_STOPWATCH_H_
 
 #include <ostream>
-#include <string>
 
 #include "base/time/time.h"
 
diff --git a/components/autofill_assistant/browser/actions/use_address_action.h b/components/autofill_assistant/browser/actions/use_address_action.h
index f818308..2c145c1 100644
--- a/components/autofill_assistant/browser/actions/use_address_action.h
+++ b/components/autofill_assistant/browser/actions/use_address_action.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_ACTIONS_USE_ADDRESS_ACTION_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/autofill_assistant/browser/element_area.h b/components/autofill_assistant/browser/element_area.h
index 01c78e1..2652fa0 100644
--- a/components/autofill_assistant/browser/element_area.h
+++ b/components/autofill_assistant/browser/element_area.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_ELEMENT_AREA_H_
 #define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_ELEMENT_AREA_H_
 
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/blocklist/opt_out_blocklist/opt_out_blocklist_item.h b/components/blocklist/opt_out_blocklist/opt_out_blocklist_item.h
index 303709b..158260d 100644
--- a/components/blocklist/opt_out_blocklist/opt_out_blocklist_item.h
+++ b/components/blocklist/opt_out_blocklist/opt_out_blocklist_item.h
@@ -10,7 +10,6 @@
 #include <map>
 #include <memory>
 #include <queue>
-#include <string>
 
 #include "base/callback.h"
 #include "base/macros.h"
diff --git a/components/browsing_data/content/browsing_data_helper.cc b/components/browsing_data/content/browsing_data_helper.cc
index 9431308..dcb4dc62 100644
--- a/components/browsing_data/content/browsing_data_helper.cc
+++ b/components/browsing_data/content/browsing_data_helper.cc
@@ -81,6 +81,7 @@
 
 void RemoveSiteIsolationData(PrefService* prefs) {
   prefs->ClearPref(site_isolation::prefs::kUserTriggeredIsolatedOrigins);
+  prefs->ClearPref(site_isolation::prefs::kWebTriggeredIsolatedOrigins);
   // Note that this does not clear these sites from the in-memory map in
   // ChildProcessSecurityPolicy, since that is not supported at runtime. That
   // list of isolated sites is not directly exposed to users, though, and
diff --git a/components/browsing_data/content/cookie_helper.h b/components/browsing_data/content/cookie_helper.h
index 99bd6da1..799e975 100644
--- a/components/browsing_data/content/cookie_helper.h
+++ b/components/browsing_data/content/cookie_helper.h
@@ -8,7 +8,6 @@
 #include <stddef.h>
 
 #include <map>
-#include <string>
 
 #include "base/callback.h"
 #include "base/callback_forward.h"
diff --git a/components/browsing_data/content/database_helper.h b/components/browsing_data/content/database_helper.h
index 25c7983..d3f05a0 100644
--- a/components/browsing_data/content/database_helper.h
+++ b/components/browsing_data/content/database_helper.h
@@ -10,7 +10,6 @@
 
 #include <list>
 #include <set>
-#include <string>
 
 #include "base/callback_forward.h"
 #include "base/macros.h"
diff --git a/components/browsing_data/content/indexed_db_helper.h b/components/browsing_data/content/indexed_db_helper.h
index 428c6fa..86820bb0 100644
--- a/components/browsing_data/content/indexed_db_helper.h
+++ b/components/browsing_data/content/indexed_db_helper.h
@@ -9,7 +9,6 @@
 
 #include <list>
 #include <set>
-#include <string>
 
 #include "base/callback.h"
 #include "base/macros.h"
diff --git a/components/browsing_data/core/counters/browsing_data_counter.h b/components/browsing_data/core/counters/browsing_data_counter.h
index e809b0074..9d8aa95e 100644
--- a/components/browsing_data/core/counters/browsing_data_counter.h
+++ b/components/browsing_data/core/counters/browsing_data_counter.h
@@ -8,7 +8,6 @@
 #include <stdint.h>
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/cast/common/BUILD.gn b/components/cast/common/BUILD.gn
new file mode 100644
index 0000000..4eef367
--- /dev/null
+++ b/components/cast/common/BUILD.gn
@@ -0,0 +1,10 @@
+# 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.
+
+source_set("constants") {
+  sources = [
+    "constants.cc",
+    "constants.h",
+  ]
+}
diff --git a/components/cast/common/constants.cc b/components/cast/common/constants.cc
new file mode 100644
index 0000000..c662d65
--- /dev/null
+++ b/components/cast/common/constants.cc
@@ -0,0 +1,11 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/cast/common/constants.h"
+
+namespace chromecast {
+
+const char kFrozenCrKeyValue[] = "1.56.500000";
+
+}  // namespace chromecast
diff --git a/components/cast/common/constants.h b/components/cast/common/constants.h
new file mode 100644
index 0000000..affcdc2
--- /dev/null
+++ b/components/cast/common/constants.h
@@ -0,0 +1,15 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_CAST_COMMON_CONSTANTS_H_
+#define COMPONENTS_CAST_COMMON_CONSTANTS_H_
+
+namespace chromecast {
+
+// See internal b/187823385 for details.
+extern const char kFrozenCrKeyValue[];
+
+}  // namespace chromecast
+
+#endif  // COMPONENTS_CAST_COMMON_CONSTANTS_H_
diff --git a/components/cast_channel/cast_socket.h b/components/cast_channel/cast_socket.h
index fd3ad68d..bdf05e2 100644
--- a/components/cast_channel/cast_socket.h
+++ b/components/cast_channel/cast_socket.h
@@ -8,7 +8,6 @@
 #include <stdint.h>
 
 #include <queue>
-#include <string>
 
 #include "base/cancelable_callback.h"
 #include "base/gtest_prod_util.h"
diff --git a/components/cast_channel/logger.h b/components/cast_channel/logger.h
index f4d91e8..a4b9c8e8 100644
--- a/components/cast_channel/logger.h
+++ b/components/cast_channel/logger.h
@@ -9,7 +9,6 @@
 
 #include <map>
 #include <memory>
-#include <string>
 
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
diff --git a/components/client_hints/browser/client_hints.h b/components/client_hints/browser/client_hints.h
index 92fe05e..57d192d 100644
--- a/components/client_hints/browser/client_hints.h
+++ b/components/client_hints/browser/client_hints.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_CLIENT_HINTS_BROWSER_CLIENT_HINTS_H_
 
 #include <memory>
-#include <string>
 
 #include "base/memory/ref_counted.h"
 #include "components/keyed_service/core/keyed_service.h"
diff --git a/components/content_settings/core/browser/content_settings_origin_identifier_value_map.h b/components/content_settings/core/browser/content_settings_origin_identifier_value_map.h
index 515f849db57..0aa7660f 100644
--- a/components/content_settings/core/browser/content_settings_origin_identifier_value_map.h
+++ b/components/content_settings/core/browser/content_settings_origin_identifier_value_map.h
@@ -9,7 +9,6 @@
 
 #include <map>
 #include <memory>
-#include <string>
 
 #include "base/macros.h"
 #include "base/time/time.h"
diff --git a/components/content_settings/core/browser/content_settings_provider.h b/components/content_settings/core/browser/content_settings_provider.h
index a87df22..744c6f1d 100644
--- a/components/content_settings/core/browser/content_settings_provider.h
+++ b/components/content_settings/core/browser/content_settings_provider.h
@@ -8,7 +8,6 @@
 #define COMPONENTS_CONTENT_SETTINGS_CORE_BROWSER_CONTENT_SETTINGS_PROVIDER_H_
 
 #include <memory>
-#include <string>
 
 #include "base/values.h"
 #include "components/content_settings/core/browser/content_settings_rule.h"
diff --git a/components/cronet/android/cronet_bidirectional_stream_adapter.h b/components/cronet/android/cronet_bidirectional_stream_adapter.h
index e76db766..7934fb2 100644
--- a/components/cronet/android/cronet_bidirectional_stream_adapter.h
+++ b/components/cronet/android/cronet_bidirectional_stream_adapter.h
@@ -8,7 +8,6 @@
 #include <jni.h>
 
 #include <memory>
-#include <string>
 
 #include "base/android/jni_android.h"
 #include "base/android/jni_array.h"
diff --git a/components/cronet/android/cronet_url_request_context_adapter.h b/components/cronet/android/cronet_url_request_context_adapter.h
index 18d93f1..311278ae 100644
--- a/components/cronet/android/cronet_url_request_context_adapter.h
+++ b/components/cronet/android/cronet_url_request_context_adapter.h
@@ -9,7 +9,6 @@
 #include <stdint.h>
 
 #include <memory>
-#include <string>
 
 #include "base/android/scoped_java_ref.h"
 #include "base/callback.h"
diff --git a/components/cronet/android/test/url_request_intercepting_job_factory.h b/components/cronet/android/test/url_request_intercepting_job_factory.h
index 9a46ed96..8cdc925 100644
--- a/components/cronet/android/test/url_request_intercepting_job_factory.h
+++ b/components/cronet/android/test/url_request_intercepting_job_factory.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_CRONET_ANDROID_TEST_URL_REQUEST_INTERCEPTING_JOB_FACTORY_H_
 
 #include <memory>
-#include <string>
 
 #include "base/compiler_specific.h"
 #include "base/macros.h"
diff --git a/components/cronet/native/upload_data_sink.h b/components/cronet/native/upload_data_sink.h
index 3a893b6..d233761a 100644
--- a/components/cronet/native/upload_data_sink.h
+++ b/components/cronet/native/upload_data_sink.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_CRONET_NATIVE_UPLOAD_DATA_SINK_H_
 
 #include <memory>
-#include <string>
 
 #include "base/macros.h"
 #include "base/synchronization/lock.h"
diff --git a/components/data_reduction_proxy/core/browser/data_reduction_proxy_settings.h b/components/data_reduction_proxy/core/browser/data_reduction_proxy_settings.h
index f505cd3..ab60b27 100644
--- a/components/data_reduction_proxy/core/browser/data_reduction_proxy_settings.h
+++ b/components/data_reduction_proxy/core/browser/data_reduction_proxy_settings.h
@@ -8,7 +8,6 @@
 #include <stdint.h>
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/dom_distiller/core/dom_distiller_service.h b/components/dom_distiller/core/dom_distiller_service.h
index 97d7674..121bf8d 100644
--- a/components/dom_distiller/core/dom_distiller_service.h
+++ b/components/dom_distiller/core/dom_distiller_service.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_DOM_DISTILLER_CORE_DOM_DISTILLER_SERVICE_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/download/internal/background_service/entry_utils.h b/components/download/internal/background_service/entry_utils.h
index a519d75..b251a5b1 100644
--- a/components/download/internal/background_service/entry_utils.h
+++ b/components/download/internal/background_service/entry_utils.h
@@ -7,7 +7,6 @@
 
 #include <map>
 #include <set>
-#include <string>
 #include <vector>
 
 #include "components/download/internal/background_service/model.h"
diff --git a/components/download/internal/background_service/file_monitor.h b/components/download/internal/background_service/file_monitor.h
index 11fd4dd6..050cb3b 100644
--- a/components/download/internal/background_service/file_monitor.h
+++ b/components/download/internal/background_service/file_monitor.h
@@ -7,7 +7,6 @@
 
 #include <memory>
 #include <set>
-#include <string>
 #include <vector>
 
 #include "components/download/internal/background_service/model.h"
diff --git a/components/download/internal/background_service/file_monitor_impl.h b/components/download/internal/background_service/file_monitor_impl.h
index 34ee780..3df902b 100644
--- a/components/download/internal/background_service/file_monitor_impl.h
+++ b/components/download/internal/background_service/file_monitor_impl.h
@@ -9,7 +9,6 @@
 
 #include <memory>
 #include <set>
-#include <string>
 #include <vector>
 
 #include "base/files/file_path.h"
diff --git a/components/embedder_support/android/delegate/color_chooser_android.h b/components/embedder_support/android/delegate/color_chooser_android.h
index 71ae29e..05da5e36 100644
--- a/components/embedder_support/android/delegate/color_chooser_android.h
+++ b/components/embedder_support/android/delegate/color_chooser_android.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_EMBEDDER_SUPPORT_ANDROID_DELEGATE_COLOR_CHOOSER_ANDROID_H_
 #define COMPONENTS_EMBEDDER_SUPPORT_ANDROID_DELEGATE_COLOR_CHOOSER_ANDROID_H_
 
-#include <string>
 #include <vector>
 
 #include "base/android/jni_android.h"
diff --git a/components/exo/shell_surface_base.cc b/components/exo/shell_surface_base.cc
index 6cc34314..7c9e0d2 100644
--- a/components/exo/shell_surface_base.cc
+++ b/components/exo/shell_surface_base.cc
@@ -292,14 +292,13 @@
   DISALLOW_COPY_AND_ASSIGN(CustomWindowStateDelegate);
 };
 
-void CloseAllTransientChildren(aura::Window* window) {
-  // Deleting a window may delete other transient children, so
-  // delete them by popping from the list.
-  for (;;) {
-    auto list = wm::GetTransientChildren(window);
-    if (list.empty())
-      return;
-    wm::RemoveTransientChild(window, *list.begin());
+void CloseAllShellSurfaceTransientChildren(aura::Window* window) {
+  // Deleting a window may delete other transient children. Remove other shell
+  // surface bases first so they don't get deleted.
+  auto list = wm::GetTransientChildren(window);
+  for (size_t i = 0; i < list.size(); ++i) {
+    if (GetShellSurfaceBaseForWindow(list[i]))
+      wm::RemoveTransientChild(window, list[i]);
   }
 }
 
@@ -355,8 +354,9 @@
   if (widget_) {
     widget_->GetNativeWindow()->RemoveObserver(this);
     widget_->RemoveObserver(this);
-    // Remove transient children so they are not automatically destroyed.
-    CloseAllTransientChildren(widget_->GetNativeWindow());
+    // Remove transient children which are shell surfaces so they are not
+    // automatically destroyed.
+    CloseAllShellSurfaceTransientChildren(widget_->GetNativeWindow());
     if (widget_->IsVisible())
       widget_->Hide();
     widget_->CloseNow();
@@ -806,8 +806,9 @@
   // Hide widget before surface is destroyed. This allows hide animations to
   // run using the current surface contents.
   if (widget_) {
-    // Remove transient children so they are not automatically hidden.
-    CloseAllTransientChildren(widget_->GetNativeWindow());
+    // Remove transient children which are shell surfaces so they are not
+    // automatically hidden.
+    CloseAllShellSurfaceTransientChildren(widget_->GetNativeWindow());
     widget_->Hide();
   }
 
diff --git a/components/feed/core/v2/public/feed_service.cc b/components/feed/core/v2/public/feed_service.cc
index d0c01ef..19855883 100644
--- a/components/feed/core/v2/public/feed_service.cc
+++ b/components/feed/core/v2/public/feed_service.cc
@@ -134,7 +134,7 @@
   bool IsEulaAccepted() override {
     return eula_notifier_.IsEulaAccepted() ||
            base::CommandLine::ForCurrentProcess()->HasSwitch(
-               "feedv2-accept-eula");
+               "feed-screenshot-mode");
   }
   bool IsOffline() override { return net::NetworkChangeNotifier::IsOffline(); }
   DisplayMetrics GetDisplayMetrics() override {
diff --git a/components/feed/core/v2/tasks/load_stream_from_store_task.h b/components/feed/core/v2/tasks/load_stream_from_store_task.h
index 0a5ee54..2385e5e 100644
--- a/components/feed/core/v2/tasks/load_stream_from_store_task.h
+++ b/components/feed/core/v2/tasks/load_stream_from_store_task.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_FEED_CORE_V2_TASKS_LOAD_STREAM_FROM_STORE_TASK_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/gcm_driver/account_tracker.h b/components/gcm_driver/account_tracker.h
index b89cc872..1a78a07 100644
--- a/components/gcm_driver/account_tracker.h
+++ b/components/gcm_driver/account_tracker.h
@@ -7,7 +7,6 @@
 
 #include <map>
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/observer_list.h"
diff --git a/components/heavy_ad_intervention/heavy_ad_service.h b/components/heavy_ad_intervention/heavy_ad_service.h
index 1c1c9409..7a795d1 100644
--- a/components/heavy_ad_intervention/heavy_ad_service.h
+++ b/components/heavy_ad_intervention/heavy_ad_service.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_HEAVY_AD_INTERVENTION_HEAVY_AD_SERVICE_H_
 
 #include <memory>
-#include <string>
 
 #include "base/callback.h"
 #include "base/macros.h"
diff --git a/components/history/content/browser/download_conversions.h b/components/history/content/browser/download_conversions.h
index 1482efa..274d4d8 100644
--- a/components/history/content/browser/download_conversions.h
+++ b/components/history/content/browser/download_conversions.h
@@ -7,8 +7,6 @@
 
 #include <stdint.h>
 
-#include <string>
-
 #include "components/download/public/common/download_danger_type.h"
 #include "components/download/public/common/download_interrupt_reasons.h"
 #include "components/download/public/common/download_item.h"
diff --git a/components/history/core/browser/top_sites_database.h b/components/history/core/browser/top_sites_database.h
index 14a720a6..6078cd1 100644
--- a/components/history/core/browser/top_sites_database.h
+++ b/components/history/core/browser/top_sites_database.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_HISTORY_CORE_BROWSER_TOP_SITES_DATABASE_H_
 
 #include <map>
-#include <string>
 
 #include "base/gtest_prod_util.h"
 #include "base/macros.h"
diff --git a/components/history/core/test/history_service_test_util.h b/components/history/core/test/history_service_test_util.h
index d3f94c3..4ab848d 100644
--- a/components/history/core/test/history_service_test_util.h
+++ b/components/history/core/test/history_service_test_util.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_HISTORY_CORE_TEST_HISTORY_SERVICE_TEST_UTIL_H_
 
 #include <memory>
-#include <string>
 
 #include "base/macros.h"
 
diff --git a/components/infobars/android/infobar_android.h b/components/infobars/android/infobar_android.h
index cf08fe9a..38881e8 100644
--- a/components/infobars/android/infobar_android.h
+++ b/components/infobars/android/infobar_android.h
@@ -5,8 +5,6 @@
 #ifndef COMPONENTS_INFOBARS_ANDROID_INFOBAR_ANDROID_H_
 #define COMPONENTS_INFOBARS_ANDROID_INFOBAR_ANDROID_H_
 
-#include <string>
-
 #include "base/android/scoped_java_ref.h"
 #include "base/callback.h"
 #include "base/macros.h"
diff --git a/components/js_injection/browser/web_message_reply_proxy.h b/components/js_injection/browser/web_message_reply_proxy.h
index 9a09c02..2eb52975 100644
--- a/components/js_injection/browser/web_message_reply_proxy.h
+++ b/components/js_injection/browser/web_message_reply_proxy.h
@@ -5,9 +5,6 @@
 #ifndef COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_REPLY_PROXY_H_
 #define COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_REPLY_PROXY_H_
 
-#include <string>
-
-
 namespace js_injection {
 
 struct WebMessage;
diff --git a/components/js_injection/common/origin_matcher_mojom_traits.h b/components/js_injection/common/origin_matcher_mojom_traits.h
index 873d2cb..b4c02553 100644
--- a/components/js_injection/common/origin_matcher_mojom_traits.h
+++ b/components/js_injection/common/origin_matcher_mojom_traits.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_JS_INJECTION_COMMON_ORIGIN_MATCHER_MOJOM_TRAITS_H_
 #define COMPONENTS_JS_INJECTION_COMMON_ORIGIN_MATCHER_MOJOM_TRAITS_H_
 
-#include <string>
 #include <vector>
 
 #include "components/js_injection/common/origin_matcher.h"
diff --git a/components/live_caption/BUILD.gn b/components/live_caption/BUILD.gn
index 0ef5c49..748a8e7 100644
--- a/components/live_caption/BUILD.gn
+++ b/components/live_caption/BUILD.gn
@@ -2,27 +2,29 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-static_library("live_caption") {
-  sources = [
-    "views/caption_bubble.cc",
-    "views/caption_bubble.h",
-    "views/caption_bubble_model.cc",
-    "views/caption_bubble_model.h",
-  ]
+if (!is_android) {
+  static_library("live_caption") {
+    sources = [
+      "views/caption_bubble.cc",
+      "views/caption_bubble.h",
+      "views/caption_bubble_model.cc",
+      "views/caption_bubble_model.h",
+    ]
 
-  deps = [
-    "//base",
-    "//components/strings",
-    "//components/vector_icons",
-    "//third_party/re2",
-    "//ui/accessibility",
-    "//ui/base",
-    "//ui/gfx",
-    "//ui/native_theme",
-    "//ui/strings:ui_strings_grit",
-    "//ui/views",
-  ]
-}
+    deps = [
+      "//base",
+      "//components/strings",
+      "//components/vector_icons",
+      "//third_party/re2",
+      "//ui/accessibility",
+      "//ui/base",
+      "//ui/gfx",
+      "//ui/native_theme",
+      "//ui/strings:ui_strings_grit",
+      "//ui/views",
+    ]
+  }
+}  # !is_android
 
 source_set("constants") {
   sources = [
diff --git a/components/live_caption/pref_names.cc b/components/live_caption/pref_names.cc
index 703f8f5..b596aad 100644
--- a/components/live_caption/pref_names.cc
+++ b/components/live_caption/pref_names.cc
@@ -6,6 +6,7 @@
 
 namespace prefs {
 
+#if !defined(ANDROID)
 // Whether the Live Caption feature is enabled.
 const char kLiveCaptionEnabled[] =
     "accessibility.captions.live_caption_enabled";
@@ -13,5 +14,34 @@
 // The language to use with the Live Caption feature.
 const char kLiveCaptionLanguageCode[] =
     "accessibility.captions.live_caption_language";
+#endif  // !defined(ANDROID)
+
+// String indicating the size of the captions text as a percentage.
+const char kAccessibilityCaptionsTextSize[] =
+    "accessibility.captions.text_size";
+
+// String indicating the font of the captions text.
+const char kAccessibilityCaptionsTextFont[] =
+    "accessibility.captions.text_font";
+
+// Comma-separated string indicating the RGB values of the captions text color.
+const char kAccessibilityCaptionsTextColor[] =
+    "accessibility.captions.text_color";
+
+// Integer indicating the opacity of the captions text from 0 - 100.
+const char kAccessibilityCaptionsTextOpacity[] =
+    "accessibility.captions.text_opacity";
+
+// Comma-separated string indicating the RGB values of the background color.
+const char kAccessibilityCaptionsBackgroundColor[] =
+    "accessibility.captions.background_color";
+
+// CSS string indicating the shadow of the captions text.
+const char kAccessibilityCaptionsTextShadow[] =
+    "accessibility.captions.text_shadow";
+
+// Integer indicating the opacity of the captions text background from 0 - 100.
+const char kAccessibilityCaptionsBackgroundOpacity[] =
+    "accessibility.captions.background_opacity";
 
 }  // namespace prefs
diff --git a/components/live_caption/pref_names.h b/components/live_caption/pref_names.h
index 56b66f1a..3f93d11a 100644
--- a/components/live_caption/pref_names.h
+++ b/components/live_caption/pref_names.h
@@ -7,8 +7,23 @@
 
 namespace prefs {
 
+// Live Caption is not available on Android, so exclude these unneeded
+// kLiveCaption*  prefs.
+#if !defined(ANDROID)
 extern const char kLiveCaptionEnabled[];
 extern const char kLiveCaptionLanguageCode[];
+#endif  // !defined(ANDROID)
+
+// These kAccessibilityCaptions* caption style prefs are used on Android
+// (though their primary use is for Live Caption, which is why they are housed
+// within this live_caption component instead of somewhere more generic).
+extern const char kAccessibilityCaptionsTextSize[];
+extern const char kAccessibilityCaptionsTextFont[];
+extern const char kAccessibilityCaptionsTextColor[];
+extern const char kAccessibilityCaptionsTextOpacity[];
+extern const char kAccessibilityCaptionsBackgroundColor[];
+extern const char kAccessibilityCaptionsTextShadow[];
+extern const char kAccessibilityCaptionsBackgroundOpacity[];
 
 }  // namespace prefs
 
diff --git a/components/lookalikes/core/BUILD.gn b/components/lookalikes/core/BUILD.gn
index fccb480..c46ad6f7 100644
--- a/components/lookalikes/core/BUILD.gn
+++ b/components/lookalikes/core/BUILD.gn
@@ -14,6 +14,8 @@
     "//base",
     "//components/pref_registry",
     "//components/prefs:prefs",
+    "//components/reputation/core:core",
+    "//components/reputation/core:proto",
     "//components/security_interstitials/core",
     "//components/security_state/core:features",
     "//components/strings",
@@ -36,6 +38,7 @@
   deps = [
     ":core",
     ":features",
+    "//components/reputation/core",
     "//net:test_support",
     "//testing/gtest",
   ]
diff --git a/components/lookalikes/core/DEPS b/components/lookalikes/core/DEPS
index a3c048b..33c4e65 100644
--- a/components/lookalikes/core/DEPS
+++ b/components/lookalikes/core/DEPS
@@ -3,4 +3,6 @@
   # should not be introduced.
   "-content",
   "-ios/web",
+  # components/reputation contains the lookalike (safety tips) component.
+  "+components/reputation/core",
 ]
diff --git a/components/lookalikes/core/features.cc b/components/lookalikes/core/features.cc
index 3eb15692..3d1fb0c 100644
--- a/components/lookalikes/core/features.cc
+++ b/components/lookalikes/core/features.cc
@@ -9,7 +9,7 @@
 
 // Note: this flag is ignored on iOS. See lookalike_url_util.cc.
 const base::Feature kDetectTargetEmbeddingLookalikes{
-    "TargetEmbeddingLookalikes", base::FEATURE_DISABLED_BY_DEFAULT};
+    "TargetEmbeddingLookalikes", base::FEATURE_ENABLED_BY_DEFAULT};
 
 const base::Feature kLookalikeInterstitialForPunycode{
     "LookalikeInterstitialForPunycode", base::FEATURE_ENABLED_BY_DEFAULT};
diff --git a/components/lookalikes/core/lookalike_url_util.cc b/components/lookalikes/core/lookalike_url_util.cc
index 55965fd..a3e5695 100644
--- a/components/lookalikes/core/lookalike_url_util.cc
+++ b/components/lookalikes/core/lookalike_url_util.cc
@@ -27,6 +27,7 @@
 #include "base/values.h"
 #include "build/build_config.h"
 #include "components/lookalikes/core/features.h"
+#include "components/reputation/core/safety_tips_config.h"
 #include "components/security_interstitials/core/pref_names.h"
 #include "components/security_state/core/features.h"
 #include "components/url_formatter/spoof_checks/common_words/common_words_util.h"
@@ -285,7 +286,8 @@
 // Returns whether the e2LD of the provided domain is a common word (e.g.
 // weather.com, ask.com). Target embeddings of these domains are often false
 // positives (e.g. "super-best-fancy-hotels.com" isn't spoofing "hotels.com").
-bool UsesCommonWord(const DomainInfo& domain) {
+bool UsesCommonWord(const reputation::SafetyTipsConfig* config_proto,
+                    const DomainInfo& domain) {
   // kDomainsPermittedInEndEmbeddings are based on domains with common words,
   // but they should not be excluded here (and instead are checked later).
   for (auto* permitted_ending : kDomainsPermittedInEndEmbeddings) {
@@ -300,7 +302,13 @@
     return true;
   }
 
-  // Also check the local lists.
+  // Search for words in the component-provided word list.
+  if (reputation::IsCommonWordInConfigProto(config_proto,
+                                            domain.domain_without_registry)) {
+    return true;
+  }
+
+  // Search for words in the local word lists.
   for (auto* common_word : kLocalAdditionalCommonWords) {
     if (domain.domain_without_registry == common_word) {
       return true;
@@ -369,8 +377,9 @@
     const DomainInfo& embedded_target,
     const base::span<const base::StringPiece>& subdomain_span,
     const LookalikeTargetAllowlistChecker& in_target_allowlist,
-    const std::string& embedding_domain) {
-  return UsesCommonWord(embedded_target) ||
+    const std::string& embedding_domain,
+    const reputation::SafetyTipsConfig* config_proto) {
+  return UsesCommonWord(config_proto, embedded_target) ||
          ASubdomainIsAllowlisted(subdomain_span, in_target_allowlist) ||
          IsEmbeddingItself(subdomain_span, embedding_domain) ||
          IsCrossTLDMatch(embedded_target, embedding_domain) ||
@@ -614,6 +623,7 @@
     const DomainInfo& navigated_domain,
     const std::vector<DomainInfo>& engaged_sites,
     const LookalikeTargetAllowlistChecker& in_target_allowlist,
+    const reputation::SafetyTipsConfig* config_proto,
     std::string* matched_domain,
     LookalikeUrlMatchType* match_type) {
   DCHECK(!navigated_domain.domain_and_registry.empty());
@@ -674,7 +684,7 @@
 
   TargetEmbeddingType embedding_type =
       GetTargetEmbeddingType(navigated_domain.hostname, engaged_sites,
-                             in_target_allowlist, matched_domain);
+                             in_target_allowlist, config_proto, matched_domain);
   if (embedding_type == TargetEmbeddingType::kSafetyTip) {
     *match_type = LookalikeUrlMatchType::kTargetEmbeddingForSafetyTips;
     return true;
@@ -723,6 +733,37 @@
     const std::string& hostname,
     const std::vector<DomainInfo>& engaged_sites,
     const LookalikeTargetAllowlistChecker& in_target_allowlist,
+    const reputation::SafetyTipsConfig* config_proto,
+    std::string* safe_hostname) {
+  // Because of how target embeddings are detected (i.e. by sweeping the URL
+  // from back to front), we're guaranteed to find tail-embedding before other
+  // target embedding. Tail embedding triggers a safety tip, but interstitials
+  // are more important than safety tips, so if we find a safety tippable
+  // embedding with SearchForEmbeddings, go search again not permitting safety
+  // tips to see if we can also find an interstitiallable embedding.
+  auto result = SearchForEmbeddings(
+      hostname, engaged_sites, in_target_allowlist, config_proto,
+      /*safety_tips_allowed=*/true, safe_hostname);
+  if (result == TargetEmbeddingType::kSafetyTip) {
+    std::string no_st_safe_hostname;
+    auto no_st_result = SearchForEmbeddings(
+        hostname, engaged_sites, in_target_allowlist, config_proto,
+        /*safety_tips_allowed=*/false, &no_st_safe_hostname);
+    if (no_st_result == TargetEmbeddingType::kNone) {
+      return result;
+    }
+    *safe_hostname = no_st_safe_hostname;
+    return no_st_result;
+  }
+  return result;
+}
+
+TargetEmbeddingType SearchForEmbeddings(
+    const std::string& hostname,
+    const std::vector<DomainInfo>& engaged_sites,
+    const LookalikeTargetAllowlistChecker& in_target_allowlist,
+    const reputation::SafetyTipsConfig* config_proto,
+    bool safety_tips_allowed,
     std::string* safe_hostname) {
   const std::string embedding_domain = GetETLDPlusOne(hostname);
   const std::vector<base::StringPiece> hostname_tokens =
@@ -762,7 +803,8 @@
       if (no_separator_dominfo.domain_without_registry.length() >
               kMinE2LDLengthForTargetEmbedding &&
           !IsAllowedToBeEmbedded(no_separator_dominfo, no_separator_tokens,
-                                 in_target_allowlist, embedding_domain)) {
+                                 in_target_allowlist, embedding_domain,
+                                 config_proto)) {
         *safe_hostname = embedded_target;
         return TargetEmbeddingType::kInterstitial;
       }
@@ -791,9 +833,17 @@
       for (auto& engaged_site : engaged_sites) {
         if (engaged_site.hostname == embedded_dominfo.hostname &&
             !IsAllowedToBeEmbedded(embedded_dominfo, span, in_target_allowlist,
-                                   embedding_domain)) {
+                                   embedding_domain, config_proto)) {
           *safe_hostname = engaged_site.hostname;
-          return TargetEmbeddingType::kInterstitial;
+          // Tail-embedding (e.g. evil-google.com, where the embedding happens
+          // at the very end of the hostname) is a safety tip, but only when
+          // safety tips are allowed. If it's tail embedding but we can't create
+          // a safety tip, keep looking.  Non-tail-embeddings are interstitials.
+          if (end != static_cast<int>(hostname_tokens.size())) {
+            return TargetEmbeddingType::kInterstitial;
+          } else if (safety_tips_allowed) {
+            return TargetEmbeddingType::kSafetyTip;
+          }  // else keep searching.
         }
       }
     }
@@ -803,8 +853,17 @@
     if (DoesETLDPlus1MatchTopDomainOrEngagedSite(
             etld_check_dominfo, engaged_sites, safe_hostname) &&
         !IsAllowedToBeEmbedded(etld_check_dominfo, etld_check_span,
-                               in_target_allowlist, embedding_domain)) {
-      return TargetEmbeddingType::kInterstitial;
+                               in_target_allowlist, embedding_domain,
+                               config_proto)) {
+      // Tail-embedding (e.g. evil-google.com, where the embedding happens at
+      // the very end of the hostname) is a safety tip, but only when safety
+      // tips are allowed. If it's tail embedding but we can't create a safety
+      // tip, keep looking.  Non-tail-embeddings are interstitials.
+      if (end != static_cast<int>(hostname_tokens.size())) {
+        return TargetEmbeddingType::kInterstitial;
+      } else if (safety_tips_allowed) {
+        return TargetEmbeddingType::kSafetyTip;
+      }  // else keep searching.
     }
   }
   return TargetEmbeddingType::kNone;
diff --git a/components/lookalikes/core/lookalike_url_util.h b/components/lookalikes/core/lookalike_url_util.h
index 4882419..e5a8ba87 100644
--- a/components/lookalikes/core/lookalike_url_util.h
+++ b/components/lookalikes/core/lookalike_url_util.h
@@ -11,6 +11,7 @@
 #include "base/callback.h"
 #include "components/pref_registry/pref_registry_syncable.h"
 #include "components/prefs/pref_service.h"
+#include "components/reputation/core/safety_tips.pb.h"
 #include "components/url_formatter/url_formatter.h"
 #include "url/gurl.h"
 
@@ -162,31 +163,33 @@
     const DomainInfo& navigated_domain,
     const std::vector<DomainInfo>& engaged_sites,
     const LookalikeTargetAllowlistChecker& in_target_allowlist,
+    const reputation::SafetyTipsConfig* config_proto,
     std::string* matched_domain,
     LookalikeUrlMatchType* match_type);
 
 void RecordUMAFromMatchType(LookalikeUrlMatchType match_type);
 
 // Checks to see if a URL is a target embedding lookalike. This function sets
-// |safe_hostname| to the url of the embedded target domain.
-// At the moment we consider the following cases as Target Embedding:
-// example-google.com-site.com, example.google.com-site.com,
-// example-google-info-site.com, example.google.com.site.com,
-// example-googlé.com-site.com where the embedded target is google.com. We
-// detect embeddings of top 500 domains and engaged domains. However, to reduce
-// false positives, we do not protect domains that are shorter than 7 characters
-// long (e.g. com.ru).
-// This function checks possible targets against |in_target_allowlist| to skip
-// permitted embeddings.
-// If no target embedding is found, the return value will be set to |kNonw|.
-// When the target is embedded with another TLD instead of its actual TLD, it
-// should trigger a Safety Tip when the embedded TLD is a ccTLD. In this
-// situation, return value will be |kSafetyTip|. All the other triggers will
-// result in a |kInterstitial| return value.
+// |safe_hostname| to the url of the embedded target domain. See the unit tests
+// for what qualifies as target embedding.
 TargetEmbeddingType GetTargetEmbeddingType(
     const std::string& hostname,
     const std::vector<DomainInfo>& engaged_sites,
     const LookalikeTargetAllowlistChecker& in_target_allowlist,
+    const reputation::SafetyTipsConfig* config_proto,
+    std::string* safe_hostname);
+
+// Same as GetTargetEmbeddingType, but explicitly state whether or not a safety
+// tip is permitted via |safety_tips_allowed|. Safety tips are presently only
+// used for tail embedding (e.g. "evil-google.com"). This function may return
+// kSafetyTip preferentially to kInterstitial -- call with !safety_tips_allowed
+// if you're interested in determining if there's *also* an interstitial.
+TargetEmbeddingType SearchForEmbeddings(
+    const std::string& hostname,
+    const std::vector<DomainInfo>& engaged_sites,
+    const LookalikeTargetAllowlistChecker& in_target_allowlist,
+    const reputation::SafetyTipsConfig* config_proto,
+    bool safety_tips_allowed,
     std::string* safe_hostname);
 
 // Returns true if a navigation to an IDN should be blocked.
diff --git a/components/lookalikes/core/lookalike_url_util_unittest.cc b/components/lookalikes/core/lookalike_url_util_unittest.cc
index 4062ea9..e7b52ca 100644
--- a/components/lookalikes/core/lookalike_url_util_unittest.cc
+++ b/components/lookalikes/core/lookalike_url_util_unittest.cc
@@ -7,8 +7,22 @@
 #include "base/bind.h"
 #include "base/strings/utf_string_conversions.h"
 #include "components/lookalikes/core/features.h"
+#include "components/reputation/core/safety_tip_test_utils.h"
+#include "components/reputation/core/safety_tips_config.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+std::string TargetEmbeddingTypeToString(TargetEmbeddingType type) {
+  switch (type) {
+    case TargetEmbeddingType::kNone:
+      return "kNone";
+    case TargetEmbeddingType::kInterstitial:
+      return "kInterstitial";
+    case TargetEmbeddingType::kSafetyTip:
+      return "kSafetyTip";
+  }
+  NOTREACHED();
+}
+
 TEST(LookalikeUrlUtilTest, IsEditDistanceAtMostOne) {
   const struct TestCase {
     const wchar_t* domain;
@@ -139,7 +153,7 @@
   const TargetEmbeddingType expected_type;
 };
 
-TEST(LookalikeUrlUtilTest, TargetEmbeddingTest) {
+TEST(LookalikeUrlUtilTest, TargetEmbedding) {
   const std::vector<DomainInfo> kEngagedSites = {
       GetDomainInfo(GURL("https://highengagement.com")),
       GetDomainInfo(GURL("https://highengagement.inthesubdomain.com")),
@@ -278,12 +292,15 @@
       {"google.com.google.com", "", TargetEmbeddingType::kNone},
       {"www.google.com.google.com", "", TargetEmbeddingType::kNone},
 
-      // Detect embeddings at the end of the domain, too.
-      {"www-google.com", "google.com", TargetEmbeddingType::kInterstitial},
+      // Detect embeddings at the end of the domain, too, but as a Safety Tip.
+      {"www-google.com", "google.com", TargetEmbeddingType::kSafetyTip},
       {"www-highengagement.com", "highengagement.com",
-       TargetEmbeddingType::kInterstitial},
+       TargetEmbeddingType::kSafetyTip},
       {"subdomain-highengagement.com", "subdomain.highengagement.com",
-       TargetEmbeddingType::kInterstitial},
+       TargetEmbeddingType::kSafetyTip},
+      // If the match duplicates the TLD, it's not quite tail-embedding.
+      {"google-com.com", "google.com", TargetEmbeddingType::kInterstitial},
+      // If there are multiple options, it should choose the more severe one.
       {"google-com.google-com.com", "google.com",
        TargetEmbeddingType::kInterstitial},
       {"subdomain.google-com.google-com.com", "google.com",
@@ -300,14 +317,17 @@
       // works for domains on the list, but not for others.
       {"office.com-foo.com", "office.com", TargetEmbeddingType::kInterstitial},
       {"example-office.com", "", TargetEmbeddingType::kNone},
-      {"example-google.com", "google.com", TargetEmbeddingType::kInterstitial},
+      {"example-google.com", "google.com", TargetEmbeddingType::kSafetyTip},
   };
 
+  reputation::InitializeBlankLookalikeAllowlistForTesting();
+  auto* config_proto = reputation::GetSafetyTipsRemoteConfigProto();
+
   for (auto& test_case : kTestCases) {
     std::string safe_hostname;
     TargetEmbeddingType embedding_type = GetTargetEmbeddingType(
         test_case.hostname, kEngagedSites,
-        base::BindRepeating(&IsGoogleScholar), &safe_hostname);
+        base::BindRepeating(&IsGoogleScholar), config_proto, &safe_hostname);
     if (test_case.expected_type != TargetEmbeddingType::kNone) {
       EXPECT_EQ(safe_hostname, test_case.expected_safe_host)
           << test_case.hostname << " should trigger on "
@@ -315,19 +335,43 @@
           << (safe_hostname.empty() ? "it didn't trigger at all."
                                     : "triggered on " + safe_hostname);
       EXPECT_EQ(embedding_type, test_case.expected_type)
-          << test_case.hostname << " should trigger on "
+          << test_case.hostname << " should trigger "
+          << TargetEmbeddingTypeToString(test_case.expected_type) << " against "
           << test_case.expected_safe_host << " but it returned "
-          << (embedding_type == TargetEmbeddingType::kNone
-                  ? "kNone."
-                  : "something unexpected");
+          << TargetEmbeddingTypeToString(embedding_type);
     } else {
       EXPECT_EQ(embedding_type, TargetEmbeddingType::kNone)
-          << test_case.hostname << " unexpectedly triggered on "
+          << test_case.hostname << " unexpectedly triggered "
+          << TargetEmbeddingTypeToString(embedding_type) << " against "
           << safe_hostname;
     }
   }
 }
 
+TEST(LookalikeUrlUtilTest, TargetEmbeddingIgnoresComponentWordlist) {
+  const std::vector<DomainInfo> kEngagedSites = {
+      GetDomainInfo(GURL("https://commonword.com")),
+      GetDomainInfo(GURL("https://uncommonword.com")),
+  };
+
+  reputation::SetSafetyTipAllowlistPatterns({}, {}, {"commonword"});
+  auto* config_proto = reputation::GetSafetyTipsRemoteConfigProto();
+  TargetEmbeddingType embedding_type;
+  std::string safe_hostname;
+
+  // Engaged sites using uncommon words are still blocked.
+  embedding_type = GetTargetEmbeddingType(
+      "uncommonword.com.evil.com", kEngagedSites,
+      base::BindRepeating(&IsGoogleScholar), config_proto, &safe_hostname);
+  EXPECT_EQ(embedding_type, TargetEmbeddingType::kInterstitial);
+
+  // But engaged sites using common words are not blocked.
+  embedding_type = GetTargetEmbeddingType(
+      "commonword.com.evil.com", kEngagedSites,
+      base::BindRepeating(&IsGoogleScholar), config_proto, &safe_hostname);
+  EXPECT_EQ(embedding_type, TargetEmbeddingType::kNone);
+}
+
 struct GetETLDPlusOneTestCase {
   const std::string hostname;
   const std::string expected_etldp1;
diff --git a/components/media_control/renderer/media_playback_options.h b/components/media_control/renderer/media_playback_options.h
index f355d74..54d536f 100644
--- a/components/media_control/renderer/media_playback_options.h
+++ b/components/media_control/renderer/media_playback_options.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_MEDIA_CONTROL_RENDERER_MEDIA_PLAYBACK_OPTIONS_H_
 #define COMPONENTS_MEDIA_CONTROL_RENDERER_MEDIA_PLAYBACK_OPTIONS_H_
 
-#include <string>
 #include <vector>
 
 #include "base/callback_forward.h"
diff --git a/components/media_router/browser/media_router_dialog_controller.h b/components/media_router/browser/media_router_dialog_controller.h
index 009b2cc..ac6880fc 100644
--- a/components/media_router/browser/media_router_dialog_controller.h
+++ b/components/media_router/browser/media_router_dialog_controller.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_MEDIA_ROUTER_BROWSER_MEDIA_ROUTER_DIALOG_CONTROLLER_H_
 
 #include <memory>
-#include <string>
 
 #include "base/callback.h"
 #include "base/macros.h"
diff --git a/components/media_router/browser/route_message_observer.h b/components/media_router/browser/route_message_observer.h
index 8c2452d..675b503 100644
--- a/components/media_router/browser/route_message_observer.h
+++ b/components/media_router/browser/route_message_observer.h
@@ -7,7 +7,6 @@
 
 #include <stdint.h>
 
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/metrics/file_metrics_provider.h b/components/metrics/file_metrics_provider.h
index f5fbb8a4..fe78275 100644
--- a/components/metrics/file_metrics_provider.h
+++ b/components/metrics/file_metrics_provider.h
@@ -7,7 +7,6 @@
 
 #include <list>
 #include <memory>
-#include <string>
 
 #include "base/callback_forward.h"
 #include "base/files/file_path.h"
diff --git a/components/metrics/library_support/histogram_manager.h b/components/metrics/library_support/histogram_manager.h
index 331eca0..1465284 100644
--- a/components/metrics/library_support/histogram_manager.h
+++ b/components/metrics/library_support/histogram_manager.h
@@ -8,7 +8,6 @@
 #include <stdint.h>
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/lazy_instance.h"
diff --git a/components/metrics/metrics_log_manager.h b/components/metrics/metrics_log_manager.h
index 88d8fe2..123a186 100644
--- a/components/metrics/metrics_log_manager.h
+++ b/components/metrics/metrics_log_manager.h
@@ -8,7 +8,6 @@
 #include <stddef.h>
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/metrics/structured/persistent_proto.h b/components/metrics/structured/persistent_proto.h
index e73941cc..8f73a44 100644
--- a/components/metrics/structured/persistent_proto.h
+++ b/components/metrics/structured/persistent_proto.h
@@ -5,8 +5,6 @@
 #ifndef COMPONENTS_METRICS_STRUCTURED_PERSISTENT_PROTO_H_
 #define COMPONENTS_METRICS_STRUCTURED_PERSISTENT_PROTO_H_
 
-#include <string>
-
 #include "base/files/file_path.h"
 #include "base/macros.h"
 #include "base/memory/scoped_refptr.h"
diff --git a/components/metrics/structured/structured_metrics_provider.h b/components/metrics/structured/structured_metrics_provider.h
index 4be682a..d7d6b87 100644
--- a/components/metrics/structured/structured_metrics_provider.h
+++ b/components/metrics/structured/structured_metrics_provider.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_METRICS_STRUCTURED_STRUCTURED_METRICS_PROVIDER_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/files/file_path.h"
diff --git a/components/nacl/renderer/file_downloader.h b/components/nacl/renderer/file_downloader.h
index 3760c38..1ed94f1 100644
--- a/components/nacl/renderer/file_downloader.h
+++ b/components/nacl/renderer/file_downloader.h
@@ -7,8 +7,6 @@
 
 #include <stdint.h>
 
-#include <string>
-
 #include "base/callback.h"
 #include "base/files/file.h"
 #include "components/nacl/renderer/ppb_nacl_private.h"
diff --git a/components/no_state_prefetch/browser/no_state_prefetch_manager.h b/components/no_state_prefetch/browser/no_state_prefetch_manager.h
index 13616c0..40346fe6 100644
--- a/components/no_state_prefetch/browser/no_state_prefetch_manager.h
+++ b/components/no_state_prefetch/browser/no_state_prefetch_manager.h
@@ -9,7 +9,6 @@
 
 #include <memory>
 #include <set>
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/ntp_snippets/category_rankers/constant_category_ranker.h b/components/ntp_snippets/category_rankers/constant_category_ranker.h
index 258e1045..3307f8b 100644
--- a/components/ntp_snippets/category_rankers/constant_category_ranker.h
+++ b/components/ntp_snippets/category_rankers/constant_category_ranker.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_NTP_SNIPPETS_CATEGORY_RANKERS_CONSTANT_CATEGORY_RANKER_H_
 #define COMPONENTS_NTP_SNIPPETS_CATEGORY_RANKERS_CONSTANT_CATEGORY_RANKER_H_
 
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/ntp_snippets/remote/remote_suggestions_scheduler_impl.h b/components/ntp_snippets/remote/remote_suggestions_scheduler_impl.h
index 3558ffb..35aa46a1 100644
--- a/components/ntp_snippets/remote/remote_suggestions_scheduler_impl.h
+++ b/components/ntp_snippets/remote/remote_suggestions_scheduler_impl.h
@@ -7,7 +7,6 @@
 
 #include <memory>
 #include <set>
-#include <string>
 #include <utility>
 #include <vector>
 
diff --git a/components/ntp_tiles/icon_cacher_impl.h b/components/ntp_tiles/icon_cacher_impl.h
index 768c076..96e7a72 100644
--- a/components/ntp_tiles/icon_cacher_impl.h
+++ b/components/ntp_tiles/icon_cacher_impl.h
@@ -7,7 +7,6 @@
 
 #include <memory>
 #include <set>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/offline_pages/core/background/request_queue.h b/components/offline_pages/core/background/request_queue.h
index e55e1f6b..2bd0b69 100644
--- a/components/offline_pages/core/background/request_queue.h
+++ b/components/offline_pages/core/background/request_queue.h
@@ -9,7 +9,6 @@
 
 #include <memory>
 #include <set>
-#include <string>
 #include <utility>
 #include <vector>
 
diff --git a/components/offline_pages/core/offline_page_archive_publisher.h b/components/offline_pages/core/offline_page_archive_publisher.h
index 5d90657..cea4b63 100644
--- a/components/offline_pages/core/offline_page_archive_publisher.h
+++ b/components/offline_pages/core/offline_page_archive_publisher.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_OFFLINE_PAGES_CORE_OFFLINE_PAGE_ARCHIVE_PUBLISHER_H_
 
 #include <cstdint>
-#include <string>
 
 #include "base/callback.h"
 #include "base/files/file_path.h"
diff --git a/components/offline_pages/core/offline_page_test_archive_publisher.h b/components/offline_pages/core/offline_page_test_archive_publisher.h
index 40e141a0..55a8202a 100644
--- a/components/offline_pages/core/offline_page_test_archive_publisher.h
+++ b/components/offline_pages/core/offline_page_test_archive_publisher.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_OFFLINE_PAGES_CORE_OFFLINE_PAGE_TEST_ARCHIVE_PUBLISHER_H_
 
 #include <cstdint>
-#include <string>
 
 #include "base/callback.h"
 #include "components/offline_pages/core/offline_page_archive_publisher.h"
diff --git a/components/offline_pages/core/prefetch/tasks/remove_url_task.h b/components/offline_pages/core/prefetch/tasks/remove_url_task.h
index e5f4c22..d1f3f1de 100644
--- a/components/offline_pages/core/prefetch/tasks/remove_url_task.h
+++ b/components/offline_pages/core/prefetch/tasks/remove_url_task.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_OFFLINE_PAGES_CORE_PREFETCH_TASKS_REMOVE_URL_TASK_H_
 #define COMPONENTS_OFFLINE_PAGES_CORE_PREFETCH_TASKS_REMOVE_URL_TASK_H_
 
-#include <string>
 #include <vector>
 
 #include "base/memory/weak_ptr.h"
diff --git a/components/omnibox/browser/bookmark_provider.h b/components/omnibox/browser/bookmark_provider.h
index 6af51eac..7740b18 100644
--- a/components/omnibox/browser/bookmark_provider.h
+++ b/components/omnibox/browser/bookmark_provider.h
@@ -7,8 +7,6 @@
 
 #include <stddef.h>
 
-#include <string>
-
 #include "base/gtest_prod_util.h"
 #include "components/bookmarks/browser/titled_url_match.h"
 #include "components/omnibox/browser/autocomplete_input.h"
diff --git a/components/omnibox/browser/omnibox_controller.h b/components/omnibox/browser/omnibox_controller.h
index c7c3b67..2650532b 100644
--- a/components/omnibox/browser/omnibox_controller.h
+++ b/components/omnibox/browser/omnibox_controller.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_OMNIBOX_BROWSER_OMNIBOX_CONTROLLER_H_
 
 #include <memory>
-#include <string>
 
 #include "base/compiler_specific.h"
 #include "components/omnibox/browser/autocomplete_controller.h"
diff --git a/components/on_load_script_injector/browser/on_load_script_injector_host.h b/components/on_load_script_injector/browser/on_load_script_injector_host.h
index 21356eb..bbe65a7 100644
--- a/components/on_load_script_injector/browser/on_load_script_injector_host.h
+++ b/components/on_load_script_injector/browser/on_load_script_injector_host.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_ON_LOAD_SCRIPT_INJECTOR_BROWSER_ON_LOAD_SCRIPT_INJECTOR_HOST_H_
 
 #include <map>
-#include <string>
 #include <vector>
 
 #include "base/memory/read_only_shared_memory_region.h"
diff --git a/components/optimization_guide/content/browser/page_text_observer.h b/components/optimization_guide/content/browser/page_text_observer.h
index 95e780a..4e6ed3e 100644
--- a/components/optimization_guide/content/browser/page_text_observer.h
+++ b/components/optimization_guide/content/browser/page_text_observer.h
@@ -7,7 +7,6 @@
 
 #include <stdint.h>
 #include <set>
-#include <string>
 
 #include "base/callback.h"
 #include "base/memory/weak_ptr.h"
diff --git a/components/optimization_guide/core/optimization_guide_features.cc b/components/optimization_guide/core/optimization_guide_features.cc
index 8d8fc21..4fcb18fa 100644
--- a/components/optimization_guide/core/optimization_guide_features.cc
+++ b/components/optimization_guide/core/optimization_guide_features.cc
@@ -36,8 +36,13 @@
     "OptimizationHintsFetching", base::FEATURE_ENABLED_BY_DEFAULT};
 
 const base::Feature kRemoteOptimizationGuideFetchingAnonymousDataConsent{
-    "OptimizationHintsFetchingAnonymousDataConsent",
-    base::FEATURE_ENABLED_BY_DEFAULT};
+  "OptimizationHintsFetchingAnonymousDataConsent",
+#if defined(OS_ANDROID)
+      base::FEATURE_ENABLED_BY_DEFAULT
+#else   // !defined(OS_ANDROID)
+      base::FEATURE_DISABLED_BY_DEFAULT
+#endif  // defined(OS_ANDROID)
+};
 
 // Enables performance info in the context menu and fetching from a remote
 // Optimization Guide Service.
diff --git a/components/page_info/android/BUILD.gn b/components/page_info/android/BUILD.gn
index 9b56e29..a18094a 100644
--- a/components/page_info/android/BUILD.gn
+++ b/components/page_info/android/BUILD.gn
@@ -32,21 +32,6 @@
 
 android_resources("java_resources") {
   sources = [
-    "java/res/drawable-hdpi/pageinfo_bad.png",
-    "java/res/drawable-hdpi/pageinfo_good.png",
-    "java/res/drawable-hdpi/pageinfo_warning.png",
-    "java/res/drawable-mdpi/pageinfo_bad.png",
-    "java/res/drawable-mdpi/pageinfo_good.png",
-    "java/res/drawable-mdpi/pageinfo_warning.png",
-    "java/res/drawable-xhdpi/pageinfo_bad.png",
-    "java/res/drawable-xhdpi/pageinfo_good.png",
-    "java/res/drawable-xhdpi/pageinfo_warning.png",
-    "java/res/drawable-xxhdpi/pageinfo_bad.png",
-    "java/res/drawable-xxhdpi/pageinfo_good.png",
-    "java/res/drawable-xxhdpi/pageinfo_warning.png",
-    "java/res/drawable-xxxhdpi/pageinfo_bad.png",
-    "java/res/drawable-xxxhdpi/pageinfo_good.png",
-    "java/res/drawable-xxxhdpi/pageinfo_warning.png",
     "java/res/drawable/ic_tune_24dp.xml",
     "java/res/drawable/page_info_bg.xml",
     "java/res/layout/connection_info.xml",
diff --git a/components/page_info/android/java/res/drawable-hdpi/pageinfo_bad.png b/components/page_info/android/java/res/drawable-hdpi/pageinfo_bad.png
deleted file mode 100644
index cd802be..0000000
--- a/components/page_info/android/java/res/drawable-hdpi/pageinfo_bad.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-hdpi/pageinfo_good.png b/components/page_info/android/java/res/drawable-hdpi/pageinfo_good.png
deleted file mode 100644
index 3aa5349..0000000
--- a/components/page_info/android/java/res/drawable-hdpi/pageinfo_good.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-hdpi/pageinfo_warning.png b/components/page_info/android/java/res/drawable-hdpi/pageinfo_warning.png
deleted file mode 100644
index 841b01f..0000000
--- a/components/page_info/android/java/res/drawable-hdpi/pageinfo_warning.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-mdpi/pageinfo_bad.png b/components/page_info/android/java/res/drawable-mdpi/pageinfo_bad.png
deleted file mode 100644
index 92ce9bdc..0000000
--- a/components/page_info/android/java/res/drawable-mdpi/pageinfo_bad.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-mdpi/pageinfo_good.png b/components/page_info/android/java/res/drawable-mdpi/pageinfo_good.png
deleted file mode 100644
index 60678e1..0000000
--- a/components/page_info/android/java/res/drawable-mdpi/pageinfo_good.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-mdpi/pageinfo_warning.png b/components/page_info/android/java/res/drawable-mdpi/pageinfo_warning.png
deleted file mode 100644
index 002da07..0000000
--- a/components/page_info/android/java/res/drawable-mdpi/pageinfo_warning.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-xhdpi/pageinfo_bad.png b/components/page_info/android/java/res/drawable-xhdpi/pageinfo_bad.png
deleted file mode 100644
index b9a0354..0000000
--- a/components/page_info/android/java/res/drawable-xhdpi/pageinfo_bad.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-xhdpi/pageinfo_good.png b/components/page_info/android/java/res/drawable-xhdpi/pageinfo_good.png
deleted file mode 100644
index 11f8c32..0000000
--- a/components/page_info/android/java/res/drawable-xhdpi/pageinfo_good.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-xhdpi/pageinfo_warning.png b/components/page_info/android/java/res/drawable-xhdpi/pageinfo_warning.png
deleted file mode 100644
index 918c09c..0000000
--- a/components/page_info/android/java/res/drawable-xhdpi/pageinfo_warning.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-xxhdpi/pageinfo_bad.png b/components/page_info/android/java/res/drawable-xxhdpi/pageinfo_bad.png
deleted file mode 100644
index b259de5..0000000
--- a/components/page_info/android/java/res/drawable-xxhdpi/pageinfo_bad.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-xxhdpi/pageinfo_good.png b/components/page_info/android/java/res/drawable-xxhdpi/pageinfo_good.png
deleted file mode 100644
index 6c23d7c8..0000000
--- a/components/page_info/android/java/res/drawable-xxhdpi/pageinfo_good.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-xxhdpi/pageinfo_warning.png b/components/page_info/android/java/res/drawable-xxhdpi/pageinfo_warning.png
deleted file mode 100644
index 57b86b3b..0000000
--- a/components/page_info/android/java/res/drawable-xxhdpi/pageinfo_warning.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-xxxhdpi/pageinfo_bad.png b/components/page_info/android/java/res/drawable-xxxhdpi/pageinfo_bad.png
deleted file mode 100644
index 1c27dfd..0000000
--- a/components/page_info/android/java/res/drawable-xxxhdpi/pageinfo_bad.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-xxxhdpi/pageinfo_good.png b/components/page_info/android/java/res/drawable-xxxhdpi/pageinfo_good.png
deleted file mode 100644
index d11f3d7..0000000
--- a/components/page_info/android/java/res/drawable-xxxhdpi/pageinfo_good.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/res/drawable-xxxhdpi/pageinfo_warning.png b/components/page_info/android/java/res/drawable-xxxhdpi/pageinfo_warning.png
deleted file mode 100644
index 19d1ee2..0000000
--- a/components/page_info/android/java/res/drawable-xxxhdpi/pageinfo_warning.png
+++ /dev/null
Binary files differ
diff --git a/components/page_info/android/java/src/org/chromium/components/page_info/PageInfoFeatures.java b/components/page_info/android/java/src/org/chromium/components/page_info/PageInfoFeatures.java
index b477a07..340be5a 100644
--- a/components/page_info/android/java/src/org/chromium/components/page_info/PageInfoFeatures.java
+++ b/components/page_info/android/java/src/org/chromium/components/page_info/PageInfoFeatures.java
@@ -19,14 +19,12 @@
 public class PageInfoFeatures extends Features {
     public static final String PAGE_INFO_DISCOVERABILITY_NAME = "PageInfoDiscoverability";
     public static final String PAGE_INFO_HISTORY_NAME = "PageInfoHistory";
-    public static final String PAGE_INFO_V2_NAME = "PageInfoV2";
 
     // This list must be kept in sync with kFeaturesExposedToJava in page_info_features.cc.
     public static final PageInfoFeatures PAGE_INFO_DISCOVERABILITY =
             new PageInfoFeatures(0, PAGE_INFO_DISCOVERABILITY_NAME);
     public static final PageInfoFeatures PAGE_INFO_HISTORY =
             new PageInfoFeatures(1, PAGE_INFO_HISTORY_NAME);
-    public static final PageInfoFeatures PAGE_INFO_V2 = new PageInfoFeatures(2, PAGE_INFO_V2_NAME);
 
     private final int mOrdinal;
 
diff --git a/components/page_info/android/page_info_controller_android.cc b/components/page_info/android/page_info_controller_android.cc
index fff0218..404e75c 100644
--- a/components/page_info/android/page_info_controller_android.cc
+++ b/components/page_info/android/page_info_controller_android.cc
@@ -200,8 +200,7 @@
     // setting should show up in Page Info is in ShouldShowPermission in
     // page_info.cc.
     return permission.default_setting;
-  } else if (permission.type == ContentSettingsType::JAVASCRIPT &&
-             base::FeatureList::IsEnabled(page_info::kPageInfoV2)) {
+  } else if (permission.type == ContentSettingsType::JAVASCRIPT) {
     // The javascript content setting should show up if it is blocked globally
     // to give users an easy way to create exceptions.
     return permission.default_setting;
diff --git a/components/page_info/android/page_info_features.cc b/components/page_info/android/page_info_features.cc
index db517b7..96bd37f4 100644
--- a/components/page_info/android/page_info_features.cc
+++ b/components/page_info/android/page_info_features.cc
@@ -17,7 +17,6 @@
 const base::Feature* kFeaturesExposedToJava[] = {
     &kPageInfoDiscoverability,
     &kPageInfoHistory,
-    &kPageInfoV2,
 };
 
 }  // namespace
diff --git a/components/page_info/features.cc b/components/page_info/features.cc
index 9d9f81da..e86dbf9 100644
--- a/components/page_info/features.cc
+++ b/components/page_info/features.cc
@@ -14,7 +14,6 @@
                                              base::FEATURE_DISABLED_BY_DEFAULT};
 const base::Feature kPageInfoHistory{"PageInfoHistory",
                                      base::FEATURE_DISABLED_BY_DEFAULT};
-const base::Feature kPageInfoV2{"PageInfoV2", base::FEATURE_ENABLED_BY_DEFAULT};
 #endif
 
 #if !defined(OS_ANDROID)
diff --git a/components/page_info/features.h b/components/page_info/features.h
index b25fe47c..1f70655 100644
--- a/components/page_info/features.h
+++ b/components/page_info/features.h
@@ -18,8 +18,6 @@
 extern const base::Feature kPageInfoDiscoverability;
 // Enables the history sub page for Page Info.
 extern const base::Feature kPageInfoHistory;
-// Enables the second version of the Page Info View.
-extern const base::Feature kPageInfoV2;
 #endif
 
 #if !defined(OS_ANDROID)
diff --git a/components/page_info/page_info_ui.cc b/components/page_info/page_info_ui.cc
index d34cecf..f28a5de 100644
--- a/components/page_info/page_info_ui.cc
+++ b/components/page_info/page_info_ui.cc
@@ -279,10 +279,6 @@
 
 std::unique_ptr<PageInfoUI::SecurityDescription>
 PageInfoUI::GetSecurityDescription(const IdentityInfo& identity_info) const {
-  bool page_info_v2_enabled = false;
-#if defined(OS_ANDROID)
-  page_info_v2_enabled = base::FeatureList::IsEnabled(page_info::kPageInfoV2);
-#endif
   switch (identity_info.safe_browsing_status) {
     case PageInfo::SAFE_BROWSING_STATUS_NONE:
       break;
@@ -329,73 +325,97 @@
   }
 
   switch (identity_info.identity_status) {
-    case PageInfo::SITE_IDENTITY_STATUS_INTERNAL_PAGE:
 #if defined(OS_ANDROID)
+    case PageInfo::SITE_IDENTITY_STATUS_INTERNAL_PAGE:
       return CreateSecurityDescription(SecuritySummaryColor::GREEN, 0,
                                        IDS_PAGE_INFO_INTERNAL_PAGE,
                                        SecurityDescriptionType::INTERNAL);
-#else
-      // Internal pages on desktop have their own UI implementations which
-      // should never call this function.
-      NOTREACHED();
-      FALLTHROUGH;
-#endif
     case PageInfo::SITE_IDENTITY_STATUS_EV_CERT:
-      FALLTHROUGH;
     case PageInfo::SITE_IDENTITY_STATUS_CERT:
-      FALLTHROUGH;
     case PageInfo::SITE_IDENTITY_STATUS_ADMIN_PROVIDED_CERT:
       switch (identity_info.connection_status) {
         case PageInfo::SITE_CONNECTION_STATUS_INSECURE_ACTIVE_SUBRESOURCE:
           return CreateSecurityDescription(
-              SecuritySummaryColor::RED,
-              page_info_v2_enabled ? IDS_PAGE_INFO_NOT_SECURE_SUMMARY_SHORT
-                                   : IDS_PAGE_INFO_NOT_SECURE_SUMMARY,
+              SecuritySummaryColor::RED, IDS_PAGE_INFO_NOT_SECURE_SUMMARY_SHORT,
               IDS_PAGE_INFO_NOT_SECURE_DETAILS,
               SecurityDescriptionType::CONNECTION);
         case PageInfo::SITE_CONNECTION_STATUS_INSECURE_FORM_ACTION:
           return CreateSecurityDescription(
               SecuritySummaryColor::RED,
-              page_info_v2_enabled ? IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY_SHORT
-                                   : IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY,
+              IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY_SHORT,
               IDS_PAGE_INFO_NOT_SECURE_DETAILS,
               SecurityDescriptionType::CONNECTION);
         case PageInfo::SITE_CONNECTION_STATUS_INSECURE_PASSIVE_SUBRESOURCE:
           return CreateSecurityDescription(
               SecuritySummaryColor::RED,
-              page_info_v2_enabled ? IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY_SHORT
-                                   : IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY,
+              IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY_SHORT,
               IDS_PAGE_INFO_MIXED_CONTENT_DETAILS,
               SecurityDescriptionType::CONNECTION);
         case PageInfo::SITE_CONNECTION_STATUS_LEGACY_TLS:
           return CreateSecurityDescription(
               SecuritySummaryColor::RED,
-              page_info_v2_enabled ? IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY_SHORT
-                                   : IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY,
+              IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY_SHORT,
               IDS_PAGE_INFO_LEGACY_TLS_DETAILS,
               SecurityDescriptionType::CONNECTION);
         default:
-          int secure_details = IDS_PAGE_INFO_SECURE_DETAILS;
-#if defined(OS_ANDROID)
-          if (page_info_v2_enabled) {
-            // Do not show details for secure connections.
-            secure_details = 0;
-          }
-#endif
-          return CreateSecurityDescription(
-              SecuritySummaryColor::GREEN, IDS_PAGE_INFO_SECURE_SUMMARY,
-              secure_details, SecurityDescriptionType::CONNECTION);
+          // Do not show details for secure connections.
+          return CreateSecurityDescription(SecuritySummaryColor::GREEN,
+                                           IDS_PAGE_INFO_SECURE_SUMMARY, 0,
+                                           SecurityDescriptionType::CONNECTION);
       }
     case PageInfo::SITE_IDENTITY_STATUS_DEPRECATED_SIGNATURE_ALGORITHM:
     case PageInfo::SITE_IDENTITY_STATUS_UNKNOWN:
     case PageInfo::SITE_IDENTITY_STATUS_NO_CERT:
     default:
-      return CreateSecurityDescription(
-          SecuritySummaryColor::RED,
-          page_info_v2_enabled ? IDS_PAGE_INFO_NOT_SECURE_SUMMARY_SHORT
-                               : IDS_PAGE_INFO_NOT_SECURE_SUMMARY,
-          IDS_PAGE_INFO_NOT_SECURE_DETAILS,
-          SecurityDescriptionType::CONNECTION);
+      return CreateSecurityDescription(SecuritySummaryColor::RED,
+                                       IDS_PAGE_INFO_NOT_SECURE_SUMMARY_SHORT,
+                                       IDS_PAGE_INFO_NOT_SECURE_DETAILS,
+                                       SecurityDescriptionType::CONNECTION);
+#else
+    case PageInfo::SITE_IDENTITY_STATUS_INTERNAL_PAGE:
+      // Internal pages on desktop have their own UI implementations which
+      // should never call this function.
+      NOTREACHED();
+      FALLTHROUGH;
+    case PageInfo::SITE_IDENTITY_STATUS_EV_CERT:
+    case PageInfo::SITE_IDENTITY_STATUS_CERT:
+    case PageInfo::SITE_IDENTITY_STATUS_ADMIN_PROVIDED_CERT:
+      switch (identity_info.connection_status) {
+        case PageInfo::SITE_CONNECTION_STATUS_INSECURE_ACTIVE_SUBRESOURCE:
+          return CreateSecurityDescription(SecuritySummaryColor::RED,
+                                           IDS_PAGE_INFO_NOT_SECURE_SUMMARY,
+                                           IDS_PAGE_INFO_NOT_SECURE_DETAILS,
+                                           SecurityDescriptionType::CONNECTION);
+        case PageInfo::SITE_CONNECTION_STATUS_INSECURE_FORM_ACTION:
+          return CreateSecurityDescription(SecuritySummaryColor::RED,
+                                           IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY,
+                                           IDS_PAGE_INFO_NOT_SECURE_DETAILS,
+                                           SecurityDescriptionType::CONNECTION);
+        case PageInfo::SITE_CONNECTION_STATUS_INSECURE_PASSIVE_SUBRESOURCE:
+          return CreateSecurityDescription(SecuritySummaryColor::RED,
+                                           IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY,
+                                           IDS_PAGE_INFO_MIXED_CONTENT_DETAILS,
+                                           SecurityDescriptionType::CONNECTION);
+        case PageInfo::SITE_CONNECTION_STATUS_LEGACY_TLS:
+          return CreateSecurityDescription(SecuritySummaryColor::RED,
+                                           IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY,
+                                           IDS_PAGE_INFO_LEGACY_TLS_DETAILS,
+                                           SecurityDescriptionType::CONNECTION);
+        default:
+          return CreateSecurityDescription(SecuritySummaryColor::GREEN,
+                                           IDS_PAGE_INFO_SECURE_SUMMARY,
+                                           IDS_PAGE_INFO_SECURE_DETAILS,
+                                           SecurityDescriptionType::CONNECTION);
+      }
+    case PageInfo::SITE_IDENTITY_STATUS_DEPRECATED_SIGNATURE_ALGORITHM:
+    case PageInfo::SITE_IDENTITY_STATUS_UNKNOWN:
+    case PageInfo::SITE_IDENTITY_STATUS_NO_CERT:
+    default:
+      return CreateSecurityDescription(SecuritySummaryColor::RED,
+                                       IDS_PAGE_INFO_NOT_SECURE_SUMMARY,
+                                       IDS_PAGE_INFO_NOT_SECURE_DETAILS,
+                                       SecurityDescriptionType::CONNECTION);
+#endif
   }
 }
 
@@ -525,95 +545,39 @@
 #if defined(OS_ANDROID)
 // static
 int PageInfoUI::GetIdentityIconID(PageInfo::SiteIdentityStatus status) {
-  if (base::FeatureList::IsEnabled(page_info::kPageInfoV2)) {
-    switch (status) {
-      case PageInfo::SITE_IDENTITY_STATUS_UNKNOWN:
-      case PageInfo::SITE_IDENTITY_STATUS_INTERNAL_PAGE:
-      case PageInfo::SITE_IDENTITY_STATUS_CERT:
-      case PageInfo::SITE_IDENTITY_STATUS_EV_CERT:
-        return IDR_PAGEINFO_GOOD_V2;
-      case PageInfo::SITE_IDENTITY_STATUS_NO_CERT:
-      case PageInfo::SITE_IDENTITY_STATUS_ERROR:
-      case PageInfo::SITE_IDENTITY_STATUS_ADMIN_PROVIDED_CERT:
-      case PageInfo::SITE_IDENTITY_STATUS_DEPRECATED_SIGNATURE_ALGORITHM:
-        return IDR_PAGEINFO_BAD_V2;
-    }
-
-    return 0;
-  }
-
-  int resource_id = IDR_PAGEINFO_INFO;
   switch (status) {
     case PageInfo::SITE_IDENTITY_STATUS_UNKNOWN:
     case PageInfo::SITE_IDENTITY_STATUS_INTERNAL_PAGE:
-      break;
     case PageInfo::SITE_IDENTITY_STATUS_CERT:
     case PageInfo::SITE_IDENTITY_STATUS_EV_CERT:
-      resource_id = IDR_PAGEINFO_GOOD;
-      break;
+      return IDR_PAGEINFO_GOOD;
     case PageInfo::SITE_IDENTITY_STATUS_NO_CERT:
-      resource_id = IDR_PAGEINFO_WARNING_MAJOR;
-      break;
     case PageInfo::SITE_IDENTITY_STATUS_ERROR:
-      resource_id = IDR_PAGEINFO_BAD;
-      break;
     case PageInfo::SITE_IDENTITY_STATUS_ADMIN_PROVIDED_CERT:
-      resource_id = IDR_PAGEINFO_ENTERPRISE_MANAGED;
-      break;
     case PageInfo::SITE_IDENTITY_STATUS_DEPRECATED_SIGNATURE_ALGORITHM:
-      resource_id = IDR_PAGEINFO_WARNING_MINOR;
-      break;
-    default:
-      NOTREACHED();
-      break;
+      return IDR_PAGEINFO_BAD;
   }
 
-  return resource_id;
+  return 0;
 }
 
 // static
 int PageInfoUI::GetConnectionIconID(PageInfo::SiteConnectionStatus status) {
-  if (base::FeatureList::IsEnabled(page_info::kPageInfoV2)) {
-    switch (status) {
-      case PageInfo::SITE_CONNECTION_STATUS_UNKNOWN:
-      case PageInfo::SITE_CONNECTION_STATUS_INTERNAL_PAGE:
-      case PageInfo::SITE_CONNECTION_STATUS_ENCRYPTED:
-        return IDR_PAGEINFO_GOOD_V2;
-      case PageInfo::SITE_CONNECTION_STATUS_INSECURE_PASSIVE_SUBRESOURCE:
-      case PageInfo::SITE_CONNECTION_STATUS_INSECURE_FORM_ACTION:
-      case PageInfo::SITE_CONNECTION_STATUS_LEGACY_TLS:
-      case PageInfo::SITE_CONNECTION_STATUS_UNENCRYPTED:
-      case PageInfo::SITE_CONNECTION_STATUS_INSECURE_ACTIVE_SUBRESOURCE:
-      case PageInfo::SITE_CONNECTION_STATUS_ENCRYPTED_ERROR:
-        return IDR_PAGEINFO_BAD_V2;
-    }
-
-    return 0;
-  }
-  int resource_id = IDR_PAGEINFO_INFO;
-
   switch (status) {
     case PageInfo::SITE_CONNECTION_STATUS_UNKNOWN:
     case PageInfo::SITE_CONNECTION_STATUS_INTERNAL_PAGE:
-      break;
     case PageInfo::SITE_CONNECTION_STATUS_ENCRYPTED:
-      resource_id = IDR_PAGEINFO_GOOD;
-      break;
+      return IDR_PAGEINFO_GOOD;
     case PageInfo::SITE_CONNECTION_STATUS_INSECURE_PASSIVE_SUBRESOURCE:
     case PageInfo::SITE_CONNECTION_STATUS_INSECURE_FORM_ACTION:
     case PageInfo::SITE_CONNECTION_STATUS_LEGACY_TLS:
-      resource_id = IDR_PAGEINFO_WARNING_MINOR;
-      break;
     case PageInfo::SITE_CONNECTION_STATUS_UNENCRYPTED:
-      resource_id = IDR_PAGEINFO_WARNING_MAJOR;
-      break;
     case PageInfo::SITE_CONNECTION_STATUS_INSECURE_ACTIVE_SUBRESOURCE:
     case PageInfo::SITE_CONNECTION_STATUS_ENCRYPTED_ERROR:
-      resource_id = IDR_PAGEINFO_BAD;
-      break;
+      return IDR_PAGEINFO_BAD;
   }
 
-  return resource_id;
+  return 0;
 }
 
 int PageInfoUI::GetIdentityIconColorID(PageInfo::SiteIdentityStatus status) {
diff --git a/components/password_manager/core/browser/android_affiliation/android_affiliation_service.h b/components/password_manager/core/browser/android_affiliation/android_affiliation_service.h
index ce20126..1c28ef3 100644
--- a/components/password_manager/core/browser/android_affiliation/android_affiliation_service.h
+++ b/components/password_manager/core/browser/android_affiliation/android_affiliation_service.h
@@ -5,8 +5,6 @@
 #ifndef COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_ANDROID_AFFILIATION_ANDROID_AFFILIATION_SERVICE_H_
 #define COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_ANDROID_AFFILIATION_ANDROID_AFFILIATION_SERVICE_H_
 
-#include <string>
-
 #include "base/callback.h"
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
diff --git a/components/password_manager/core/browser/credential_cache.h b/components/password_manager/core/browser/credential_cache.h
index cbb13ad..832d7c85 100644
--- a/components/password_manager/core/browser/credential_cache.h
+++ b/components/password_manager/core/browser/credential_cache.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_CREDENTIAL_CACHE_H_
 #define COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_CREDENTIAL_CACHE_H_
 
-#include <string>
 #include <vector>
 
 #include "base/types/strong_alias.h"
diff --git a/components/password_manager/core/browser/http_auth_manager_impl.h b/components/password_manager/core/browser/http_auth_manager_impl.h
index be88d5e..1bbb4ad 100644
--- a/components/password_manager/core/browser/http_auth_manager_impl.h
+++ b/components/password_manager/core/browser/http_auth_manager_impl.h
@@ -7,7 +7,6 @@
 
 #include <map>
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "components/password_manager/core/browser/browser_save_password_progress_logger.h"
diff --git a/components/password_manager/core/browser/import/password_importer.h b/components/password_manager/core/browser/import/password_importer.h
index 1517bef..a412ea8 100644
--- a/components/password_manager/core/browser/import/password_importer.h
+++ b/components/password_manager/core/browser/import/password_importer.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_PASSWORD_IMPORTER_H_
 #define COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_PASSWORD_IMPORTER_H_
 
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/password_manager/core/browser/leak_detection/leak_detection_check_factory_impl.h b/components/password_manager/core/browser/leak_detection/leak_detection_check_factory_impl.h
index f1f69a0..65358611 100644
--- a/components/password_manager/core/browser/leak_detection/leak_detection_check_factory_impl.h
+++ b/components/password_manager/core/browser/leak_detection/leak_detection_check_factory_impl.h
@@ -5,8 +5,6 @@
 #ifndef COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_LEAK_DETECTION_LEAK_DETECTION_CHECK_FACTORY_IMPL_H_
 #define COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_LEAK_DETECTION_LEAK_DETECTION_CHECK_FACTORY_IMPL_H_
 
-#include <string>
-
 #include "base/macros.h"
 #include "components/password_manager/core/browser/leak_detection/leak_detection_check_factory.h"
 #include "url/gurl.h"
diff --git a/components/password_manager/core/browser/password_form_filling.h b/components/password_manager/core/browser/password_form_filling.h
index 52d143d..2cb828c4 100644
--- a/components/password_manager/core/browser/password_form_filling.h
+++ b/components/password_manager/core/browser/password_form_filling.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_PASSWORD_FORM_FILLING_H_
 #define COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_PASSWORD_FORM_FILLING_H_
 
-#include <string>
 #include <vector>
 
 #include "base/memory/weak_ptr.h"
diff --git a/components/password_manager/core/browser/password_reuse_detector_consumer.h b/components/password_manager/core/browser/password_reuse_detector_consumer.h
index f7a499a3..76b4dc50 100644
--- a/components/password_manager/core/browser/password_reuse_detector_consumer.h
+++ b/components/password_manager/core/browser/password_reuse_detector_consumer.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_PASSWORD_REUSE_DETECTOR_CONSUMER_H_
 #define COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_PASSWORD_REUSE_DETECTOR_CONSUMER_H_
 
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/payments/core/payment_address.h b/components/payments/core/payment_address.h
index b29b7d4..26e39ec 100644
--- a/components/payments/core/payment_address.h
+++ b/components/payments/core/payment_address.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_PAYMENTS_CORE_PAYMENT_ADDRESS_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "components/payments/mojom/payment_request_data.mojom.h"
diff --git a/components/payments/core/payment_currency_amount.h b/components/payments/core/payment_currency_amount.h
index 48919c2f..5f72d656 100644
--- a/components/payments/core/payment_currency_amount.h
+++ b/components/payments/core/payment_currency_amount.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_PAYMENTS_CORE_PAYMENT_CURRENCY_AMOUNT_H_
 
 #include <memory>
-#include <string>
 
 #include "components/payments/mojom/payment_request_data.mojom.h"
 
diff --git a/components/payments/core/payment_details_modifier.h b/components/payments/core/payment_details_modifier.h
index d749236e..8e8132da 100644
--- a/components/payments/core/payment_details_modifier.h
+++ b/components/payments/core/payment_details_modifier.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_PAYMENTS_CORE_PAYMENT_DETAILS_MODIFIER_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "components/payments/core/payment_item.h"
diff --git a/components/pdf/renderer/BUILD.gn b/components/pdf/renderer/BUILD.gn
index 49fa2d3..48d09ca 100644
--- a/components/pdf/renderer/BUILD.gn
+++ b/components/pdf/renderer/BUILD.gn
@@ -3,6 +3,9 @@
 # found in the LICENSE file.
 
 import("//build/config/features.gni")
+import("//pdf/features.gni")
+
+assert(enable_pdf)
 
 static_library("renderer") {
   sources = [
diff --git a/components/pdf/renderer/pdf_accessibility_tree.h b/components/pdf/renderer/pdf_accessibility_tree.h
index a7cca7d..710f243 100644
--- a/components/pdf/renderer/pdf_accessibility_tree.h
+++ b/components/pdf/renderer/pdf_accessibility_tree.h
@@ -7,7 +7,6 @@
 
 #include <map>
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "content/public/renderer/plugin_ax_tree_source.h"
diff --git a/components/performance_manager/performance_manager_registry_impl.h b/components/performance_manager/performance_manager_registry_impl.h
index 290d19d7..309c702f 100644
--- a/components/performance_manager/performance_manager_registry_impl.h
+++ b/components/performance_manager/performance_manager_registry_impl.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_PERFORMANCE_MANAGER_PERFORMANCE_MANAGER_REGISTRY_IMPL_H_
 
 #include <memory>
-#include <string>
 
 #include "base/containers/flat_map.h"
 #include "base/containers/flat_set.h"
diff --git a/components/performance_manager/performance_manager_tab_helper.h b/components/performance_manager/performance_manager_tab_helper.h
index 8ffb2c7e..6ded9df 100644
--- a/components/performance_manager/performance_manager_tab_helper.h
+++ b/components/performance_manager/performance_manager_tab_helper.h
@@ -7,7 +7,6 @@
 
 #include <map>
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/performance_manager/persistence/site_data/site_data_cache_inspector.h b/components/performance_manager/persistence/site_data/site_data_cache_inspector.h
index 70f9f40..c4294a6 100644
--- a/components/performance_manager/persistence/site_data/site_data_cache_inspector.h
+++ b/components/performance_manager/persistence/site_data/site_data_cache_inspector.h
@@ -7,7 +7,6 @@
 
 #include <cstdint>
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/performance_manager/v8_memory/v8_detailed_memory_unittest.cc b/components/performance_manager/v8_memory/v8_detailed_memory_unittest.cc
index 526540f..abe7b8fd 100644
--- a/components/performance_manager/v8_memory/v8_detailed_memory_unittest.cc
+++ b/components/performance_manager/v8_memory/v8_detailed_memory_unittest.cc
@@ -23,6 +23,7 @@
 #include "base/test/test_timeouts.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
+#include "build/build_config.h"
 #include "components/performance_manager/graph/frame_node_impl.h"
 #include "components/performance_manager/graph/page_node_impl.h"
 #include "components/performance_manager/graph/process_node_impl.h"
@@ -1345,7 +1346,13 @@
   memory_request.RemoveObserver(&observer);
 }
 
-TEST_F(V8DetailedMemoryDecoratorTest, SingleProcessRequest) {
+// TODO(crbug.com/1203439) Sometimes timing out on Windows.
+#if defined(OS_WIN)
+#define MAYBE_SingleProcessRequest DISABLED_SingleProcessRequest
+#else
+#define MAYBE_SingleProcessRequest SingleProcessRequest
+#endif
+TEST_F(V8DetailedMemoryDecoratorTest, MAYBE_SingleProcessRequest) {
   // Create 2 renderer processes. Create one request that measures both of
   // them, and one request that measures only one.
   constexpr RenderProcessHostId kProcessId1 = RenderProcessHostId(0xFAB);
diff --git a/components/permissions/permission_prompt.h b/components/permissions/permission_prompt.h
index a1e3dec7..d671876 100644
--- a/components/permissions/permission_prompt.h
+++ b/components/permissions/permission_prompt.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_PERMISSIONS_PERMISSION_PROMPT_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/policy/core/browser/url_blocklist_manager.h b/components/policy/core/browser/url_blocklist_manager.h
index d1713d7c..f87e4fe 100644
--- a/components/policy/core/browser/url_blocklist_manager.h
+++ b/components/policy/core/browser/url_blocklist_manager.h
@@ -10,7 +10,6 @@
 
 #include <map>
 #include <memory>
-#include <string>
 
 #include "base/compiler_specific.h"
 #include "base/macros.h"
diff --git a/components/policy/core/common/policy_bundle.h b/components/policy/core/common/policy_bundle.h
index 955e34df..03bb4f8 100644
--- a/components/policy/core/common/policy_bundle.h
+++ b/components/policy/core/common/policy_bundle.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_POLICY_CORE_COMMON_POLICY_BUNDLE_H_
 
 #include <map>
-#include <string>
 
 #include "base/macros.h"
 #include "components/policy/core/common/policy_map.h"
diff --git a/components/policy/core/common/policy_service_impl.h b/components/policy/core/common/policy_service_impl.h
index f9dec79..286a304 100644
--- a/components/policy/core/common/policy_service_impl.h
+++ b/components/policy/core/common/policy_service_impl.h
@@ -8,7 +8,6 @@
 #include <map>
 #include <memory>
 #include <set>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/query_tiles/test/test_utils.h b/components/query_tiles/test/test_utils.h
index eae4b61..69766de2 100644
--- a/components/query_tiles/test/test_utils.h
+++ b/components/query_tiles/test/test_utils.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_QUERY_TILES_TEST_TEST_UTILS_H_
 #define COMPONENTS_QUERY_TILES_TEST_TEST_UTILS_H_
 
-#include <string>
 #include <vector>
 
 #include "components/query_tiles/internal/tile_group.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 73e4509..e301610a 100644
--- a/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
+++ b/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
@@ -354,11 +354,7 @@
     return;
   if (![self _isConsideredOpenForPersistentState])
     return;
-  // By the time state is restored, we don't want to miniaturize. Windows will
-  // be properly restored as miniaturized despite us not saving this.
-  // See https://crbug.com/1204517 for context/details.
-  if ([self isMiniaturized])
-    return;
+
   base::scoped_nsobject<NSMutableData> restorableStateData(
       [[NSMutableData alloc] init]);
   base::scoped_nsobject<NSKeyedArchiver> encoder([[NSKeyedArchiver alloc]
diff --git a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h
index 253e773c..b4655c1c 100644
--- a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h
+++ b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h
@@ -293,6 +293,9 @@
   // Returns true if capture exists and is currently active.
   bool HasCapture();
 
+  // Returns true if window restoration data exists from session restore.
+  bool HasWindowRestorationData();
+
   // CocoaMouseCaptureDelegate:
   void PostCapturedEvent(NSEvent* event) override;
   void OnMouseCaptureLost() override;
@@ -383,7 +386,7 @@
   bool invalidate_shadow_on_frame_swap_ = false;
 
   // A blob representing the window's saved state, which is applied and cleared
-  // the first time it's shown.
+  // on the first call to SetVisibilityState().
   std::vector<uint8_t> pending_restoration_data_;
 
   mojo::AssociatedReceiver<remote_cocoa::mojom::NativeWidgetNSWindow>
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 f20d8147..def66d5 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
@@ -654,6 +654,33 @@
 
 void NativeWidgetNSWindowBridge::SetVisibilityState(
     WindowVisibilityState new_state) {
+  // During session restore this method gets called from RestoreTabsToBrowser()
+  // with new_state = kShowAndActivateWindow. We consume restoration data on our
+  // first time through this method so we can use its existence as an
+  // indication that session restoration is underway. We'll use this later to
+  // decide whether or not to actually honor the WindowVisibilityState change
+  // request. This window may live in the dock, for example, in which case we
+  // don't really want to kShowAndActivateWindow. Even if the window is on the
+  // desktop we still won't want to kShowAndActivateWindow because doing so
+  // might trigger a transition to a different space (and we don't want to
+  // switch spaces on start-up). When session restore determines the Active
+  // window it will also call SetVisibilityState(), on that pass the window
+  // can/will be activated.
+  bool session_restore_in_progress = false;
+
+  // Restore Cocoa window state.
+  if (HasWindowRestorationData()) {
+    NSData* restore_ns_data =
+        [NSData dataWithBytes:pending_restoration_data_.data()
+                       length:pending_restoration_data_.size()];
+    base::scoped_nsobject<NSKeyedUnarchiver> decoder(
+        [[NSKeyedUnarchiver alloc] initForReadingWithData:restore_ns_data]);
+    [window_ restoreStateWithCoder:decoder];
+    pending_restoration_data_.clear();
+
+    session_restore_in_progress = true;
+  }
+
   // Ensure that:
   //  - A window with an invisible parent is not made visible.
   //  - A parent changing visibility updates child window visibility.
@@ -695,20 +722,11 @@
       return;
   }
 
-  if (!pending_restoration_data_.empty()) {
-    NSData* restore_ns_data =
-        [NSData dataWithBytes:pending_restoration_data_.data()
-                       length:pending_restoration_data_.size()];
-    base::scoped_nsobject<NSKeyedUnarchiver> decoder(
-        [[NSKeyedUnarchiver alloc] initForReadingWithData:restore_ns_data]);
-    [window_ restoreStateWithCoder:decoder];
-    pending_restoration_data_.clear();
-
-    // When first showing a window with restoration data, don't activate it.
-    // This avoids switching spaces or un-miniaturizing it right away.
-    // Additional activations act normally.
-    if (new_state == WindowVisibilityState::kShowAndActivateWindow)
-      new_state = WindowVisibilityState::kShowInactive;
+  // Don't activate a window during session restore, to avoid switching spaces
+  // (or pulling it out of the dock) during startup.
+  if (session_restore_in_progress &&
+      new_state == WindowVisibilityState::kShowAndActivateWindow) {
+    new_state = WindowVisibilityState::kShowInactive;
   }
 
   if (IsWindowModalSheet()) {
@@ -780,6 +798,10 @@
   return mouse_capture_ && mouse_capture_->IsActive();
 }
 
+bool NativeWidgetNSWindowBridge::HasWindowRestorationData() {
+  return !pending_restoration_data_.empty();
+}
+
 bool NativeWidgetNSWindowBridge::RunMoveLoop(const gfx::Vector2d& drag_offset) {
   // https://crbug.com/876493
   CHECK(!HasCapture());
diff --git a/components/reporting/client/report_queue_provider.h b/components/reporting/client/report_queue_provider.h
index 11e9332..5d961f6 100644
--- a/components/reporting/client/report_queue_provider.h
+++ b/components/reporting/client/report_queue_provider.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_REPORTING_CLIENT_REPORT_QUEUE_PROVIDER_H_
 
 #include <memory>
-#include <string>
 #include <utility>
 
 #include "base/callback.h"
diff --git a/components/reporting/storage/storage.h b/components/reporting/storage/storage.h
index 1be689f..6578d5d7 100644
--- a/components/reporting/storage/storage.h
+++ b/components/reporting/storage/storage.h
@@ -7,7 +7,6 @@
 
 #include <map>
 #include <memory>
-#include <string>
 #include <utility>
 
 #include "base/callback.h"
diff --git a/components/reporting/util/statusor.h b/components/reporting/util/statusor.h
index 89fdfbd..f287281f 100644
--- a/components/reporting/util/statusor.h
+++ b/components/reporting/util/statusor.h
@@ -57,7 +57,6 @@
 #define COMPONENTS_REPORTING_UTIL_STATUSOR_H_
 
 #include <new>
-#include <string>
 #include <type_traits>
 #include <utility>
 
diff --git a/components/reputation/core/safety_tip_test_utils.cc b/components/reputation/core/safety_tip_test_utils.cc
index 165e09c..1b45d901 100644
--- a/components/reputation/core/safety_tip_test_utils.cc
+++ b/components/reputation/core/safety_tip_test_utils.cc
@@ -53,13 +53,16 @@
 }
 
 void SetSafetyTipAllowlistPatterns(std::vector<std::string> patterns,
-                                   std::vector<std::string> target_patterns) {
+                                   std::vector<std::string> target_patterns,
+                                   std::vector<std::string> common_words) {
   auto config_proto = GetConfig();
   config_proto->clear_allowed_pattern();
   config_proto->clear_allowed_target_pattern();
+  config_proto->clear_common_word();
 
   std::sort(patterns.begin(), patterns.end());
   std::sort(target_patterns.begin(), target_patterns.end());
+  std::sort(common_words.begin(), common_words.end());
 
   for (const auto& pattern : patterns) {
     UrlPattern* page = config_proto->add_allowed_pattern();
@@ -69,11 +72,14 @@
     HostPattern* page = config_proto->add_allowed_target_pattern();
     page->set_regex(pattern);
   }
+  for (const auto& word : common_words) {
+    config_proto->add_common_word(word);
+  }
   SetSafetyTipsRemoteConfigProto(std::move(config_proto));
 }
 
 void InitializeBlankLookalikeAllowlistForTesting() {
-  SetSafetyTipAllowlistPatterns({}, {});
+  SetSafetyTipAllowlistPatterns({}, {}, {});
 }
 
 }  // namespace reputation
diff --git a/components/reputation/core/safety_tip_test_utils.h b/components/reputation/core/safety_tip_test_utils.h
index 666f97f..58b8065 100644
--- a/components/reputation/core/safety_tip_test_utils.h
+++ b/components/reputation/core/safety_tip_test_utils.h
@@ -31,7 +31,8 @@
 // |target_patterns| is the list of hostname regexes allowed to be targets of
 // lookalikes.
 void SetSafetyTipAllowlistPatterns(std::vector<std::string> patterns,
-                                   std::vector<std::string> target_patterns);
+                                   std::vector<std::string> target_patterns,
+                                   std::vector<std::string> common_words);
 
 // Ensure that the allowlist has been initialized. This is important as some
 // code (e.g. the elision policy) is fail-open (i.e. it won't elide without an
diff --git a/components/reputation/core/safety_tips.proto b/components/reputation/core/safety_tips.proto
index b14bfaa3..44b5b520 100644
--- a/components/reputation/core/safety_tips.proto
+++ b/components/reputation/core/safety_tips.proto
@@ -65,4 +65,9 @@
   //   In these cases it's simpler to allowlist the target instead of the
   //   embedder.
   repeated HostPattern allowed_target_pattern = 4;
+
+  // A *sorted* list of common words. These words are combined with the list at
+  // components/url_formatter/spoof_checks/common_words. The combined list is
+  // used in some lookalike heuristics to prevent common false positives.
+  repeated string common_word = 5;
 }
diff --git a/components/reputation/core/safety_tips_config.cc b/components/reputation/core/safety_tips_config.cc
index e3cf98d..a3ef886 100644
--- a/components/reputation/core/safety_tips_config.cc
+++ b/components/reputation/core/safety_tips_config.cc
@@ -5,6 +5,7 @@
 #include "components/reputation/core/safety_tips_config.h"
 
 #include "base/no_destructor.h"
+#include "base/ranges/algorithm.h"
 #include "components/safe_browsing/core/db/v4_protocol_manager_util.h"
 #include "third_party/re2/src/re2/re2.h"
 #include "url/gurl.h"
@@ -169,4 +170,20 @@
   return security_state::SafetyTipStatus::kNone;
 }
 
+bool IsCommonWordInConfigProto(const SafetyTipsConfig* proto,
+                               const std::string& word) {
+  // proto is nullptr when running in non-Lookalike tests.
+  if (proto == nullptr) {
+    return false;
+  }
+
+  auto common_words = proto->common_word();
+  DCHECK(base::ranges::is_sorted(common_words.begin(), common_words.end()));
+  auto lower = std::lower_bound(
+      common_words.begin(), common_words.end(), word,
+      [](const std::string& a, const std::string& b) -> bool { return a < b; });
+
+  return lower != common_words.end() && word == *lower;
+}
+
 }  // namespace reputation
diff --git a/components/reputation/core/safety_tips_config.h b/components/reputation/core/safety_tips_config.h
index 532c0dc..9d0d406 100644
--- a/components/reputation/core/safety_tips_config.h
+++ b/components/reputation/core/safety_tips_config.h
@@ -42,6 +42,10 @@
 // in sorted order.
 security_state::SafetyTipStatus GetSafetyTipUrlBlockType(const GURL& url);
 
+// Returns whether |word| is included in the component updater common word list
+bool IsCommonWordInConfigProto(const SafetyTipsConfig* proto,
+                               const std::string& word);
+
 }  // namespace reputation
 
 #endif  // COMPONENTS_REPUTATION_CORE_SAFETY_TIPS_CONFIG_H_
diff --git a/components/reputation/core/safety_tips_config_unittest.cc b/components/reputation/core/safety_tips_config_unittest.cc
index 792f54c..4c7756e 100644
--- a/components/reputation/core/safety_tips_config_unittest.cc
+++ b/components/reputation/core/safety_tips_config_unittest.cc
@@ -13,7 +13,7 @@
 namespace reputation {
 
 TEST(SafetyTipsConfigTest, TestUrlAllowlist) {
-  SetSafetyTipAllowlistPatterns({"example.com/"}, {});
+  SetSafetyTipAllowlistPatterns({"example.com/"}, {}, {});
   auto* config = GetSafetyTipsRemoteConfigProto();
   EXPECT_TRUE(IsUrlAllowlistedBySafetyTipsComponent(
       config, GURL("http://example.com")));
@@ -22,7 +22,7 @@
 }
 
 TEST(SafetyTipsConfigTest, TestTargetUrlAllowlist) {
-  SetSafetyTipAllowlistPatterns({}, {"exa.*\\.com"});
+  SetSafetyTipAllowlistPatterns({}, {"exa.*\\.com"}, {});
   auto* config = GetSafetyTipsRemoteConfigProto();
   EXPECT_TRUE(
       IsTargetHostAllowlistedBySafetyTipsComponent(config, "example.com"));
@@ -30,4 +30,14 @@
       IsTargetHostAllowlistedBySafetyTipsComponent(config, "example.org"));
 }
 
+TEST(SafetyTipsConfigTest, TestCommonWords) {
+  // IsCommonWordInConfigProto does a binary search of sorted common words.
+  SetSafetyTipAllowlistPatterns({}, {}, {"common3", "common1", "common2"});
+  auto* config = GetSafetyTipsRemoteConfigProto();
+  EXPECT_TRUE(IsCommonWordInConfigProto(config, "common1"));
+  EXPECT_TRUE(IsCommonWordInConfigProto(config, "common2"));
+  EXPECT_TRUE(IsCommonWordInConfigProto(config, "common3"));
+  EXPECT_FALSE(IsCommonWordInConfigProto(config, "uncommon"));
+}
+
 }  // namespace reputation
diff --git a/components/resources/android/page_info_resource_id.h b/components/resources/android/page_info_resource_id.h
index dd09006..7ec3a91 100644
--- a/components/resources/android/page_info_resource_id.h
+++ b/components/resources/android/page_info_resource_id.h
@@ -24,23 +24,9 @@
 
 // PageInfoUI images, used in ConnectionInfoView
 // Good:
-DECLARE_RESOURCE_ID(IDR_PAGEINFO_GOOD, R.drawable.pageinfo_good)
-DECLARE_RESOURCE_ID(IDR_PAGEINFO_GOOD_V2, R.drawable.omnibox_https_valid)
-// Warnings:
-DECLARE_RESOURCE_ID(IDR_PAGEINFO_WARNING_MINOR, R.drawable.pageinfo_warning)
+DECLARE_RESOURCE_ID(IDR_PAGEINFO_GOOD, R.drawable.omnibox_https_valid)
 // Bad:
-DECLARE_RESOURCE_ID(IDR_PAGEINFO_BAD, R.drawable.pageinfo_bad)
-DECLARE_RESOURCE_ID(IDR_PAGEINFO_BAD_V2, R.drawable.omnibox_not_secure_warning)
-// Should never occur, use warning just in case:
-// Enterprise managed: ChromeOS only.
-DECLARE_RESOURCE_ID(IDR_PAGEINFO_ENTERPRISE_MANAGED,
-                    R.drawable.pageinfo_warning)
-// Info: Only shown on chrome:// urls, which don't show the connection info
-// popup.
-DECLARE_RESOURCE_ID(IDR_PAGEINFO_INFO, R.drawable.pageinfo_warning)
-// Major warning: Used on insecure pages, which don't show the connection info
-// popup.
-DECLARE_RESOURCE_ID(IDR_PAGEINFO_WARNING_MAJOR, R.drawable.pageinfo_warning)
+DECLARE_RESOURCE_ID(IDR_PAGEINFO_BAD, R.drawable.omnibox_not_secure_warning)
 
 // PageInfoUI colors, used in ConnectionInfoView
 // Good:
diff --git a/components/safe_browsing/android/safe_browsing_api_handler.h b/components/safe_browsing/android/safe_browsing_api_handler.h
index a343c4b..b4be21c 100644
--- a/components/safe_browsing/android/safe_browsing_api_handler.h
+++ b/components/safe_browsing/android/safe_browsing_api_handler.h
@@ -9,7 +9,6 @@
 #define COMPONENTS_SAFE_BROWSING_ANDROID_SAFE_BROWSING_API_HANDLER_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/safe_browsing/android/safe_browsing_api_handler_bridge.h b/components/safe_browsing/android/safe_browsing_api_handler_bridge.h
index bcee335..4ba4ebb8 100644
--- a/components/safe_browsing/android/safe_browsing_api_handler_bridge.h
+++ b/components/safe_browsing/android/safe_browsing_api_handler_bridge.h
@@ -9,7 +9,6 @@
 
 #include <jni.h>
 
-#include <string>
 #include <vector>
 
 #include "base/android/jni_android.h"
diff --git a/components/safe_browsing/content/browser/client_side_phishing_model.cc b/components/safe_browsing/content/browser/client_side_phishing_model.cc
index b37fb01a..bf77c6d7 100644
--- a/components/safe_browsing/content/browser/client_side_phishing_model.cc
+++ b/components/safe_browsing/content/browser/client_side_phishing_model.cc
@@ -4,9 +4,11 @@
 
 #include "components/safe_browsing/content/browser/client_side_phishing_model.h"
 
+#include "base/command_line.h"
 #include "base/memory/singleton.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/task/post_task.h"
+#include "base/task/thread_pool.h"
 #include "components/safe_browsing/core/proto/client_model.pb.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
@@ -14,6 +16,29 @@
 
 namespace safe_browsing {
 
+namespace {
+
+// Command-line flag that can be used to override the current CSD model. Must be
+// provided with an absolute path.
+const char kOverrideCsdModelFlag[] = "csd-model-override-path";
+
+std::string ReadFileIntoString(base::FilePath path) {
+  if (path.empty())
+    return std::string();
+
+  base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
+  if (!file.IsValid())
+    return std::string();
+
+  std::vector<char> model_data(file.GetLength());
+  if (file.ReadAtCurrentPos(model_data.data(), model_data.size()) == -1)
+    return std::string();
+
+  return std::string(model_data.begin(), model_data.end());
+}
+
+}  // namespace
+
 using base::AutoLock;
 
 struct ClientSidePhishingModelSingletonTrait
@@ -32,7 +57,9 @@
                          ClientSidePhishingModelSingletonTrait>::get();
 }
 
-ClientSidePhishingModel::ClientSidePhishingModel() = default;
+ClientSidePhishingModel::ClientSidePhishingModel() {
+  MaybeOverrideModel();
+}
 
 ClientSidePhishingModel::~ClientSidePhishingModel() {
   AutoLock lock(lock_);  // DCHECK fail if the lock is held.
@@ -62,7 +89,9 @@
   AutoLock lock(lock_);
 
   bool proto_valid = false;
-  if (!model_str.empty()) {
+  if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
+          kOverrideCsdModelFlag) &&
+      !model_str.empty()) {
     ClientSideModel model_proto;
     proto_valid = model_proto.ParseFromString(model_str);
     base::UmaHistogramBoolean("SBClientPhishing.ModelDynamicUpdateSuccess",
@@ -108,4 +137,42 @@
   visual_tflite_model_ = std::move(file);
 }
 
+void ClientSidePhishingModel::MaybeOverrideModel() {
+  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
+          kOverrideCsdModelFlag)) {
+    base::FilePath overriden_model_path =
+        base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
+            kOverrideCsdModelFlag);
+
+    base::ThreadPool::PostTaskAndReplyWithResult(
+        FROM_HERE, {base::MayBlock()},
+        base::BindOnce(&ReadFileIntoString, overriden_model_path),
+        // base::Unretained is safe because this is a singleton.
+        base::BindOnce(&ClientSidePhishingModel::OnGetOverridenModelData,
+                       base::Unretained(this)));
+  }
+}
+
+void ClientSidePhishingModel::OnGetOverridenModelData(
+    const std::string& model_data) {
+  if (model_data.empty()) {
+    VLOG(2) << "Overriden model data is empty";
+    return;
+  }
+
+  std::unique_ptr<ClientSideModel> model(new ClientSideModel());
+  if (!model->ParseFromArray(model_data.data(), model_data.size())) {
+    VLOG(2) << "Overriden model data is not a valid ClientSideModel proto";
+    return;
+  }
+
+  VLOG(2) << "Model overriden successfully";
+  model_str_ = model_data;
+
+  // Unretained is safe because this is a singleton.
+  base::PostTask(FROM_HERE, {content::BrowserThread::UI},
+                 base::BindOnce(&ClientSidePhishingModel::NotifyCallbacksOnUI,
+                                base::Unretained(this)));
+}
+
 }  // namespace safe_browsing
diff --git a/components/safe_browsing/content/browser/client_side_phishing_model.h b/components/safe_browsing/content/browser/client_side_phishing_model.h
index bd0dde6..021162c 100644
--- a/components/safe_browsing/content/browser/client_side_phishing_model.h
+++ b/components/safe_browsing/content/browser/client_side_phishing_model.h
@@ -59,6 +59,12 @@
 
   void NotifyCallbacksOnUI();
 
+  // Called to check the command line and maybe override the current model.
+  void MaybeOverrideModel();
+
+  // Callback when the local file overriding the model has been read.
+  void OnGetOverridenModelData(const std::string& model_data);
+
   // The list of callbacks to notify when a new model is ready. Protected by
   // lock_. Will always be notified on the UI thread.
   base::RepeatingCallbackList<void()> callbacks_;
@@ -72,6 +78,7 @@
   mutable base::Lock lock_;
 
   friend struct ClientSidePhishingModelSingletonTrait;
+  FRIEND_TEST_ALL_PREFIXES(ClientSidePhishingModelTest, CanOverrideWithFlag);
 };
 
 }  // namespace safe_browsing
diff --git a/components/safe_browsing/content/browser/client_side_phishing_model_unittest.cc b/components/safe_browsing/content/browser/client_side_phishing_model_unittest.cc
index 7ce5927c2..66d3147 100644
--- a/components/safe_browsing/content/browser/client_side_phishing_model_unittest.cc
+++ b/components/safe_browsing/content/browser/client_side_phishing_model_unittest.cc
@@ -7,6 +7,7 @@
 #include "base/files/scoped_temp_dir.h"
 #include "base/logging.h"
 #include "base/run_loop.h"
+#include "base/test/scoped_command_line.h"
 #include "components/safe_browsing/core/proto/client_model.pb.h"
 #include "content/public/test/browser_task_environment.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -158,4 +159,42 @@
   EXPECT_TRUE(ClientSidePhishingModel::GetInstance()->IsEnabled());
 }
 
+TEST(ClientSidePhishingModelTest, CanOverrideWithFlag) {
+  base::ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+  base::FilePath file_path =
+      temp_dir.GetPath().AppendASCII("overridden_model.proto");
+  base::File file(file_path, base::File::FLAG_OPEN_ALWAYS |
+                                 base::File::FLAG_READ |
+                                 base::File::FLAG_WRITE);
+  ClientSideModel model_proto;
+  model_proto.set_version(123);
+  model_proto.set_max_words_per_term(0);  // Required field
+  std::string file_contents = model_proto.SerializeAsString();
+  file.WriteAtCurrentPos(file_contents.data(), file_contents.size());
+
+  base::test::ScopedCommandLine command_line;
+  command_line.GetProcessCommandLine()->AppendSwitchPath(
+      "--csd-model-override-path", file_path);
+
+  content::BrowserTaskEnvironment task_environment;
+  base::RunLoop run_loop;
+  bool called = false;
+  base::CallbackListSubscription subscription =
+      ClientSidePhishingModel::GetInstance()->RegisterCallback(
+          base::BindRepeating(
+              [](base::RepeatingClosure quit_closure, bool* called) {
+                *called = true;
+                std::move(quit_closure).Run();
+              },
+              run_loop.QuitClosure(), &called));
+
+  ClientSidePhishingModel::GetInstance()->MaybeOverrideModel();
+
+  run_loop.Run();
+
+  EXPECT_EQ(ClientSidePhishingModel::GetInstance()->GetModelStr(),
+            file_contents);
+}
+
 }  // namespace safe_browsing
diff --git a/components/safe_browsing/content/browser/threat_details_history.h b/components/safe_browsing/content/browser/threat_details_history.h
index 4f76efe..15fef3f 100644
--- a/components/safe_browsing/content/browser/threat_details_history.h
+++ b/components/safe_browsing/content/browser/threat_details_history.h
@@ -7,7 +7,6 @@
 
 // This class gets redirect chain for urls from the history service.
 
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/security_interstitials/content/cert_report_helper.h b/components/security_interstitials/content/cert_report_helper.h
index 41a72e9..64527b7d 100644
--- a/components/security_interstitials/content/cert_report_helper.h
+++ b/components/security_interstitials/content/cert_report_helper.h
@@ -5,8 +5,6 @@
 #ifndef COMPONENTS_SECURITY_INTERSTITIALS_CONTENT_CERT_REPORT_HELPER_H_
 #define COMPONENTS_SECURITY_INTERSTITIALS_CONTENT_CERT_REPORT_HELPER_H_
 
-#include <string>
-
 #include "base/macros.h"
 #include "components/security_interstitials/content/certificate_error_report.h"
 #include "components/security_interstitials/core/controller_client.h"
diff --git a/components/security_interstitials/content/content_metrics_helper.h b/components/security_interstitials/content/content_metrics_helper.h
index e073b96a..e6e2352 100644
--- a/components/security_interstitials/content/content_metrics_helper.h
+++ b/components/security_interstitials/content/content_metrics_helper.h
@@ -5,8 +5,6 @@
 #ifndef COMPONENTS_SECURITY_INTERSTITIALS_CONTENT_CONTENT_METRICS_HELPER_H_
 #define COMPONENTS_SECURITY_INTERSTITIALS_CONTENT_CONTENT_METRICS_HELPER_H_
 
-#include <string>
-
 #include "base/macros.h"
 #include "components/captive_portal/core/buildflags.h"
 #include "components/security_interstitials/core/metrics_helper.h"
diff --git a/components/send_tab_to_self/send_tab_to_self_sync_service.h b/components/send_tab_to_self/send_tab_to_self_sync_service.h
index 2dd9097..7e78060 100644
--- a/components/send_tab_to_self/send_tab_to_self_sync_service.h
+++ b/components/send_tab_to_self/send_tab_to_self_sync_service.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SEND_TAB_TO_SELF_SEND_TAB_TO_SELF_SYNC_SERVICE_H_
 
 #include <memory>
-#include <string>
 
 #include "base/memory/weak_ptr.h"
 #include "components/keyed_service/core/keyed_service.h"
diff --git a/components/services/storage/indexed_db/scopes/scope_lock.h b/components/services/storage/indexed_db/scopes/scope_lock.h
index 26067646..35cde17 100644
--- a/components/services/storage/indexed_db/scopes/scope_lock.h
+++ b/components/services/storage/indexed_db/scopes/scope_lock.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SERVICES_STORAGE_INDEXED_DB_SCOPES_SCOPE_LOCK_H_
 
 #include <iosfwd>
-#include <string>
 
 #include "base/callback.h"
 #include "base/callback_helpers.h"
diff --git a/components/services/storage/indexed_db/scopes/scopes_lock_manager.h b/components/services/storage/indexed_db/scopes/scopes_lock_manager.h
index 3af61e05..4622866 100644
--- a/components/services/storage/indexed_db/scopes/scopes_lock_manager.h
+++ b/components/services/storage/indexed_db/scopes/scopes_lock_manager.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SERVICES_STORAGE_INDEXED_DB_SCOPES_SCOPES_LOCK_MANAGER_H_
 
 #include <iosfwd>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/signin/core/browser/account_reconcilor.h b/components/signin/core/browser/account_reconcilor.h
index 0052728..fe573eeb 100644
--- a/components/signin/core/browser/account_reconcilor.h
+++ b/components/signin/core/browser/account_reconcilor.h
@@ -5,7 +5,6 @@
 #define COMPONENTS_SIGNIN_CORE_BROWSER_ACCOUNT_RECONCILOR_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/compiler_specific.h"
diff --git a/components/signin/core/browser/account_reconcilor_delegate.h b/components/signin/core/browser/account_reconcilor_delegate.h
index e6ad19f..7e6e287 100644
--- a/components/signin/core/browser/account_reconcilor_delegate.h
+++ b/components/signin/core/browser/account_reconcilor_delegate.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_SIGNIN_CORE_BROWSER_ACCOUNT_RECONCILOR_DELEGATE_H_
 #define COMPONENTS_SIGNIN_CORE_BROWSER_ACCOUNT_RECONCILOR_DELEGATE_H_
 
-#include <string>
 #include <vector>
 
 #include "base/time/time.h"
diff --git a/components/signin/core/browser/dice_account_reconcilor_delegate.h b/components/signin/core/browser/dice_account_reconcilor_delegate.h
index 1cb43ab..5563584 100644
--- a/components/signin/core/browser/dice_account_reconcilor_delegate.h
+++ b/components/signin/core/browser/dice_account_reconcilor_delegate.h
@@ -5,8 +5,6 @@
 #ifndef COMPONENTS_SIGNIN_CORE_BROWSER_DICE_ACCOUNT_RECONCILOR_DELEGATE_H_
 #define COMPONENTS_SIGNIN_CORE_BROWSER_DICE_ACCOUNT_RECONCILOR_DELEGATE_H_
 
-#include <string>
-
 #include "base/macros.h"
 #include "components/signin/core/browser/account_reconcilor_delegate.h"
 #include "components/signin/public/base/account_consistency_method.h"
diff --git a/components/signin/core/browser/mirror_account_reconcilor_delegate.h b/components/signin/core/browser/mirror_account_reconcilor_delegate.h
index 192325ee..08ff4ec 100644
--- a/components/signin/core/browser/mirror_account_reconcilor_delegate.h
+++ b/components/signin/core/browser/mirror_account_reconcilor_delegate.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_SIGNIN_CORE_BROWSER_MIRROR_ACCOUNT_RECONCILOR_DELEGATE_H_
 #define COMPONENTS_SIGNIN_CORE_BROWSER_MIRROR_ACCOUNT_RECONCILOR_DELEGATE_H_
 
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/signin/internal/identity_manager/child_account_info_fetcher_android.h b/components/signin/internal/identity_manager/child_account_info_fetcher_android.h
index e51b133..864e7347 100644
--- a/components/signin/internal/identity_manager/child_account_info_fetcher_android.h
+++ b/components/signin/internal/identity_manager/child_account_info_fetcher_android.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SIGNIN_INTERNAL_IDENTITY_MANAGER_CHILD_ACCOUNT_INFO_FETCHER_ANDROID_H_
 
 #include <jni.h>
-#include <string>
 
 #include "base/android/scoped_java_ref.h"
 #include "components/signin/public/identity_manager/account_info.h"
diff --git a/components/signin/internal/identity_manager/primary_account_manager.h b/components/signin/internal/identity_manager/primary_account_manager.h
index d310d5e..a87da77 100644
--- a/components/signin/internal/identity_manager/primary_account_manager.h
+++ b/components/signin/internal/identity_manager/primary_account_manager.h
@@ -19,7 +19,6 @@
 #define COMPONENTS_SIGNIN_INTERNAL_IDENTITY_MANAGER_PRIMARY_ACCOUNT_MANAGER_H_
 
 #include <memory>
-#include <string>
 
 #include "base/macros.h"
 #include "base/observer_list.h"
diff --git a/components/signin/ios/browser/account_consistency_service.h b/components/signin/ios/browser/account_consistency_service.h
index 263e3de0..7220a445 100644
--- a/components/signin/ios/browser/account_consistency_service.h
+++ b/components/signin/ios/browser/account_consistency_service.h
@@ -7,7 +7,6 @@
 
 #include <map>
 #include <set>
-#include <string>
 
 #include "base/callback.h"
 #include "base/macros.h"
diff --git a/components/signin/public/base/multilogin_parameters.h b/components/signin/public/base/multilogin_parameters.h
index ee90a46b..4859c7c 100644
--- a/components/signin/public/base/multilogin_parameters.h
+++ b/components/signin/public/base/multilogin_parameters.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_SIGNIN_PUBLIC_BASE_MULTILOGIN_PARAMETERS_H_
 #define COMPONENTS_SIGNIN_PUBLIC_BASE_MULTILOGIN_PARAMETERS_H_
 
-#include <string>
 #include <vector>
 
 #include "google_apis/gaia/core_account_id.h"
diff --git a/components/site_isolation/BUILD.gn b/components/site_isolation/BUILD.gn
index ee7127a3..464d684 100644
--- a/components/site_isolation/BUILD.gn
+++ b/components/site_isolation/BUILD.gn
@@ -49,6 +49,7 @@
   deps = [
     ":buildflags",
     "//base",
+    "//base/util/values:values_util",
     "//components/prefs",
     "//components/user_prefs",
     "//content/public/browser",
diff --git a/components/site_isolation/pref_names.cc b/components/site_isolation/pref_names.cc
index 403cf01..4bd35dd 100644
--- a/components/site_isolation/pref_names.cc
+++ b/components/site_isolation/pref_names.cc
@@ -8,10 +8,20 @@
 namespace prefs {
 
 // A list of origins that were heuristically determined to need process
-// isolation. For example, an origin may be placed on this list in response to
-// the user typing a password on it.
+// isolation due to an action triggered by the user. For example, an origin may
+// be placed on this list in response to the user typing a password on it.
 const char kUserTriggeredIsolatedOrigins[] =
     "site_isolation.user_triggered_isolated_origins";
 
+// A list of origins that were determined to need process isolation based on
+// heuristics triggered directly by web sites. For example, an origin may be
+// placed on this list in response to serving Cross-Origin-Opener-Policy
+// headers.  Unlike the user-triggered list above, web-triggered isolated
+// origins are subject to stricter size and eviction policies, to guard against
+// too many sites triggering isolation and to eventually stop isolation if web
+// sites stop serving headers that triggered it.
+const char kWebTriggeredIsolatedOrigins[] =
+    "site_isolation.web_triggered_isolated_origins";
+
 }  // namespace prefs
 }  // namespace site_isolation
diff --git a/components/site_isolation/pref_names.h b/components/site_isolation/pref_names.h
index ca61fa2..c390945 100644
--- a/components/site_isolation/pref_names.h
+++ b/components/site_isolation/pref_names.h
@@ -9,6 +9,7 @@
 namespace prefs {
 
 extern const char kUserTriggeredIsolatedOrigins[];
+extern const char kWebTriggeredIsolatedOrigins[];
 
 }  // namespace prefs
 }  // namespace site_isolation
diff --git a/components/site_isolation/site_isolation_policy.cc b/components/site_isolation/site_isolation_policy.cc
index f36a3478..615c7aa 100644
--- a/components/site_isolation/site_isolation_policy.cc
+++ b/components/site_isolation/site_isolation_policy.cc
@@ -5,8 +5,9 @@
 #include "components/site_isolation/site_isolation_policy.h"
 
 #include "base/metrics/field_trial_params.h"
-#include "base/metrics/histogram_macros.h"
+#include "base/metrics/histogram_functions.h"
 #include "base/system/sys_info.h"
+#include "base/util/values/values_util.h"
 #include "build/build_config.h"
 #include "components/prefs/pref_service.h"
 #include "components/prefs/scoped_user_pref_update.h"
@@ -16,9 +17,17 @@
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/site_instance.h"
 #include "content/public/browser/site_isolation_policy.h"
+#include "content/public/common/content_features.h"
 
 namespace site_isolation {
 
+namespace {
+
+using IsolatedOriginSource =
+    content::ChildProcessSecurityPolicy::IsolatedOriginSource;
+
+}  // namespace
+
 // static
 bool SiteIsolationPolicy::IsIsolationForPasswordSitesEnabled() {
   // If the user has explicitly enabled site isolation for password sites from
@@ -133,12 +142,30 @@
 void SiteIsolationPolicy::PersistIsolatedOrigin(
     content::BrowserContext* context,
     const url::Origin& origin,
-    content::ChildProcessSecurityPolicy::IsolatedOriginSource source) {
+    IsolatedOriginSource source) {
+  DCHECK(context);
   DCHECK(!context->IsOffTheRecord());
-  // TODO(alexmos): Support web-triggered IsolatedOriginSources.
-  DCHECK_EQ(source, content::ChildProcessSecurityPolicy::IsolatedOriginSource::
-                        USER_TRIGGERED);
+  DCHECK(!origin.opaque());
 
+  // This function currently supports two sources for persistence, for
+  // user-triggered and web-triggered isolated origins.
+  if (source == IsolatedOriginSource::USER_TRIGGERED) {
+    PersistUserTriggeredIsolatedOrigin(context, origin);
+  } else if (source == IsolatedOriginSource::WEB_TRIGGERED) {
+    PersistWebTriggeredIsolatedOrigin(context, origin);
+  } else {
+    NOTREACHED();
+  }
+}
+
+// static
+void SiteIsolationPolicy::PersistUserTriggeredIsolatedOrigin(
+    content::BrowserContext* context,
+    const url::Origin& origin) {
+  // User-triggered isolated origins are currently stored in a simple list of
+  // unlimited size.
+  // TODO(alexmos): Cap the maximum number of entries and evict older entries.
+  // See https://crbug.com/1172407.
   ListPrefUpdate update(user_prefs::UserPrefs::Get(context),
                         site_isolation::prefs::kUserTriggeredIsolatedOrigins);
   base::ListValue* list = update.Get();
@@ -148,31 +175,91 @@
 }
 
 // static
+void SiteIsolationPolicy::PersistWebTriggeredIsolatedOrigin(
+    content::BrowserContext* context,
+    const url::Origin& origin) {
+  // Web-triggered isolated origins are stored in a dictionary of (origin,
+  // timestamp) pairs.  The number of entries is capped by a field trial param,
+  // and older entries are evicted.
+  DictionaryPrefUpdate update(
+      user_prefs::UserPrefs::Get(context),
+      site_isolation::prefs::kWebTriggeredIsolatedOrigins);
+  base::DictionaryValue* dict = update.Get();
+
+  // Add the origin.  If it already exists, this will just update the
+  // timestamp.
+  dict->SetKey(origin.Serialize(), util::TimeToValue(base::Time::Now()));
+
+  // Check whether the maximum number of stored sites was exceeded and remove
+  // one or more entries, starting with the oldest timestamp. Note that more
+  // than one entry may need to be removed, since the maximum number of entries
+  // could change over time (via a change in the field trial param).
+  size_t max_size =
+      ::features::kSiteIsolationForCrossOriginOpenerPolicyMaxSitesParam.Get();
+  while (dict->DictSize() > max_size) {
+    auto items = dict->DictItems();
+    auto oldest_site_time_pair = std::min_element(
+        items.begin(), items.end(), [](auto pair_a, auto pair_b) {
+          absl::optional<base::Time> time_a = util::ValueToTime(pair_a.second);
+          absl::optional<base::Time> time_b = util::ValueToTime(pair_b.second);
+          // has_value() should always be true unless the prefs were corrupted.
+          // In that case, prioritize the corrupted entry for removal.
+          return (time_a.has_value() ? time_a.value() : base::Time::Min()) <
+                 (time_b.has_value() ? time_b.value() : base::Time::Min());
+        });
+    dict->RemoveKey(oldest_site_time_pair->first);
+  }
+}
+
+// static
 void SiteIsolationPolicy::ApplyPersistedIsolatedOrigins(
     content::BrowserContext* browser_context) {
+  auto* policy = content::ChildProcessSecurityPolicy::GetInstance();
+
   // If the user turned off password-triggered isolation, don't apply any
   // stored isolated origins, but also don't clear them from prefs, so that
   // they can be used if password-triggered isolation is re-enabled later.
-  if (!IsIsolationForPasswordSitesEnabled())
-    return;
+  if (IsIsolationForPasswordSitesEnabled()) {
+    std::vector<url::Origin> origins;
+    for (const auto& value : user_prefs::UserPrefs::Get(browser_context)
+                                 ->GetList(prefs::kUserTriggeredIsolatedOrigins)
+                                 ->GetList()) {
+      origins.push_back(url::Origin::Create(GURL(value.GetString())));
+    }
 
-  std::vector<url::Origin> origins;
-  for (const auto& value : user_prefs::UserPrefs::Get(browser_context)
-                               ->GetList(prefs::kUserTriggeredIsolatedOrigins)
-                               ->GetList()) {
-    origins.push_back(url::Origin::Create(GURL(value.GetString())));
+    if (!origins.empty()) {
+      policy->AddFutureIsolatedOrigins(
+          origins, IsolatedOriginSource::USER_TRIGGERED, browser_context);
+    }
+
+    base::UmaHistogramCounts1000(
+        "SiteIsolation.SavedUserTriggeredIsolatedOrigins.Size", origins.size());
   }
 
-  if (!origins.empty()) {
-    auto* policy = content::ChildProcessSecurityPolicy::GetInstance();
-    using IsolatedOriginSource =
-        content::ChildProcessSecurityPolicy::IsolatedOriginSource;
-    policy->AddFutureIsolatedOrigins(
-        origins, IsolatedOriginSource::USER_TRIGGERED, browser_context);
-  }
+  // Similarly, load saved web-triggered isolated origins only if isolation of
+  // COOP sites (currently the only source of these origins) is enabled with
+  // persistence, but don't remove them from prefs otherwise.
+  if (content::SiteIsolationPolicy::ShouldPersistIsolatedCOOPSites()) {
+    std::vector<url::Origin> origins;
 
-  UMA_HISTOGRAM_COUNTS_1000(
-      "SiteIsolation.SavedUserTriggeredIsolatedOrigins.Size", origins.size());
+    // TODO(alexmos): Implement support for skipping and expiring entries that
+    // are older than a predefined threshold.
+
+    auto* dict = user_prefs::UserPrefs::Get(browser_context)
+                     ->GetDictionary(prefs::kWebTriggeredIsolatedOrigins);
+    if (dict) {
+      for (const auto& site_time_pair : dict->DictItems())
+        origins.push_back(url::Origin::Create(GURL(site_time_pair.first)));
+    }
+
+    if (!origins.empty()) {
+      policy->AddFutureIsolatedOrigins(
+          origins, IsolatedOriginSource::WEB_TRIGGERED, browser_context);
+    }
+
+    base::UmaHistogramCounts100(
+        "SiteIsolation.SavedWebTriggeredIsolatedOrigins.Size", origins.size());
+  }
 }
 
 // static
diff --git a/components/site_isolation/site_isolation_policy.h b/components/site_isolation/site_isolation_policy.h
index 28621aa..55e1312 100644
--- a/components/site_isolation/site_isolation_policy.h
+++ b/components/site_isolation/site_isolation_policy.h
@@ -87,6 +87,14 @@
   static bool ShouldPdfCompositorBeEnabledForOopifs();
 
  private:
+  // Helpers for implementing PersistIsolatedOrigin().
+  static void PersistUserTriggeredIsolatedOrigin(
+      content::BrowserContext* context,
+      const url::Origin& origin);
+  static void PersistWebTriggeredIsolatedOrigin(
+      content::BrowserContext* context,
+      const url::Origin& origin);
+
   DISALLOW_IMPLICIT_CONSTRUCTORS(SiteIsolationPolicy);
 };
 
diff --git a/components/site_isolation/site_isolation_policy_unittest.cc b/components/site_isolation/site_isolation_policy_unittest.cc
index 99665ff..d1ed1dd 100644
--- a/components/site_isolation/site_isolation_policy_unittest.cc
+++ b/components/site_isolation/site_isolation_policy_unittest.cc
@@ -10,6 +10,7 @@
 #include "base/test/mock_entropy_provider.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/scoped_field_trial_list_resetter.h"
+#include "base/util/values/values_util.h"
 #include "build/branding_buildflags.h"
 #include "build/build_config.h"
 #include "components/prefs/pref_registry_simple.h"
@@ -38,6 +39,9 @@
 namespace site_isolation {
 namespace {
 
+using IsolatedOriginSource =
+    content::ChildProcessSecurityPolicy::IsolatedOriginSource;
+
 // Some command-line switches override field trials - the tests need to be
 // skipped in this case.
 bool ShouldSkipBecauseOfConflictingCommandLineSwitches() {
@@ -101,6 +105,8 @@
  public:
   SiteIsolationPolicyTest() {
     prefs_.registry()->RegisterListPref(prefs::kUserTriggeredIsolatedOrigins);
+    prefs_.registry()->RegisterDictionaryPref(
+        prefs::kWebTriggeredIsolatedOrigins);
     user_prefs::UserPrefs::Set(&browser_context_, &prefs_);
   }
 
@@ -117,6 +123,110 @@
   DISALLOW_COPY_AND_ASSIGN(SiteIsolationPolicyTest);
 };
 
+class WebTriggeredIsolatedOriginsPolicyTest : public SiteIsolationPolicyTest {
+ public:
+  WebTriggeredIsolatedOriginsPolicyTest() = default;
+
+  void PersistOrigin(const std::string& origin) {
+    SiteIsolationPolicy::PersistIsolatedOrigin(
+        browser_context(), url::Origin::Create(GURL(origin)),
+        IsolatedOriginSource::WEB_TRIGGERED);
+  }
+
+  std::vector<std::string> GetStoredOrigins() {
+    std::vector<std::string> origins;
+    auto* dict = user_prefs::UserPrefs::Get(browser_context())
+                     ->GetDictionary(prefs::kWebTriggeredIsolatedOrigins);
+    for (auto pair : dict->DictItems())
+      origins.push_back(pair.first);
+    return origins;
+  }
+
+ protected:
+  void SetUp() override {
+    // Limit the max number of stored sites to 3.
+    feature_list_.InitAndEnableFeatureWithParameters(
+        ::features::kSiteIsolationForCrossOriginOpenerPolicy,
+        {{"stored_sites_max_size", base::NumberToString(3)},
+         {"should_persist_across_restarts", "true"}});
+    SetEnableStrictSiteIsolation(false);
+    SiteIsolationPolicyTest::SetUp();
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+
+  DISALLOW_COPY_AND_ASSIGN(WebTriggeredIsolatedOriginsPolicyTest);
+};
+
+// Verify that persisting web-triggered isolated origins properly saves the
+// origins to prefs and respects the maximum number of entries (3 in this
+// test).
+TEST_F(WebTriggeredIsolatedOriginsPolicyTest, PersistIsolatedOrigin) {
+  PersistOrigin("https://foo1.com");
+  PersistOrigin("https://foo2.com");
+  PersistOrigin("https://foo3.com");
+
+  EXPECT_THAT(GetStoredOrigins(),
+              testing::UnorderedElementsAre(
+                  "https://foo1.com", "https://foo2.com", "https://foo3.com"));
+
+  // Adding foo4.com should evict the oldest entry (foo1.com).
+  PersistOrigin("https://foo4.com");
+  EXPECT_THAT(GetStoredOrigins(),
+              testing::UnorderedElementsAre(
+                  "https://foo2.com", "https://foo3.com", "https://foo4.com"));
+
+  // Adding foo5.com and foo6.com should evict the next two oldest entries.
+  PersistOrigin("https://foo5.com");
+  PersistOrigin("https://foo6.com");
+  EXPECT_THAT(GetStoredOrigins(),
+              testing::UnorderedElementsAre(
+                  "https://foo4.com", "https://foo5.com", "https://foo6.com"));
+
+  // Updating the timestamp on foo5.com should keep the current three entries.
+  PersistOrigin("https://foo5.com");
+  EXPECT_THAT(GetStoredOrigins(),
+              testing::UnorderedElementsAre(
+                  "https://foo4.com", "https://foo5.com", "https://foo6.com"));
+
+  // Adding two new entries should now evict foo4.com and foo6.com, since
+  // foo5.com has a more recent timestamp.
+  PersistOrigin("https://foo7.com");
+  PersistOrigin("https://foo8.com");
+  EXPECT_THAT(GetStoredOrigins(),
+              testing::UnorderedElementsAre(
+                  "https://foo5.com", "https://foo7.com", "https://foo8.com"));
+}
+
+// Verify that when origins stored in prefs contain more than the current
+// maximum number of entries, we clean up older entries when adding a new one
+// to go back under the size limit.
+TEST_F(WebTriggeredIsolatedOriginsPolicyTest, UpdatedMaxSize) {
+  // Populate the pref manually with more entries than the 3 allowed by the
+  // field trial param.
+  DictionaryPrefUpdate update(
+      user_prefs::UserPrefs::Get(browser_context()),
+      site_isolation::prefs::kWebTriggeredIsolatedOrigins);
+  base::DictionaryValue* dict = update.Get();
+  dict->SetKey("https://foo1.com", util::TimeToValue(base::Time::Now()));
+  dict->SetKey("https://foo2.com", util::TimeToValue(base::Time::Now()));
+  dict->SetKey("https://foo3.com", util::TimeToValue(base::Time::Now()));
+  dict->SetKey("https://foo4.com", util::TimeToValue(base::Time::Now()));
+  dict->SetKey("https://foo5.com", util::TimeToValue(base::Time::Now()));
+  EXPECT_THAT(GetStoredOrigins(),
+              testing::UnorderedElementsAre(
+                  "https://foo1.com", "https://foo2.com", "https://foo3.com",
+                  "https://foo4.com", "https://foo5.com"));
+
+  // Now, attempt to save a new origin.  This should evict the three oldest
+  // entries to make room for the new origin.
+  PersistOrigin("https://foo6.com");
+  EXPECT_THAT(GetStoredOrigins(),
+              testing::UnorderedElementsAre(
+                  "https://foo4.com", "https://foo5.com", "https://foo6.com"));
+}
+
 // Helper class that enables site isolation for password sites.
 class PasswordSiteIsolationPolicyTest : public SiteIsolationPolicyTest {
  public:
diff --git a/components/soda/BUILD.gn b/components/soda/BUILD.gn
index 134f6c3..b4a8af8 100644
--- a/components/soda/BUILD.gn
+++ b/components/soda/BUILD.gn
@@ -19,7 +19,16 @@
   ]
 
   if (is_chromeos_ash) {
-    deps += [ "//ash/public/cpp" ]
+    sources += [
+      "soda_installer_impl_chromeos.cc",
+      "soda_installer_impl_chromeos.h",
+    ]
+
+    deps += [
+      "//ash/public/cpp",
+      "//chromeos/dbus/dlcservice",
+      "//ui/base",
+    ]
   }
 }
 
diff --git a/components/soda/DEPS b/components/soda/DEPS
index 565b349b..2a23cda 100644
--- a/components/soda/DEPS
+++ b/components/soda/DEPS
@@ -1,9 +1,11 @@
 include_rules = [
   "+ash/public/cpp",
+  "+chromeos/dbus/dlcservice/dlcservice_client.h",
   "+components/component_updater/component_updater_paths.h",
   "+components/crx_file",
   "+components/live_caption",
   "+components/prefs",
   "+components/strings/grit/components_strings.h",
   "+media/base",
+  "+ui/base",
 ]
diff --git a/chrome/browser/ash/accessibility/soda_installer_impl_chromeos.cc b/components/soda/soda_installer_impl_chromeos.cc
similarity index 98%
rename from chrome/browser/ash/accessibility/soda_installer_impl_chromeos.cc
rename to components/soda/soda_installer_impl_chromeos.cc
index 713c175..d24c22f 100644
--- a/chrome/browser/ash/accessibility/soda_installer_impl_chromeos.cc
+++ b/components/soda/soda_installer_impl_chromeos.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/ash/accessibility/soda_installer_impl_chromeos.h"
+#include "components/soda/soda_installer_impl_chromeos.h"
 
 #include "base/bind.h"
 #include "base/feature_list.h"
diff --git a/chrome/browser/ash/accessibility/soda_installer_impl_chromeos.h b/components/soda/soda_installer_impl_chromeos.h
similarity index 93%
rename from chrome/browser/ash/accessibility/soda_installer_impl_chromeos.h
rename to components/soda/soda_installer_impl_chromeos.h
index ccedccd9..cbcb13c 100644
--- a/chrome/browser/ash/accessibility/soda_installer_impl_chromeos.h
+++ b/components/soda/soda_installer_impl_chromeos.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 CHROME_BROWSER_ASH_ACCESSIBILITY_SODA_INSTALLER_IMPL_CHROMEOS_H_
-#define CHROME_BROWSER_ASH_ACCESSIBILITY_SODA_INSTALLER_IMPL_CHROMEOS_H_
+#ifndef COMPONENTS_SODA_SODA_INSTALLER_IMPL_CHROMEOS_H_
+#define COMPONENTS_SODA_SODA_INSTALLER_IMPL_CHROMEOS_H_
 
 #include "base/files/file_path.h"
 #include "chromeos/dbus/dlcservice/dlcservice_client.h"
@@ -92,4 +92,4 @@
 
 }  // namespace speech
 
-#endif  // CHROME_BROWSER_ASH_ACCESSIBILITY_SODA_INSTALLER_IMPL_CHROMEOS_H_
+#endif  // COMPONENTS_SODA_SODA_INSTALLER_IMPL_CHROMEOS_H_
diff --git a/components/speech/chunked_byte_buffer.h b/components/speech/chunked_byte_buffer.h
index dddc2ee..418ceaa7 100644
--- a/components/speech/chunked_byte_buffer.h
+++ b/components/speech/chunked_byte_buffer.h
@@ -9,7 +9,6 @@
 #include <stdint.h>
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/subresource_filter/content/browser/async_document_subresource_filter.h b/components/subresource_filter/content/browser/async_document_subresource_filter.h
index 3ad212f1..defd32c 100644
--- a/components/subresource_filter/content/browser/async_document_subresource_filter.h
+++ b/components/subresource_filter/content/browser/async_document_subresource_filter.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SUBRESOURCE_FILTER_CONTENT_BROWSER_ASYNC_DOCUMENT_SUBRESOURCE_FILTER_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/subresource_filter/content/browser/ruleset_service.h b/components/subresource_filter/content/browser/ruleset_service.h
index 33a3176..c2bb66fe 100644
--- a/components/subresource_filter/content/browser/ruleset_service.h
+++ b/components/subresource_filter/content/browser/ruleset_service.h
@@ -33,7 +33,6 @@
 #include <stdint.h>
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/subresource_filter/tools/ruleset_converter/rule_stream.h b/components/subresource_filter/tools/ruleset_converter/rule_stream.h
index bab5871..e24fdb0 100644
--- a/components/subresource_filter/tools/ruleset_converter/rule_stream.h
+++ b/components/subresource_filter/tools/ruleset_converter/rule_stream.h
@@ -8,7 +8,6 @@
 #include <istream>
 #include <memory>
 #include <ostream>
-#include <string>
 
 #include "components/subresource_filter/tools/ruleset_converter/ruleset_format.h"
 #include "components/url_pattern_index/proto/rules.pb.h"
diff --git a/components/sync/base/enum_set.h b/components/sync/base/enum_set.h
index aef2fa01..138e222 100644
--- a/components/sync/base/enum_set.h
+++ b/components/sync/base/enum_set.h
@@ -7,7 +7,6 @@
 
 #include <bitset>
 #include <cstddef>
-#include <string>
 #include <type_traits>
 #include <utility>
 
diff --git a/components/sync/driver/data_type_status_table.h b/components/sync/driver/data_type_status_table.h
index 424d74b1..42b39f8f 100644
--- a/components/sync/driver/data_type_status_table.h
+++ b/components/sync/driver/data_type_status_table.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_DRIVER_DATA_TYPE_STATUS_TABLE_H_
 
 #include <map>
-#include <string>
 
 #include "components/sync/base/model_type.h"
 #include "components/sync/model/sync_error.h"
diff --git a/components/sync/driver/model_type_controller.h b/components/sync/driver/model_type_controller.h
index c9d52fb..65e6a47 100644
--- a/components/sync/driver/model_type_controller.h
+++ b/components/sync/driver/model_type_controller.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_DRIVER_MODEL_TYPE_CONTROLLER_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/sync/driver/sync_session_durations_metrics_recorder.h b/components/sync/driver/sync_session_durations_metrics_recorder.h
index a381fd281..9dfdcf1 100644
--- a/components/sync/driver/sync_session_durations_metrics_recorder.h
+++ b/components/sync/driver/sync_session_durations_metrics_recorder.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_DRIVER_SYNC_SESSION_DURATIONS_METRICS_RECORDER_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/scoped_observation.h"
diff --git a/components/sync/engine/cycle/data_type_tracker.h b/components/sync/engine/cycle/data_type_tracker.h
index 1d4b503..7213b38 100644
--- a/components/sync/engine/cycle/data_type_tracker.h
+++ b/components/sync/engine/cycle/data_type_tracker.h
@@ -8,7 +8,6 @@
 #include <stddef.h>
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/sync/engine/cycle/sync_cycle.h b/components/sync/engine/cycle/sync_cycle.h
index bd6ff2a8..a222f23 100644
--- a/components/sync/engine/cycle/sync_cycle.h
+++ b/components/sync/engine/cycle/sync_cycle.h
@@ -8,7 +8,6 @@
 #include <map>
 #include <memory>
 #include <set>
-#include <string>
 #include <utility>
 #include <vector>
 
diff --git a/components/sync/engine/nigori/keystore_keys_handler.h b/components/sync/engine/nigori/keystore_keys_handler.h
index cbb0b2d..3295a274 100644
--- a/components/sync/engine/nigori/keystore_keys_handler.h
+++ b/components/sync/engine/nigori/keystore_keys_handler.h
@@ -5,7 +5,6 @@
 #ifndef COMPONENTS_SYNC_ENGINE_NIGORI_KEYSTORE_KEYS_HANDLER_H_
 #define COMPONENTS_SYNC_ENGINE_NIGORI_KEYSTORE_KEYS_HANDLER_H_
 
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/sync/engine/sync_scheduler.h b/components/sync/engine/sync_scheduler.h
index fedfa2d..63f2a6a 100644
--- a/components/sync/engine/sync_scheduler.h
+++ b/components/sync/engine/sync_scheduler.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_ENGINE_SYNC_SCHEDULER_H_
 
 #include <memory>
-#include <string>
 
 #include "base/callback.h"
 #include "base/compiler_specific.h"
diff --git a/components/sync/model/blocking_model_type_store.h b/components/sync/model/blocking_model_type_store.h
index 532eb36c..ea54802 100644
--- a/components/sync/model/blocking_model_type_store.h
+++ b/components/sync/model/blocking_model_type_store.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_MODEL_BLOCKING_MODEL_TYPE_STORE_H_
 
 #include <memory>
-#include <string>
 
 #include "components/sync/base/model_type.h"
 #include "components/sync/model/model_error.h"
diff --git a/components/sync/model/model_type_store.h b/components/sync/model/model_type_store.h
index 3078550ca..501fc9f9 100644
--- a/components/sync/model/model_type_store.h
+++ b/components/sync/model/model_type_store.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_MODEL_MODEL_TYPE_STORE_H_
 
 #include <memory>
-#include <string>
 
 #include "base/callback.h"
 #include "base/macros.h"
diff --git a/components/sync/model/model_type_store_impl.h b/components/sync/model/model_type_store_impl.h
index afb1873..c47b6c3 100644
--- a/components/sync/model/model_type_store_impl.h
+++ b/components/sync/model/model_type_store_impl.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_MODEL_MODEL_TYPE_STORE_IMPL_H_
 
 #include <memory>
-#include <string>
 
 #include "base/memory/ref_counted.h"
 #include "base/memory/weak_ptr.h"
diff --git a/components/sync/test/fake_server/android/fake_server_helper_android.h b/components/sync/test/fake_server/android/fake_server_helper_android.h
index 8ca53165..f5c71d02 100644
--- a/components/sync/test/fake_server/android/fake_server_helper_android.h
+++ b/components/sync/test/fake_server/android/fake_server_helper_android.h
@@ -8,7 +8,6 @@
 #include <jni.h>
 
 #include <memory>
-#include <string>
 
 #include "base/android/scoped_java_ref.h"
 #include "components/sync/test/fake_server/entity_builder_factory.h"
diff --git a/components/sync/test/model/fake_model_type_controller_delegate.h b/components/sync/test/model/fake_model_type_controller_delegate.h
index d0998639..afb24fe 100644
--- a/components/sync/test/model/fake_model_type_controller_delegate.h
+++ b/components/sync/test/model/fake_model_type_controller_delegate.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_TEST_MODEL_FAKE_MODEL_TYPE_CONTROLLER_DELEGATE_H_
 
 #include <memory>
-#include <string>
 
 #include "base/memory/weak_ptr.h"
 #include "components/sync/base/model_type.h"
diff --git a/components/sync/trusted_vault/securebox.h b/components/sync/trusted_vault/securebox.h
index 5310e7d..fa5486b 100644
--- a/components/sync/trusted_vault/securebox.h
+++ b/components/sync/trusted_vault/securebox.h
@@ -7,7 +7,6 @@
 
 #include <cstdint>
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/containers/span.h"
diff --git a/components/sync/trusted_vault/trusted_vault_connection.h b/components/sync/trusted_vault/trusted_vault_connection.h
index 400bf06f..faff2fba 100644
--- a/components/sync/trusted_vault/trusted_vault_connection.h
+++ b/components/sync/trusted_vault/trusted_vault_connection.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_TRUSTED_VAULT_TRUSTED_VAULT_CONNECTION_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback.h"
diff --git a/components/sync/trusted_vault/trusted_vault_connection_impl.h b/components/sync/trusted_vault/trusted_vault_connection_impl.h
index 1485df1..626b0352 100644
--- a/components/sync/trusted_vault/trusted_vault_connection_impl.h
+++ b/components/sync/trusted_vault/trusted_vault_connection_impl.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_TRUSTED_VAULT_TRUSTED_VAULT_CONNECTION_IMPL_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/memory/scoped_refptr.h"
diff --git a/components/sync_bookmarks/bookmark_remote_updates_handler.h b/components/sync_bookmarks/bookmark_remote_updates_handler.h
index dc32e41..dcf8f89 100644
--- a/components/sync_bookmarks/bookmark_remote_updates_handler.h
+++ b/components/sync_bookmarks/bookmark_remote_updates_handler.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_BOOKMARKS_BOOKMARK_REMOTE_UPDATES_HANDLER_H_
 
 #include <map>
-#include <string>
 #include <vector>
 
 #include "components/sync/base/unique_position.h"
diff --git a/components/sync_device_info/device_info_sync_service_impl.h b/components/sync_device_info/device_info_sync_service_impl.h
index b2209da..7be70a9c 100644
--- a/components/sync_device_info/device_info_sync_service_impl.h
+++ b/components/sync_device_info/device_info_sync_service_impl.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_DEVICE_INFO_DEVICE_INFO_SYNC_SERVICE_IMPL_H_
 
 #include <memory>
-#include <string>
 
 #include "base/callback_helpers.h"
 #include "components/sync/invalidations/fcm_registration_token_observer.h"
diff --git a/components/sync_sessions/session_sync_service_impl.h b/components/sync_sessions/session_sync_service_impl.h
index 234abb8..08e46e64 100644
--- a/components/sync_sessions/session_sync_service_impl.h
+++ b/components/sync_sessions/session_sync_service_impl.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_SESSIONS_SESSION_SYNC_SERVICE_IMPL_H_
 
 #include <memory>
-#include <string>
 
 #include "base/callback_list.h"
 #include "base/memory/weak_ptr.h"
diff --git a/components/sync_sessions/tab_node_pool.h b/components/sync_sessions/tab_node_pool.h
index 01f6874..39f1c5b 100644
--- a/components/sync_sessions/tab_node_pool.h
+++ b/components/sync_sessions/tab_node_pool.h
@@ -9,7 +9,6 @@
 
 #include <map>
 #include <set>
-#include <string>
 
 #include "base/feature_list.h"
 #include "base/macros.h"
diff --git a/components/sync_user_events/fake_user_event_service.h b/components/sync_user_events/fake_user_event_service.h
index 7b3bee3..64ea7c1 100644
--- a/components/sync_user_events/fake_user_event_service.h
+++ b/components/sync_user_events/fake_user_event_service.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_USER_EVENTS_FAKE_USER_EVENT_SERVICE_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/sync_user_events/no_op_user_event_service.h b/components/sync_user_events/no_op_user_event_service.h
index fd69d6f..1f385af 100644
--- a/components/sync_user_events/no_op_user_event_service.h
+++ b/components/sync_user_events/no_op_user_event_service.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_USER_EVENTS_NO_OP_USER_EVENT_SERVICE_H_
 
 #include <memory>
-#include <string>
 
 #include "base/macros.h"
 #include "components/sync_user_events/user_event_service.h"
diff --git a/components/sync_user_events/user_event_service.h b/components/sync_user_events/user_event_service.h
index 75d6ea43..c8d5b93 100644
--- a/components/sync_user_events/user_event_service.h
+++ b/components/sync_user_events/user_event_service.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_USER_EVENTS_USER_EVENT_SERVICE_H_
 
 #include <memory>
-#include <string>
 
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
diff --git a/components/sync_user_events/user_event_service_impl.h b/components/sync_user_events/user_event_service_impl.h
index 70ea7636..968252e 100644
--- a/components/sync_user_events/user_event_service_impl.h
+++ b/components/sync_user_events/user_event_service_impl.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_SYNC_USER_EVENTS_USER_EVENT_SERVICE_IMPL_H_
 
 #include <memory>
-#include <string>
 
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
diff --git a/components/translate/core/browser/mock_translate_ranker.h b/components/translate/core/browser/mock_translate_ranker.h
index cc6b513f..c2aa345c 100644
--- a/components/translate/core/browser/mock_translate_ranker.h
+++ b/components/translate/core/browser/mock_translate_ranker.h
@@ -6,14 +6,12 @@
 #define COMPONENTS_TRANSLATE_CORE_BROWSER_MOCK_TRANSLATE_RANKER_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "components/translate/core/browser/translate_ranker.h"
 #include "services/metrics/public/cpp/ukm_source_id.h"
 #include "testing/gmock/include/gmock/gmock.h"
 
-
 namespace metrics {
 class TranslateEventProto;
 }
diff --git a/components/translate/core/browser/translate_ranker.h b/components/translate/core/browser/translate_ranker.h
index 243aafe..7f9f836 100644
--- a/components/translate/core/browser/translate_ranker.h
+++ b/components/translate/core/browser/translate_ranker.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_TRANSLATE_CORE_BROWSER_TRANSLATE_RANKER_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/macros.h"
diff --git a/components/update_client/background_downloader_win.h b/components/update_client/background_downloader_win.h
index db3aa3e..1abccbf 100644
--- a/components/update_client/background_downloader_win.h
+++ b/components/update_client/background_downloader_win.h
@@ -10,7 +10,6 @@
 #include <wrl/client.h>
 
 #include <memory>
-#include <string>
 
 #include "base/memory/ref_counted.h"
 #include "base/sequence_checker.h"
diff --git a/components/user_manager/user_image/user_image.h b/components/user_manager/user_image/user_image.h
index 55e4978..7b8da59 100644
--- a/components/user_manager/user_image/user_image.h
+++ b/components/user_manager/user_image/user_image.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_USER_MANAGER_USER_IMAGE_USER_IMAGE_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/files/file_path.h"
diff --git a/components/variations/variations_ids_provider.cc b/components/variations/variations_ids_provider.cc
index b4a90d2c..2b64ee3 100644
--- a/components/variations/variations_ids_provider.cc
+++ b/components/variations/variations_ids_provider.cc
@@ -359,10 +359,10 @@
   // This is the bottleneck for the creation of the header, so validate the size
   // here. Force a hard maximum on the ID count in case the Variations server
   // returns too many IDs and DOSs receiving servers with large requests.
-  DCHECK_LE(total_id_count, 35U);
+  DCHECK_LE(total_id_count, 50U);
   UMA_HISTOGRAM_COUNTS_100("Variations.Headers.ExperimentCount",
                            total_id_count);
-  if (total_id_count > 50)
+  if (total_id_count > 75)
     return std::string();
 
   std::string serialized;
diff --git a/components/variations/variations_murmur_hash.h b/components/variations/variations_murmur_hash.h
index a1e28ee3..d4a0291 100644
--- a/components/variations/variations_murmur_hash.h
+++ b/components/variations/variations_murmur_hash.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_VARIATIONS_VARIATIONS_MURMUR_HASH_H_
 
 #include <cstdint>
-#include <string>
 #include <vector>
 
 #include "base/compiler_specific.h"
diff --git a/components/viz/common/frame_sinks/begin_frame_source.h b/components/viz/common/frame_sinks/begin_frame_source.h
index c57af2fe..c05efdb 100644
--- a/components/viz/common/frame_sinks/begin_frame_source.h
+++ b/components/viz/common/frame_sinks/begin_frame_source.h
@@ -9,7 +9,6 @@
 #include <stdint.h>
 
 #include <memory>
-#include <string>
 
 #include "base/check.h"
 #include "base/containers/flat_set.h"
diff --git a/components/viz/service/display/surface_aggregator.h b/components/viz/service/display/surface_aggregator.h
index dfc9bb3..988868e 100644
--- a/components/viz/service/display/surface_aggregator.h
+++ b/components/viz/service/display/surface_aggregator.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_VIZ_SERVICE_DISPLAY_SURFACE_AGGREGATOR_H_
 
 #include <memory>
-#include <string>
 #include <unordered_map>
 #include <utility>
 #include <vector>
diff --git a/components/viz/service/main/viz_main_impl.h b/components/viz/service/main/viz_main_impl.h
index 37c89f0..27e0c52 100644
--- a/components/viz/service/main/viz_main_impl.h
+++ b/components/viz/service/main/viz_main_impl.h
@@ -6,7 +6,6 @@
 #define COMPONENTS_VIZ_SERVICE_MAIN_VIZ_MAIN_IMPL_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/single_thread_task_runner.h"
diff --git a/components/webapps/browser/installable/installable_manager.h b/components/webapps/browser/installable/installable_manager.h
index a7abdf4b8..bb161de 100644
--- a/components/webapps/browser/installable/installable_manager.h
+++ b/components/webapps/browser/installable/installable_manager.h
@@ -7,7 +7,6 @@
 
 #include <map>
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback_forward.h"
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index 7f32ce81..b104146 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -1023,6 +1023,8 @@
     "interest_group/ad_auction.h",
     "interest_group/ad_auction_service_impl.cc",
     "interest_group/ad_auction_service_impl.h",
+    "interest_group/auction_runner.cc",
+    "interest_group/auction_runner.h",
     "interest_group/auction_url_loader_factory_proxy.cc",
     "interest_group/auction_url_loader_factory_proxy.h",
     "interest_group/interest_group_manager.cc",
@@ -2225,16 +2227,6 @@
       "zygote_host/zygote_host_impl_linux.h",
     ]
 
-    if (ozone_platform_x11 || use_x11) {
-      sources += [
-        "gpu_data_manager_visual_proxy_ozone_linux.cc",
-        "gpu_data_manager_visual_proxy_ozone_linux.h",
-      ]
-      if (use_x11) {
-        deps += [ "//ui/base/x:gl" ]
-      }
-    }
-
     public_deps += [ "//components/services/font/public/mojom" ]
 
     deps += [
diff --git a/content/browser/accessibility/web_contents_accessibility_android.cc b/content/browser/accessibility/web_contents_accessibility_android.cc
index b7bf1fd..ee23d97 100644
--- a/content/browser/accessibility/web_contents_accessibility_android.cc
+++ b/content/browser/accessibility/web_contents_accessibility_android.cc
@@ -29,6 +29,7 @@
 #include "content/public/android/content_jni_headers/WebContentsAccessibilityImpl_jni.h"
 #include "content/public/common/content_features.h"
 #include "content/public/common/use_zoom_for_dsf_policy.h"
+#include "ui/accessibility/accessibility_features.h"
 #include "ui/accessibility/ax_assistant_structure.h"
 #include "ui/events/android/motion_event_android.h"
 
@@ -268,7 +269,8 @@
 }
 
 void WebContentsAccessibilityAndroid::Enable(JNIEnv* env,
-                                             const JavaParamRef<jobject>& obj) {
+                                             const JavaParamRef<jobject>& obj,
+                                             jboolean screen_reader_mode) {
   BrowserAccessibilityStateImpl* accessibility_state =
       BrowserAccessibilityStateImpl::GetInstance();
   auto* manager = GetRootBrowserAccessibilityManager();
@@ -282,6 +284,13 @@
     return;
   }
 
+  if (features::IsComputeAXModeEnabled()) {
+    ui::AXMode mode =
+        screen_reader_mode ? ui::kAXModeComplete : ui::kAXModeBasic;
+    accessibility_state->AddAccessibilityModeFlags(mode);
+    return;
+  }
+
   // Otherwise, enable accessibility globally unless it was
   // explicitly disallowed by a command-line flag, then enable it for
   // this WebContents if that succeeded.
@@ -300,6 +309,28 @@
   }
 }
 
+void WebContentsAccessibilityAndroid::SetAXMode(
+    JNIEnv* env,
+    const JavaParamRef<jobject>& obj,
+    jboolean screen_reader_mode) {
+  if (!features::IsComputeAXModeEnabled())
+    return;
+
+  BrowserAccessibilityStateImpl* accessibility_state =
+      BrowserAccessibilityStateImpl::GetInstance();
+
+  if (screen_reader_mode) {
+    accessibility_state->AddAccessibilityModeFlags(ui::kAXModeComplete);
+  } else {
+    // Remove the mode flags present in kAXModeComplete but not in
+    // kAXModeBasic, thereby reverting the mode to kAXModeBasic while
+    // not touching any other flags.
+    ui::AXMode remove_mode_flags(ui::kAXModeComplete.mode() &
+                                 ~ui::kAXModeBasic.mode());
+    accessibility_state->RemoveAccessibilityModeFlags(remove_mode_flags);
+  }
+}
+
 bool WebContentsAccessibilityAndroid::ShouldRespectDisplayedPasswordText() {
   JNIEnv* env = AttachCurrentThread();
   ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
diff --git a/content/browser/accessibility/web_contents_accessibility_android.h b/content/browser/accessibility/web_contents_accessibility_android.h
index 4cf809b..d7b9d16 100644
--- a/content/browser/accessibility/web_contents_accessibility_android.h
+++ b/content/browser/accessibility/web_contents_accessibility_android.h
@@ -71,7 +71,12 @@
   // Global methods.
   jboolean IsEnabled(JNIEnv* env,
                      const base::android::JavaParamRef<jobject>& obj);
-  void Enable(JNIEnv* env, const base::android::JavaParamRef<jobject>& obj);
+  void Enable(JNIEnv* env,
+              const base::android::JavaParamRef<jobject>& obj,
+              jboolean screen_reader_mode);
+  void SetAXMode(JNIEnv* env,
+                 const base::android::JavaParamRef<jobject>& obj,
+                 jboolean screen_reader_mode);
 
   base::android::ScopedJavaLocalRef<jstring> GetSupportedHtmlElementTypes(
       JNIEnv* env,
diff --git a/content/browser/browser_main_loop.cc b/content/browser/browser_main_loop.cc
index f1f6dc64a..058bdd40 100644
--- a/content/browser/browser_main_loop.cc
+++ b/content/browser/browser_main_loop.cc
@@ -194,10 +194,6 @@
 #include <glib-object.h>
 #endif
 
-#if defined(USE_X11) || defined(USE_OZONE_PLATFORM_X11)
-#include "content/browser/gpu_data_manager_visual_proxy_ozone_linux.h"
-#endif
-
 #if defined(OS_WIN)
 #include "media/device_monitors/system_message_window_win.h"
 #include "sandbox/win/src/process_mitigations.h"
@@ -807,15 +803,6 @@
   // while avoiding doing so in unit tests by making it explicitly enabled here.
   GpuDataManagerImpl::GetInstance()->StartUmaTimer();
 
-// Temporarily used by both Ozone/Linux and X11/Linux. Once X11/Linux goes
-// away, will be used only by Ozone/Linux.
-// TODO(https://crbug.com/1085700): make sure it's only used by Ozone/Linux.
-#if defined(USE_X11) || defined(USE_OZONE_PLATFORM_X11)
-  gpu_data_manager_visual_proxy_ =
-      std::make_unique<GpuDataManagerVisualProxyOzoneLinux>(
-          GpuDataManagerImpl::GetInstance());
-#endif
-
 #if !BUILDFLAG(GOOGLE_CHROME_BRANDING) || defined(OS_ANDROID)
   // Single-process is an unsupported and not fully tested mode, so
   // don't enable it for official Chrome builds (except on Android).
diff --git a/content/browser/browser_main_loop.h b/content/browser/browser_main_loop.h
index 30d6d6f..62acb18 100644
--- a/content/browser/browser_main_loop.h
+++ b/content/browser/browser_main_loop.h
@@ -108,10 +108,6 @@
 class ScreenOrientationDelegate;
 #endif
 
-#if defined(USE_X11) || defined(USE_OZONE_PLATFORM_X11)
-class GpuDataManagerVisualProxyOzoneLinux;
-#endif
-
 // Implements the main browser loop stages called from BrowserMainRunner.
 // See comments in browser_main_parts.h for additional info.
 class CONTENT_EXPORT BrowserMainLoop {
@@ -341,10 +337,6 @@
   // Members initialized in |PreCreateThreads()| -------------------------------
   // Torn down in ShutdownThreadsAndCleanUp.
   std::unique_ptr<base::MemoryPressureMonitor> memory_pressure_monitor_;
-#if defined(USE_X11) || defined(USE_OZONE_PLATFORM_X11)
-  std::unique_ptr<GpuDataManagerVisualProxyOzoneLinux>
-      gpu_data_manager_visual_proxy_;
-#endif
 
   // Members initialized in |CreateThreads()| ----------------------------------
   std::unique_ptr<BrowserProcessIOThread> io_thread_;
diff --git a/content/browser/compute_pressure/compute_pressure_manager.cc b/content/browser/compute_pressure/compute_pressure_manager.cc
index c02638c4c..f2729e7 100644
--- a/content/browser/compute_pressure/compute_pressure_manager.cc
+++ b/content/browser/compute_pressure/compute_pressure_manager.cc
@@ -21,6 +21,7 @@
 #include "mojo/public/cpp/bindings/message.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "services/network/public/cpp/is_potentially_trustworthy.h"
+#include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/mojom/compute_pressure/compute_pressure.mojom.h"
 
 namespace content {
@@ -68,6 +69,11 @@
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK(frame_id);
 
+  if (!base::FeatureList::IsEnabled(blink::features::kComputePressure)) {
+    mojo::ReportBadMessage("Compute Pressure not enabled");
+    return;
+  }
+
   if (!network::IsOriginPotentiallyTrustworthy(origin)) {
     mojo::ReportBadMessage("Compute Pressure access from an insecure origin");
     return;
diff --git a/content/browser/compute_pressure/compute_pressure_manager_unittest.cc b/content/browser/compute_pressure/compute_pressure_manager_unittest.cc
index d8b4f95..7cb20495 100644
--- a/content/browser/compute_pressure/compute_pressure_manager_unittest.cc
+++ b/content/browser/compute_pressure/compute_pressure_manager_unittest.cc
@@ -7,6 +7,7 @@
 #include <memory>
 #include <utility>
 
+#include "base/test/scoped_feature_list.h"
 #include "content/browser/compute_pressure/compute_pressure_test_support.h"
 #include "content/public/browser/global_routing_id.h"
 #include "content/test/fake_mojo_message_dispatch_context.h"
@@ -16,14 +17,13 @@
 #include "mojo/public/cpp/test_support/test_utils.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/mojom/compute_pressure/compute_pressure.mojom.h"
 
 namespace content {
 
 class ComputePressureManagerTest : public RenderViewHostImplTestHarness {
  public:
-  ComputePressureManagerTest() = default;
-
   void SetUp() override {
     RenderViewHostImplTestHarness::SetUp();
     NavigateAndCommit(kTestUrl);
@@ -68,6 +68,8 @@
       {0.2, 0.5, 0.8},
       {0.5}};
 
+  base::test::ScopedFeatureList scoped_feature_list_;
+
   GlobalFrameRoutingId main_frame_id_;
   // This member is a std::unique_ptr instead of a plain ComputePressureManager
   // so it can be replaced inside tests.
@@ -212,4 +214,17 @@
             blink::mojom::ComputePressureStatus::kNotSupported);
 }
 
+TEST_F(ComputePressureManagerTest, AddObserver_NoFeature) {
+  scoped_feature_list_.Reset();
+  scoped_feature_list_.InitAndDisableFeature(blink::features::kComputePressure);
+
+  FakeMojoMessageDispatchContext fake_dispatch_context;
+  mojo::test::BadMessageObserver bad_message_observer;
+  mojo::Remote<blink::mojom::ComputePressureHost> insecure_host;
+  manager_->BindReceiver(kInsecureOrigin, main_frame_id_,
+                         insecure_host.BindNewPipeAndPassReceiver());
+  EXPECT_EQ("Compute Pressure not enabled",
+            bad_message_observer.WaitForBadMessage());
+}
+
 }  // namespace content
diff --git a/content/browser/compute_pressure/compute_pressure_origin_trial_browsertest.cc b/content/browser/compute_pressure/compute_pressure_origin_trial_browsertest.cc
new file mode 100644
index 0000000..f4eae2799
--- /dev/null
+++ b/content/browser/compute_pressure/compute_pressure_origin_trial_browsertest.cc
@@ -0,0 +1,90 @@
+// 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 <memory>
+
+#include "base/test/scoped_feature_list.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/content_browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
+#include "content/public/test/url_loader_interceptor.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/features.h"
+#include "url/gurl.h"
+
+namespace content {
+
+namespace {
+
+constexpr char kBaseDataDir[] = "content/test/data/compute_pressure";
+
+class ComputePressureOriginTrialBrowserTest : public ContentBrowserTest {
+ public:
+  ~ComputePressureOriginTrialBrowserTest() override = default;
+
+  void SetUpOnMainThread() override {
+    ContentBrowserTest::SetUpOnMainThread();
+
+    // We need to use URLLoaderInterceptor (rather than a EmbeddedTestServer),
+    // because origin trial token is associated with a fixed origin, whereas
+    // EmbeddedTestServer serves content on a random port.
+    interceptor_ = URLLoaderInterceptor::ServeFilesFromDirectoryAtOrigin(
+        kBaseDataDir, GURL("https://example.test/"));
+  }
+
+  void TearDownOnMainThread() override {
+    interceptor_.reset();
+    ContentBrowserTest::TearDownOnMainThread();
+  }
+
+  bool HasComputePressureApi() {
+    return EvalJs(shell(), "'ComputePressureObserver' in window").ExtractBool();
+  }
+
+ protected:
+  const GURL kValidTokenUrl{"https://example.test/valid_token.html"};
+  const GURL kNoTokenUrl{"https://example.test/no_token.html"};
+
+  base::test::ScopedFeatureList scoped_feature_list_;
+  std::unique_ptr<content::URLLoaderInterceptor> interceptor_;
+};
+
+IN_PROC_BROWSER_TEST_F(ComputePressureOriginTrialBrowserTest,
+                       ValidOriginTrialToken) {
+  ASSERT_TRUE(NavigateToURL(shell(), kValidTokenUrl));
+  EXPECT_TRUE(HasComputePressureApi());
+}
+
+IN_PROC_BROWSER_TEST_F(ComputePressureOriginTrialBrowserTest,
+                       NoOriginTrialToken) {
+  ASSERT_TRUE(NavigateToURL(shell(), kNoTokenUrl));
+  EXPECT_FALSE(HasComputePressureApi());
+}
+
+class ComputePressureOriginTrialKillSwitchBrowserTest
+    : public ComputePressureOriginTrialBrowserTest {
+ public:
+  ComputePressureOriginTrialKillSwitchBrowserTest() {
+    scoped_feature_list_.Reset();
+    scoped_feature_list_.InitAndDisableFeature(
+        blink::features::kComputePressure);
+  }
+};
+
+IN_PROC_BROWSER_TEST_F(ComputePressureOriginTrialKillSwitchBrowserTest,
+                       ValidOriginTrialToken) {
+  ASSERT_TRUE(NavigateToURL(shell(), kValidTokenUrl));
+  EXPECT_FALSE(HasComputePressureApi());
+}
+
+IN_PROC_BROWSER_TEST_F(ComputePressureOriginTrialKillSwitchBrowserTest,
+                       NoOriginTrialToken) {
+  ASSERT_TRUE(NavigateToURL(shell(), kNoTokenUrl));
+  EXPECT_FALSE(HasComputePressureApi());
+}
+
+}  // namespace
+
+}  // namespace content
diff --git a/content/browser/conversions/conversion_network_sender_impl.cc b/content/browser/conversions/conversion_network_sender_impl.cc
index 246635c6..b1ba2ec 100644
--- a/content/browser/conversions/conversion_network_sender_impl.cc
+++ b/content/browser/conversions/conversion_network_sender_impl.cc
@@ -196,15 +196,23 @@
   network::SimpleURLLoader* loader = it->get();
 
   // Consider a non-200 HTTP code as a non-internal error.
-  bool internal_ok = loader->NetError() == net::OK ||
-                     loader->NetError() == net::ERR_HTTP_RESPONSE_CODE_FAILURE;
-  bool external_ok = headers && headers->response_code() == net::HTTP_OK;
+  int net_error = loader->NetError();
+  bool internal_ok =
+      net_error == net::OK || net_error == net::ERR_HTTP_RESPONSE_CODE_FAILURE;
+
+  int response_code = headers ? headers->response_code() : -1;
+  bool external_ok = response_code == net::HTTP_OK;
   Status status =
       internal_ok && external_ok
           ? Status::kOk
           : !internal_ok ? Status::kInternalError : Status::kExternalError;
   base::UmaHistogramEnumeration("Conversions.ReportStatus", status);
 
+  // Since net errors are always negative and HTTP errors are always positive,
+  // it is fine to combine these in a single histogram.
+  base::UmaHistogramSparse("Conversions.Report.HttpResponseOrNetErrorCode",
+                           internal_ok ? response_code : net_error);
+
   if (loader->GetNumRetries() > 0) {
     base::UmaHistogramBoolean("Conversions.ReportRetrySucceed",
                               status == Status::kOk);
diff --git a/content/browser/conversions/conversion_network_sender_impl_unittest.cc b/content/browser/conversions/conversion_network_sender_impl_unittest.cc
index e6f9e05..a5e7f423 100644
--- a/content/browser/conversions/conversion_network_sender_impl_unittest.cc
+++ b/content/browser/conversions/conversion_network_sender_impl_unittest.cc
@@ -282,6 +282,8 @@
         kReportUrl, ""));
     // kOk = 0.
     histograms.ExpectUniqueSample("Conversions.ReportStatus", 0, 1);
+    histograms.ExpectUniqueSample(
+        "Conversions.Report.HttpResponseOrNetErrorCode", net::HTTP_OK, 1);
   }
   // Internal error.
   {
@@ -294,6 +296,8 @@
         network::mojom::URLResponseHead::New(), std::string()));
     // kInternalError = 1.
     histograms.ExpectUniqueSample("Conversions.ReportStatus", 1, 1);
+    histograms.ExpectUniqueSample(
+        "Conversions.Report.HttpResponseOrNetErrorCode", net::ERR_FAILED, 1);
   }
   {
     base::HistogramTester histograms;
@@ -303,6 +307,9 @@
         kReportUrl, std::string(), net::HTTP_UNAUTHORIZED));
     // kExternalError = 2.
     histograms.ExpectUniqueSample("Conversions.ReportStatus", 2, 1);
+    histograms.ExpectUniqueSample(
+        "Conversions.Report.HttpResponseOrNetErrorCode", net::HTTP_UNAUTHORIZED,
+        1);
   }
 }
 
diff --git a/content/browser/gpu/gpu_data_manager_impl_private.cc b/content/browser/gpu/gpu_data_manager_impl_private.cc
index 82c37883..329131b 100644
--- a/content/browser/gpu/gpu_data_manager_impl_private.cc
+++ b/content/browser/gpu/gpu_data_manager_impl_private.cc
@@ -1186,9 +1186,12 @@
     case gpu::GpuMode::HARDWARE_VULKAN:
       use_gl = browser_command_line->GetSwitchValueASCII(switches::kUseGL);
       break;
-    case gpu::GpuMode::SWIFTSHADER:
-      use_gl = gl::kGLImplementationSwiftShaderForWebGLName;
-      break;
+    case gpu::GpuMode::SWIFTSHADER: {
+      // This setting makes WebGL run on legacy SwiftShader GL when true and
+      // SwANGLE when false.
+      bool legacy_software_gl = true;
+      gl::SetSoftwareWebGLCommandLineSwitches(command_line, legacy_software_gl);
+    } break;
     default:
       use_gl = gl::kGLImplementationDisabledName;
   }
diff --git a/content/browser/gpu_data_manager_visual_proxy_ozone_linux.cc b/content/browser/gpu_data_manager_visual_proxy_ozone_linux.cc
deleted file mode 100644
index 4dbb470..0000000
--- a/content/browser/gpu_data_manager_visual_proxy_ozone_linux.cc
+++ /dev/null
@@ -1,107 +0,0 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "content/browser/gpu_data_manager_visual_proxy_ozone_linux.h"
-
-#include "base/command_line.h"
-#include "content/browser/gpu/gpu_process_host.h"
-#include "content/public/browser/browser_task_traits.h"
-#include "content/public/browser/browser_thread.h"
-#include "content/public/common/content_features.h"
-#include "ui/gfx/switches.h"
-
-#if defined(USE_OZONE)
-#include "ui/base/ui_base_features.h"
-#include "ui/ozone/public/ozone_platform.h"
-#include "ui/ozone/public/platform_gl_egl_utility.h"
-#endif
-
-#if defined(USE_X11)
-#include "ui/base/x/x11_gl_egl_utility.h"  // nogncheck
-#endif
-
-namespace content {
-
-#if defined(USE_X11) || defined(USE_OZONE_PLATFORM_X11)
-namespace {
-
-void ShutdownGpuOnProcessThread() {
-  // The GPU process sent back bad visuals, which should never happen.
-  auto* gpu_process_host =
-      GpuProcessHost::Get(GPU_PROCESS_KIND_SANDBOXED, false);
-  if (gpu_process_host)
-    gpu_process_host->ForceShutdown();
-}
-
-}  // namespace
-#endif  // defined(USE_X11) || defined(USE_OZONE_PLATFORM_X11)
-
-GpuDataManagerVisualProxyOzoneLinux::GpuDataManagerVisualProxyOzoneLinux(
-    GpuDataManagerImpl* gpu_data_manager)
-    : gpu_data_manager_(gpu_data_manager) {
-  if (!base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kHeadless))
-    scoped_observer_.Observe(gpu_data_manager_);
-}
-
-GpuDataManagerVisualProxyOzoneLinux::~GpuDataManagerVisualProxyOzoneLinux() =
-    default;
-
-void GpuDataManagerVisualProxyOzoneLinux::OnGpuInfoUpdate() {
-  OnUpdate();
-}
-
-void GpuDataManagerVisualProxyOzoneLinux::OnGpuExtraInfoUpdate() {
-  OnUpdate();
-}
-
-void GpuDataManagerVisualProxyOzoneLinux::OnUpdate() {
-#if defined(USE_OZONE)
-  // Early return for Ozone/non-X11.  Otherwise UpdateVisualsOnGpuInfoChanged()
-  // will return false, thus terminating the GPU process and ruining things.
-  if (features::IsUsingOzonePlatform() &&
-      !ui::OzonePlatform::GetInstance()->GetPlatformGLEGLUtility()) {
-    return;
-  }
-#endif
-
-#if defined(USE_X11) || defined(USE_OZONE_PLATFORM_X11)
-  gpu::GPUInfo gpu_info = gpu_data_manager_->GetGPUInfo();
-  gfx::GpuExtraInfo gpu_extra_info = gpu_data_manager_->GetGpuExtraInfo();
-  if (!UpdateVisualsOnGpuInfoChanged(
-          gpu_info.software_rendering ||
-              !gpu_data_manager_->GpuAccessAllowed(nullptr),
-          gpu_extra_info.system_visual, gpu_extra_info.rgba_visual)) {
-    if (base::FeatureList::IsEnabled(features::kProcessHostOnUI)) {
-      ShutdownGpuOnProcessThread();
-    } else {
-      GetIOThreadTaskRunner({})->PostTask(
-          FROM_HERE, base::BindOnce(&ShutdownGpuOnProcessThread));
-    }
-  }
-#endif
-}
-
-bool GpuDataManagerVisualProxyOzoneLinux::UpdateVisualsOnGpuInfoChanged(
-    bool software_rendering,
-    x11::VisualId default_visual_id,
-    x11::VisualId transparent_visual_id) {
-#if defined(USE_OZONE)
-  if (features::IsUsingOzonePlatform() &&
-      ui::OzonePlatform::GetInstance()->GetPlatformGLEGLUtility()) {
-    return ui::OzonePlatform::GetInstance()
-        ->GetPlatformGLEGLUtility()
-        ->UpdateVisualsOnGpuInfoChanged(
-            software_rendering, static_cast<uint32_t>(default_visual_id),
-            static_cast<uint32_t>(transparent_visual_id));
-  }
-#endif
-#if defined(USE_X11)
-  return ui::UpdateVisualsOnGpuInfoChanged(
-      software_rendering, default_visual_id, transparent_visual_id);
-#endif
-  NOTREACHED() << "Only X11 may invoke this!";
-  return false;
-}
-
-}  // namespace content
diff --git a/content/browser/gpu_data_manager_visual_proxy_ozone_linux.h b/content/browser/gpu_data_manager_visual_proxy_ozone_linux.h
deleted file mode 100644
index 86b63d66..0000000
--- a/content/browser/gpu_data_manager_visual_proxy_ozone_linux.h
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CONTENT_BROWSER_GPU_DATA_MANAGER_VISUAL_PROXY_OZONE_LINUX_H_
-#define CONTENT_BROWSER_GPU_DATA_MANAGER_VISUAL_PROXY_OZONE_LINUX_H_
-
-#include "base/scoped_observation.h"
-#include "content/browser/gpu/gpu_data_manager_impl.h"
-#include "content/public/browser/gpu_data_manager_observer.h"
-#include "ui/gfx/gpu_extra_info.h"
-
-namespace content {
-
-// Forwards GPUInfo updates to ui::XVisualManager on X11 (will be removed as
-// soon as Ozone is default on Linux) or PlatformEGLGLUtility.
-class GpuDataManagerVisualProxyOzoneLinux : public GpuDataManagerObserver {
- public:
-  explicit GpuDataManagerVisualProxyOzoneLinux(
-      GpuDataManagerImpl* gpu_data_manager);
-  GpuDataManagerVisualProxyOzoneLinux(
-      const GpuDataManagerVisualProxyOzoneLinux&) = delete;
-  GpuDataManagerVisualProxyOzoneLinux& operator=(
-      const GpuDataManagerVisualProxyOzoneLinux&) = delete;
-  ~GpuDataManagerVisualProxyOzoneLinux() override;
-
-  void OnGpuInfoUpdate() override;
-  void OnGpuExtraInfoUpdate() override;
-
- private:
-  void OnUpdate();
-  bool UpdateVisualsOnGpuInfoChanged(bool software_rendering,
-                                     x11::VisualId default_visual_id,
-                                     x11::VisualId transparent_visual_id);
-
-  GpuDataManagerImpl* gpu_data_manager_;
-  base::ScopedObservation<GpuDataManagerImpl, GpuDataManagerObserver>
-      scoped_observer_{this};
-};
-
-}  // namespace content
-
-#endif  // CONTENT_BROWSER_GPU_DATA_MANAGER_VISUAL_PROXY_OZONE_LINUX_H_
diff --git a/content/browser/interest_group/DEPS b/content/browser/interest_group/DEPS
index 269a2bd..be1d75fb 100644
--- a/content/browser/interest_group/DEPS
+++ b/content/browser/interest_group/DEPS
@@ -1,3 +1,11 @@
 include_rules = [
   "+content/services/auction_worklet/public/mojom",
 ]
+
+specific_include_rules = {
+  # The AuctionRunnerTests have integration tests that directly instantiate an
+  # in-process auction_worklet service.
+  "auction_runner_unittest\.cc": [
+    "+content/services/auction_worklet",
+  ],
+}
\ No newline at end of file
diff --git a/content/browser/interest_group/ad_auction.cc b/content/browser/interest_group/ad_auction.cc
index 29effe4..4ef9d7a 100644
--- a/content/browser/interest_group/ad_auction.cc
+++ b/content/browser/interest_group/ad_auction.cc
@@ -15,13 +15,14 @@
 #include "base/strings/stringprintf.h"
 #include "content/browser/devtools/devtools_instrumentation.h"
 #include "content/browser/interest_group/ad_auction_service_impl.h"
+#include "content/browser/interest_group/auction_runner.h"
 #include "content/browser/interest_group/auction_url_loader_factory_proxy.h"
 #include "content/browser/interest_group/interest_group_manager.h"
 #include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/public/browser/content_browser_client.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/common/content_client.h"
-#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
+#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
 #include "services/network/public/mojom/url_loader_factory.mojom.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
@@ -67,46 +68,6 @@
   return true;
 }
 
-struct ValidatedResult {
-  bool is_valid_auction_result = false;
-  std::string ad_json;
-};
-
-ValidatedResult ValidateAuctionResult(
-    const std::vector<auction_worklet::mojom::BiddingInterestGroupPtr>& bidders,
-    const GURL& render_url,
-    const url::Origin& owner,
-    const std::string& name) {
-  ValidatedResult result;
-  if (!render_url.is_valid() || !render_url.SchemeIs(url::kHttpsScheme))
-    return result;
-
-  for (const auto& bidder : bidders) {
-    // Auction winner must be one of the bidders and bidder must have ads.
-    if (bidder->group->owner != owner || bidder->group->name != name ||
-        !bidder->group->ads) {
-      continue;
-    }
-    // `render_url` must be one of the winning bidder's ads.
-    for (const auto& ad : bidder->group->ads.value()) {
-      if (ad->render_url == render_url) {
-        result.is_valid_auction_result = true;
-        if (ad->metadata) {
-          //`metadata` is already in JSON so no quotes are needed.
-          result.ad_json = base::StringPrintf(
-              R"({"render_url":"%s","metadata":%s})", render_url.spec().c_str(),
-              ad->metadata.value().c_str());
-        } else {
-          result.ad_json = base::StringPrintf(R"({"render_url":"%s"})",
-                                              render_url.spec().c_str());
-        }
-        return result;
-      }
-    }
-  }
-  return result;
-}
-
 }  // namespace
 
 AdAuction::AdAuction(AdAuctionServiceImpl* ad_auction_service,
@@ -157,13 +118,6 @@
   ReadNextInterestGroup();
 }
 
-void AdAuction::OnServiceCrash() {
-  // This cancels any pending async callbacks.
-  weak_ptr_factory_.InvalidateWeakPtrs();
-
-  OnAuctionFailed();
-}
-
 void AdAuction::ReadNextInterestGroup() {
   DCHECK(!pending_buyers_.empty());
 
@@ -201,39 +155,31 @@
   DCHECK(pending_buyers_.empty());
   DCHECK(!bidders_.empty());
 
+  // TODO(mmenke): This should be top frame origin, not frame origin.
   auto browser_signals = auction_worklet::mojom::BrowserSignals::New(
       ad_auction_service_->origin(), config_->seller);
 
-  mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
-  url_loader_factory_proxy_ = std::make_unique<AuctionURLLoaderFactoryProxy>(
-      url_loader_factory.InitWithNewPipeAndPassReceiver(),
-      base::BindRepeating(&AdAuctionServiceImpl::GetFrameURLLoaderFactory,
-                          base::Unretained(ad_auction_service_)),
-      base::BindRepeating(&AdAuctionServiceImpl::GetTrustedURLLoaderFactory,
-                          base::Unretained(ad_auction_service_)),
-      browser_signals->top_frame_origin, *config_, bidders_);
-
   std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders_copy;
   bidders_copy.reserve(bidders_.size());
   for (auto& bidder : bidders_)
     bidders_copy.emplace_back(bidder.Clone());
 
   // `config_` is no longer needed after this point, so pass ownership of it
-  // over to the worklet, instead of copying it.
-  ad_auction_service_->GetWorkletService()->RunAuction(
-      std::move(url_loader_factory), std::move(config_),
-      std::move(bidders_copy), std::move(browser_signals),
+  // over to the AuctionRunner, instead of copying it.
+  auction_runner_ = AuctionRunner::CreateAndStart(
+      ad_auction_service_, std::move(config_), std::move(bidders_copy),
+      std::move(browser_signals), ad_auction_service_->origin(),
       base::BindOnce(&AdAuction::WorkletComplete,
                      weak_ptr_factory_.GetWeakPtr()));
 }
 
-void AdAuction::WorkletComplete(
-    const GURL& render_url,
-    const url::Origin& owner,
-    const std::string& name,
-    auction_worklet::mojom::WinningBidderReportPtr bidder_report,
-    auction_worklet::mojom::SellerReportPtr seller_report,
-    const std::vector<std::string>& errors) {
+void AdAuction::WorkletComplete(const GURL& render_url,
+                                const std::string& ad_metadata,
+                                const url::Origin& owner,
+                                const std::string& name,
+                                const GURL& bidder_report_url,
+                                const GURL& seller_report_url,
+                                const std::vector<std::string>& errors) {
   DCHECK(callback_);
 
   // Forward debug information to devtools.
@@ -244,28 +190,21 @@
         error);
   }
 
-  // Check if returned winner's information is valid.
-  ValidatedResult result =
-      ValidateAuctionResult(bidders_, render_url, owner, name);
-  if (!result.is_valid_auction_result) {
+  if (!render_url.is_valid()) {
     OnAuctionFailed();
     return;
   }
 
-  absl::optional<GURL> bidder_report_url;
-  if (bidder_report->report_requested && bidder_report->report_url.is_valid() &&
-      bidder_report->report_url.SchemeIs(url::kHttpsScheme)) {
-    bidder_report_url = bidder_report->report_url;
-  }
+  absl::optional<GURL> opt_bidder_report_url;
+  if (bidder_report_url.is_valid())
+    opt_bidder_report_url = bidder_report_url;
 
-  absl::optional<GURL> seller_report_url;
-  if (seller_report->success && seller_report->report_url.is_valid() &&
-      seller_report->report_url.SchemeIs(url::kHttpsScheme)) {
-    seller_report_url = seller_report->report_url;
-  }
+  absl::optional<GURL> opt_seller_report_url;
+  if (seller_report_url.is_valid())
+    opt_seller_report_url = seller_report_url;
 
   ad_auction_service_->GetInterestGroupManager()->RecordInterestGroupWin(
-      owner, name, result.ad_json);
+      owner, name, ad_metadata);
   // TODO(qingxin): Decide if we should record a bid if the auction fails, or
   // the interest group doesn't make a bid.
   for (const auto& bidder : bidders_) {
@@ -273,8 +212,8 @@
         bidder->group->owner, bidder->group->name);
   }
 
-  std::move(callback_).Run(this, render_url, bidder_report_url,
-                           seller_report_url);
+  std::move(callback_).Run(this, render_url, opt_bidder_report_url,
+                           opt_seller_report_url);
 }
 
 void AdAuction::OnAuctionFailed() {
diff --git a/content/browser/interest_group/ad_auction.h b/content/browser/interest_group/ad_auction.h
index 05105fae13..bd066b5 100644
--- a/content/browser/interest_group/ad_auction.h
+++ b/content/browser/interest_group/ad_auction.h
@@ -11,7 +11,7 @@
 
 #include "base/callback_forward.h"
 #include "base/memory/weak_ptr.h"
-#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom-forward.h"
+#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom-forward.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
 #include "url/gurl.h"
@@ -20,7 +20,7 @@
 namespace content {
 
 class AdAuctionServiceImpl;
-class AuctionURLLoaderFactoryProxy;
+class AuctionRunner;
 
 // Class for running a single FLEDGE auction.
 class AdAuction {
@@ -43,10 +43,6 @@
   // May invoke `AuctionCompleteCallback` synchronously.
   void StartAuction();
 
-  // Must be called when the worklet service crashes, to ensure the renderer's
-  // callback is invoked. Synchronously invokes AuctionCompleteCallback.
-  void OnServiceCrash();
-
  private:
   // Retrieves the next interest group in `pending_buyers_` from storage,
   // removing it from the vector. OnInterestGroupRead() will be invoked
@@ -66,13 +62,13 @@
   //
   // Validates the results, reports them to `callback_`, and updates the
   // InterestGroupStorage as needed.
-  void WorkletComplete(
-      const GURL& render_url,
-      const url::Origin& owner,
-      const std::string& name,
-      auction_worklet::mojom::WinningBidderReportPtr bidder_report,
-      auction_worklet::mojom::SellerReportPtr seller_report,
-      const std::vector<std::string>& errors);
+  void WorkletComplete(const GURL& render_url,
+                       const std::string& ad_metadata,
+                       const url::Origin& owner,
+                       const std::string& name,
+                       const GURL& bidder_report_url,
+                       const GURL& seller_report_url,
+                       const std::vector<std::string>& errors);
 
   // Invokes `callback_` with empty parameters, to inform it of the failure.
   void OnAuctionFailed();
@@ -93,8 +89,7 @@
   // from interest group storage.
   std::vector<url::Origin> pending_buyers_;
 
-  // Proxy used for requests from the worklet process.
-  std::unique_ptr<AuctionURLLoaderFactoryProxy> url_loader_factory_proxy_;
+  std::unique_ptr<AuctionRunner> auction_runner_;
 
   base::WeakPtrFactory<AdAuction> weak_ptr_factory_{this};
 };
diff --git a/content/browser/interest_group/ad_auction_service_impl.cc b/content/browser/interest_group/ad_auction_service_impl.cc
index 8518cdc..3d676b2f 100644
--- a/content/browser/interest_group/ad_auction_service_impl.cc
+++ b/content/browser/interest_group/ad_auction_service_impl.cc
@@ -180,8 +180,6 @@
         ServiceProcessHost::Options()
             .WithDisplayName("Auction Worklet Service")
             .Pass());
-    auction_worklet_service_.set_disconnect_handler(base::BindOnce(
-        &AdAuctionServiceImpl::OnWorkletServiceCrash, base::Unretained(this)));
   }
   return auction_worklet_service_.get();
 }
@@ -214,14 +212,4 @@
     FetchReport(factory, *seller_report_url, origin());
 }
 
-void AdAuctionServiceImpl::OnWorkletServiceCrash() {
-  // Each loop iteration calls on OnServiceCrash() on a single element of
-  // `auctions_`, which should result in it being immediately deleted. Since the
-  // loop modifies `auctions_`, need to be careful not to hold onto an iterator
-  // for the removed element, which this loop does by checking if `auctions_` is
-  // empty, rather than using a for loop.
-  while (!auctions_.empty())
-    (*auctions_.begin())->OnServiceCrash();
-}
-
 }  // namespace content
diff --git a/content/browser/interest_group/ad_auction_service_impl.h b/content/browser/interest_group/ad_auction_service_impl.h
index 56fdf26..0acf619 100644
--- a/content/browser/interest_group/ad_auction_service_impl.h
+++ b/content/browser/interest_group/ad_auction_service_impl.h
@@ -10,6 +10,7 @@
 #include <string>
 
 #include "base/containers/unique_ptr_adapters.h"
+#include "content/browser/interest_group/auction_runner.h"
 #include "content/browser/interest_group/interest_group_manager.h"
 #include "content/common/content_export.h"
 #include "content/public/browser/frame_service_base.h"
@@ -29,7 +30,8 @@
 
 // Implements the AdAuctionService service called by Blink code.
 class CONTENT_EXPORT AdAuctionServiceImpl final
-    : public FrameServiceBase<blink::mojom::AdAuctionService> {
+    : public FrameServiceBase<blink::mojom::AdAuctionService>,
+      public AuctionRunner::Delegate {
  public:
   // Factory method for creating an instance of this interface that is
   // bound to the lifetime of the frame or receiver (whichever is shorter).
@@ -43,19 +45,10 @@
 
   InterestGroupManager* GetInterestGroupManager();
 
-  // Returns an untrusted URLLoaderFactory created by the RenderFrameHost,
-  // suitable for loading URLs like subresources. Caches the factory in
-  // `frame_url_loader_factory_` for reuse.
-  network::mojom::URLLoaderFactory* GetFrameURLLoaderFactory();
-
-  // Returns a trusted URLLoaderFactory. Consumers should set
-  // ResourceRequest::TrustedParams to specify a NetworkIsolationKey when using
-  // the returned factory. Caches the factory in `trusted_url_loader_factory_`
-  // for reuse.
-  network::mojom::URLLoaderFactory* GetTrustedURLLoaderFactory();
-
-  // Launches the worklet service, if needed.
-  auction_worklet::mojom::AuctionWorkletService* GetWorkletService();
+  // AuctionRunner::Delegate implementation:
+  network::mojom::URLLoaderFactory* GetFrameURLLoaderFactory() override;
+  network::mojom::URLLoaderFactory* GetTrustedURLLoaderFactory() override;
+  auction_worklet::mojom::AuctionWorkletService* GetWorkletService() override;
 
   using FrameServiceBase::origin;
   using FrameServiceBase::render_frame_host;
@@ -77,8 +70,6 @@
                          absl::optional<GURL> bidder_report_url,
                          absl::optional<GURL> seller_report_url);
 
-  void OnWorkletServiceCrash();
-
   // This must be above `auction_worklet_service_`, since auctions may own
   // callbacks over the AuctionWorkletService pipe, and mojo pipes must be
   // destroyed before any callbacks that are bound to them.
diff --git a/content/browser/interest_group/auction_runner.cc b/content/browser/interest_group/auction_runner.cc
new file mode 100644
index 0000000..8955c24
--- /dev/null
+++ b/content/browser/interest_group/auction_runner.cc
@@ -0,0 +1,453 @@
+// 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 "content/browser/interest_group/auction_runner.h"
+
+#include <string>
+#include <vector>
+
+#include "base/callback.h"
+#include "base/callback_forward.h"
+#include "base/strings/strcat.h"
+#include "base/strings/stringprintf.h"
+#include "base/time/time.h"
+#include "content/browser/interest_group/auction_url_loader_factory_proxy.h"
+#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
+#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
+#include "content/services/auction_worklet/public/mojom/seller_worklet.mojom.h"
+#include "net/base/escape.h"
+#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
+#include "url/gurl.h"
+
+namespace content {
+
+namespace {
+
+// All URLs received from worklets must be valid HTTPS URLs. It's up to callers
+// to call ReportBadMessage() on invalid URLs.
+bool IsUrlValid(const GURL& url) {
+  return url.is_valid() && url.SchemeIs(url::kHttpsScheme);
+}
+
+// Validates that `bid` is valid and, if it is, returns the InterestGroupAd
+// corresponding to the bid. Returns nullptr and calls ReportBadMessage() if
+// not. If non-null, the returned pointer will point at the winning
+// blink::mojom::InterestGroupAd within `bid`.
+blink::mojom::InterestGroupAd* ValidateBidAndGetAd(
+    const auction_worklet::mojom::BidderWorkletBid& bid,
+    const blink::mojom::InterestGroup& interest_group) {
+  if (bid.bid <= 0 || std::isnan(bid.bid) || !std::isfinite(bid.bid)) {
+    mojo::ReportBadMessage("Invalid bid value");
+    return nullptr;
+  }
+
+  if (bid.bid_duration < base::TimeDelta()) {
+    mojo::ReportBadMessage("Invalid bid duration");
+    return nullptr;
+  }
+
+  // This should be a subset of the next case, but best to be careful.
+  if (!IsUrlValid(bid.render_url)) {
+    mojo::ReportBadMessage("Invalid bid render URL");
+    return nullptr;
+  }
+
+  // Reject URLs not listed in the interest group.
+  for (const auto& ad : interest_group.ads.value()) {
+    if (ad->render_url == bid.render_url) {
+      return ad.get();
+    }
+  }
+
+  mojo::ReportBadMessage("Bid render URL must be an ad URL");
+  return nullptr;
+}
+
+}  // namespace
+
+AuctionRunner::BidState::BidState() = default;
+AuctionRunner::BidState::~BidState() = default;
+AuctionRunner::BidState::BidState(BidState&&) = default;
+
+std::unique_ptr<AuctionRunner> AuctionRunner::CreateAndStart(
+    Delegate* delegate,
+    blink::mojom::AuctionAdConfigPtr auction_config,
+    std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders,
+    auction_worklet::mojom::BrowserSignalsPtr browser_signals,
+    const url::Origin& frame_origin,
+    RunAuctionCallback callback) {
+  std::unique_ptr<AuctionRunner> instance(new AuctionRunner(
+      delegate, std::move(auction_config), std::move(bidders),
+      std::move(browser_signals), frame_origin, std::move(callback)));
+  instance->StartBidding();
+  return instance;
+}
+
+AuctionRunner::AuctionRunner(
+    Delegate* delegate,
+    blink::mojom::AuctionAdConfigPtr auction_config,
+    std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders,
+    auction_worklet::mojom::BrowserSignalsPtr browser_signals,
+    const url::Origin& frame_origin,
+    RunAuctionCallback callback)
+    : delegate_(delegate),
+      auction_config_(std::move(auction_config)),
+      bidders_(std::move(bidders)),
+      browser_signals_(std::move(browser_signals)),
+      frame_origin_(frame_origin),
+      callback_(std::move(callback)) {}
+
+AuctionRunner::~AuctionRunner() = default;
+
+void AuctionRunner::StartBidding() {
+  // Auctions are only run when there are bidders participating. As-is, and
+  // empty bidder vector here would result in synchronously calling back into
+  // the creator, which isn't allowed.
+  DCHECK(!bidders_.empty());
+
+  outstanding_bids_ = bidders_.size();
+  bid_states_.resize(outstanding_bids_);
+
+  for (int bid_index = 0; bid_index < outstanding_bids_; ++bid_index) {
+    const auction_worklet::mojom::BiddingInterestGroupPtr& bidder =
+        bidders_[bid_index];
+    BidState* bid_state = &bid_states_[bid_index];
+    bid_state->bidder = bidder.get();
+
+    // Assemble list of URLs the bidder can request.
+
+    // TODO(mmenke): This largely duplicates logic in the auction worklet
+    // service. Avoid duplicating code.
+    absl::optional<GURL> trusted_bidding_signals_full_url;
+    if (bid_state->bidder->group->trusted_bidding_signals_url &&
+        bid_state->bidder->group->trusted_bidding_signals_keys) {
+      std::string query_params =
+          "hostname=" + net::EscapeQueryParamValue(
+                            browser_signals_->top_frame_origin.host(), true);
+      query_params += "&keys=";
+      bool first_key = true;
+      for (const auto& key :
+           *bid_state->bidder->group->trusted_bidding_signals_keys) {
+        if (first_key) {
+          first_key = false;
+        } else {
+          query_params.append(",");
+        }
+        query_params.append(net::EscapeQueryParamValue(key, true));
+      }
+
+      GURL::Replacements replacements;
+      replacements.SetQueryStr(query_params);
+      trusted_bidding_signals_full_url =
+          bid_state->bidder->group->trusted_bidding_signals_url
+              ->ReplaceComponents(replacements);
+    }
+
+    mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
+    bid_state->url_loader_factory_ =
+        std::make_unique<AuctionURLLoaderFactoryProxy>(
+            url_loader_factory.InitWithNewPipeAndPassReceiver(),
+            base::BindRepeating(&Delegate::GetTrustedURLLoaderFactory,
+                                base::Unretained(delegate_)),
+            frame_origin_, false /* use_cors */,
+            bid_state->bidder->group->bidding_url.value_or(GURL()),
+            trusted_bidding_signals_full_url);
+
+    delegate_->GetWorkletService()->LoadBidderWorkletAndGenerateBid(
+        bid_state->bidder_worklet.BindNewPipeAndPassReceiver(),
+        std::move(url_loader_factory), bidder->Clone(),
+        auction_config_->auction_signals, PerBuyerSignals(bid_state),
+        browser_signals_->top_frame_origin, browser_signals_->seller,
+        auction_start_time_,
+        base::BindOnce(&AuctionRunner::OnGenerateBidComplete,
+                       weak_ptr_factory_.GetWeakPtr(), bid_state));
+    bid_state->bidder_worklet.set_disconnect_handler(
+        base::BindOnce(&AuctionRunner::OnGenerateBidCrashed,
+                       weak_ptr_factory_.GetWeakPtr(), bid_state));
+  }
+
+  // Also initiate the script fetch for the seller script.
+  mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
+  seller_url_loader_factory_ = std::make_unique<AuctionURLLoaderFactoryProxy>(
+      url_loader_factory.InitWithNewPipeAndPassReceiver(),
+      base::BindRepeating(&Delegate::GetFrameURLLoaderFactory,
+                          base::Unretained(delegate_)),
+      frame_origin_, true /* use_cors */, auction_config_->decision_logic_url);
+  delegate_->GetWorkletService()->LoadSellerWorklet(
+      seller_worklet_.BindNewPipeAndPassReceiver(),
+      std::move(url_loader_factory), auction_config_->decision_logic_url,
+      base::BindOnce(&AuctionRunner::OnSellerWorkletLoaded,
+                     weak_ptr_factory_.GetWeakPtr()));
+  // Fail auction if the seller worklet pipe is disconnected.
+  seller_worklet_.set_disconnect_handler(base::BindOnce(
+      &AuctionRunner::FailAuctionWithError, weak_ptr_factory_.GetWeakPtr(),
+      base::StrCat({auction_config_->decision_logic_url.spec(), " crashed."})));
+}
+
+void AuctionRunner::OnGenerateBidCrashed(BidState* state) {
+  OnGenerateBidComplete(state, auction_worklet::mojom::BidderWorkletBidPtr(),
+                        std::vector<std::string>{base::StrCat(
+                            {state->bidder->group->bidding_url->spec(),
+                             " crashed while trying to run generateBid()."})});
+}
+
+void AuctionRunner::OnGenerateBidComplete(
+    BidState* state,
+    auction_worklet::mojom::BidderWorkletBidPtr bid,
+    const std::vector<std::string>& errors) {
+  DCHECK(!state->bid_result);
+  DCHECK_GT(outstanding_bids_, 0);
+
+  --outstanding_bids_;
+
+  errors_.insert(errors_.end(), errors.begin(), errors.end());
+
+  // Ignore invalid bids.
+  if (bid) {
+    state->bid_ad = ValidateBidAndGetAd(*bid, *state->bidder->group);
+    if (!state->bid_ad)
+      bid.reset();
+  }
+
+  // On failure, close the worklet pipe. On success, clear the disconnect
+  // handler - crashed bidders only matters if it's the winning bidder that
+  // crashed. That's checked for at the end of the auction.
+  if (!bid) {
+    state->bidder_worklet.reset();
+  } else {
+    state->bidder_worklet.set_disconnect_handler(base::OnceClosure());
+  }
+
+  state->bid_result = std::move(bid);
+
+  if (ReadyToScore())
+    ScoreOne();
+}
+
+void AuctionRunner::OnSellerWorkletLoaded(
+    bool load_result,
+    const std::vector<std::string>& errors) {
+  errors_.insert(errors_.end(), errors.begin(), errors.end());
+
+  if (load_result) {
+    seller_loaded_ = true;
+    if (ReadyToScore())
+      ScoreOne();
+  } else {
+    // Failed to load the seller/auction script --- nothing useful can be
+    // done, so abort, possibly cancelling other fetches, so we don't waste
+    // time.
+    FailAuction();
+  }
+}
+
+void AuctionRunner::ScoreOne() {
+  size_t num_bidders = bid_states_.size();
+
+  // Find next valid bid to score, if any.
+  while (seller_considering_ < num_bidders) {
+    BidState* bid_state = &bid_states_[seller_considering_];
+
+    // Skip over bidders that produced no valid bid.
+    if (!bid_state->bid_result) {
+      ++seller_considering_;
+      continue;
+    }
+
+    ScoreBid(bid_state);
+    return;
+  }
+
+  DCHECK_EQ(seller_considering_, num_bidders);
+  CompleteAuction();
+}
+
+void AuctionRunner::ScoreBid(const BidState* state) {
+  seller_worklet_->ScoreAd(
+      state->bid_result->ad, state->bid_result->bid, auction_config_.Clone(),
+      browser_signals_->top_frame_origin, state->bidder->group->owner,
+      AdRenderFingerprint(state),
+      state->bid_result->bid_duration.InMilliseconds(),
+      base::BindOnce(&AuctionRunner::OnBidScored,
+                     weak_ptr_factory_.GetWeakPtr()));
+}
+
+void AuctionRunner::OnBidScored(double score,
+                                const std::vector<std::string>& errors) {
+  bid_states_[seller_considering_].seller_score = score;
+  errors_.insert(errors_.end(), errors.begin(), errors.end());
+  ++seller_considering_;
+  ScoreOne();
+}
+
+std::string AuctionRunner::AdRenderFingerprint(const BidState* state) {
+  // TODO(morlovich): "Eventually this fingerprint can be a hash of the ad web
+  // bundle, but while rendering still uses the network, it should just be a
+  // hash of the rendering URL."
+  //
+  return "#####";
+}
+
+absl::optional<std::string> AuctionRunner::PerBuyerSignals(
+    const BidState* state) {
+  if (auction_config_->per_buyer_signals.has_value()) {
+    auto it = auction_config_->per_buyer_signals.value().find(
+        state->bidder->group->owner);
+    if (it != auction_config_->per_buyer_signals.value().end())
+      return it->second;
+  }
+  return absl::nullopt;
+}
+
+void AuctionRunner::CompleteAuction() {
+  double best_bid_score = 0.0;
+  BidState* best_bid = nullptr;
+  // TODO(morlovich): What if there is a tie?
+  for (BidState& bid_state : bid_states_) {
+    if (bid_state.seller_score > best_bid_score) {
+      best_bid_score = bid_state.seller_score;
+      best_bid = &bid_state;
+    }
+  }
+
+  if (best_bid) {
+    // Will eventually send a report to the seller and clean up `this`.
+    ReportSellerResult(best_bid);
+  } else {
+    FailAuction();
+  }
+}
+
+void AuctionRunner::ReportSellerResult(BidState* best_bid) {
+  DCHECK(best_bid->bid_result);
+  DCHECK_GT(best_bid->seller_score, 0);
+  seller_worklet_->ReportResult(
+      auction_config_.Clone(), browser_signals_->top_frame_origin,
+      best_bid->bidder->group->owner, best_bid->bid_result->render_url,
+      AdRenderFingerprint(best_bid), best_bid->bid_result->bid,
+      best_bid->seller_score,
+      base::BindOnce(&AuctionRunner::OnReportSellerResultComplete,
+                     weak_ptr_factory_.GetWeakPtr(), best_bid));
+}
+
+void AuctionRunner::OnReportSellerResultComplete(
+    BidState* best_bid,
+    const absl::optional<std::string>& signals_for_winner,
+    const absl::optional<GURL>& seller_report_url,
+    const std::vector<std::string>& errors) {
+  signals_for_winner_ = signals_for_winner;
+  seller_report_url_ = seller_report_url;
+  errors_.insert(errors_.end(), errors.begin(), errors.end());
+
+  absl::optional<GURL> opt_bidder_report_url;
+  if (seller_report_url && !IsUrlValid(*seller_report_url)) {
+    mojo::ReportBadMessage("Invalid seller report URL");
+    FailAuction();
+    return;
+  }
+
+  ReportBidWin(best_bid);
+}
+
+void AuctionRunner::ReportBidWin(BidState* best_bid) {
+  CHECK(best_bid->bid_result);
+  std::string signals_for_winner_arg;
+  // TODO(mmenke): It's unclear what should happen here if
+  // `signals_for_winner_` is null. As-is, an empty string will result in the
+  // BidderWorklet's ReportWin() method failing, since it's not valid JSON.
+  if (signals_for_winner_)
+    signals_for_winner_arg = *signals_for_winner_;
+
+  // Fail the auction if the winning bidder process has crashed.
+  //
+  // TODO(mmenke): Be smarter about process crashes in general. Even without
+  // the report URL, can display the ad and report to the seller (though will
+  // need to think more about that case).
+  //
+  // TODO(mmenke): Make this FailAuction call (And likely others as well) add
+  // a failure to `messages_`.
+  if (!best_bid->bidder_worklet.is_connected()) {
+    FailAuctionWithError(
+        base::StrCat({best_bid->bidder->group->bidding_url->spec(),
+                      " crashed while idle."}));
+    return;
+  }
+
+  best_bid->bidder_worklet->ReportWin(
+      signals_for_winner_arg, best_bid->bid_result->render_url,
+      AdRenderFingerprint(best_bid), best_bid->bid_result->bid,
+      base::BindOnce(&AuctionRunner::OnReportBidWinComplete,
+                     weak_ptr_factory_.GetWeakPtr(), best_bid));
+  best_bid->bidder_worklet.set_disconnect_handler(base::BindOnce(
+      &AuctionRunner::FailAuctionWithError, weak_ptr_factory_.GetWeakPtr(),
+      base::StrCat({best_bid->bidder->group->bidding_url->spec(),
+                    " crashed while trying to run reportWin()."})));
+}
+
+void AuctionRunner::OnReportBidWinComplete(
+    const BidState* best_bid,
+    const absl::optional<GURL>& bidder_report_url,
+    const std::vector<std::string>& errors) {
+  if (bidder_report_url && !IsUrlValid(*bidder_report_url)) {
+    mojo::ReportBadMessage("Invalid bidder report URL");
+    FailAuction();
+    return;
+  }
+
+  bidder_report_url_ = bidder_report_url;
+  errors_.insert(errors_.end(), errors.begin(), errors.end());
+  ReportSuccess(best_bid);
+}
+
+void AuctionRunner::FailAuction() {
+  DCHECK(callback_);
+  ClosePipes();
+
+  std::move(callback_).Run(GURL(), std::string(), url::Origin(), std::string(),
+                           GURL(), GURL(), errors_);
+}
+
+void AuctionRunner::FailAuctionWithError(std::string error) {
+  errors_.emplace_back(std::move(error));
+  FailAuction();
+}
+
+void AuctionRunner::ReportSuccess(const BidState* state) {
+  DCHECK(callback_);
+  DCHECK(state->bid_result);
+  ClosePipes();
+
+  std::string ad_metadata;
+  if (state->bid_ad->metadata) {
+    //`metadata` is already in JSON so no quotes are needed.
+    ad_metadata =
+        base::StringPrintf(R"({"render_url":"%s","metadata":%s})",
+                           state->bid_result->render_url.spec().c_str(),
+                           state->bid_ad->metadata.value().c_str());
+  } else {
+    ad_metadata = base::StringPrintf(
+        R"({"render_url":"%s"})", state->bid_result->render_url.spec().c_str());
+  }
+
+  std::move(callback_).Run(
+      state->bid_result->render_url, ad_metadata, state->bidder->group->owner,
+      state->bidder->group->name,
+      bidder_report_url_.has_value() ? *bidder_report_url_ : GURL(),
+      seller_report_url_.has_value() ? *seller_report_url_ : GURL(), errors_);
+}
+
+void AuctionRunner::ClosePipes() {
+  // This is needed in addition to closing worklet pipes in order to ignore
+  // worklet creation callbacks.
+  weak_ptr_factory_.InvalidateWeakPtrs();
+
+  for (BidState& bid_state : bid_states_) {
+    bid_state.bidder_worklet.reset();
+  }
+  seller_worklet_.reset();
+}
+
+}  // namespace content
diff --git a/content/browser/interest_group/auction_runner.h b/content/browser/interest_group/auction_runner.h
new file mode 100644
index 0000000..569aca3
--- /dev/null
+++ b/content/browser/interest_group/auction_runner.h
@@ -0,0 +1,255 @@
+// 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 CONTENT_BROWSER_INTEREST_GROUP_AUCTION_RUNNER_H_
+#define CONTENT_BROWSER_INTEREST_GROUP_AUCTION_RUNNER_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/callback.h"
+#include "base/logging.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "content/common/content_export.h"
+#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom-forward.h"
+#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
+#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
+#include "content/services/auction_worklet/public/mojom/seller_worklet.mojom.h"
+#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
+#include "url/gurl.h"
+
+namespace content {
+
+class AuctionURLLoaderFactoryProxy;
+
+// An AuctionRunner loads and runs the bidder and seller worklets, along with
+// their reporting phases and produces the result via a callback.
+//
+// At present it initiates all fetches in parallel, running all bidder scripts
+// once they and any trusted signals they need are ready, then when all bids are
+// in runs all the scoring, and finally the reporting worklets.
+//
+// TODO(morlovich): There is no need to wait for all bidders to finish to start
+// scoring.
+//
+// TODO(mmenke): Merge this with `ad_auction`, and provide worklet-specific
+// URLLoaderFactories.
+//
+// TODO(mmenke): Add checking of values returned by auctions (e.g., for bids <=
+// 0).
+class CONTENT_EXPORT AuctionRunner {
+ public:
+  // Invoked when a FLEDGE auction is complete.
+  //
+  // `render_url` URL of auction winning ad to render.
+  //  An empty URL is used if there is no winner.
+  //
+  // `ad_metadata` The metadata for the winning ad.
+  //
+  // `winning_interest_group_owner` owner of the winning interest group.
+  //  An opaque origin if there is no winner.
+  //
+  // `winning_interest_group_name` name of winning interest group. Empty if
+  //  there is no winner.
+  //
+  // `bidder_report_url` URL to use for reporting result to the bidder. Empty if
+  //  no report should be sent.
+  //
+  // `seller_report`  URL to use for reporting result to the seller. Empty if no
+  //  report should be sent.
+  //
+  // `errors` are various error messages to be used for debugging. These are too
+  //  sensitive for the renderers to see.
+  using RunAuctionCallback =
+      base::OnceCallback<void(const GURL& render_url,
+                              const std::string& ad_metadata,
+                              const url::Origin& winning_interest_group_owner,
+                              const std::string& winning_interest_group_name,
+                              const GURL& bidder_report_url,
+                              const GURL& seller_report_url,
+                              const std::vector<std::string>& errors)>;
+
+  // Delegate class to allow dependency injection in tests. Note that all
+  // objects this returns can crash and be restarted, so passing in raw pointers
+  // would be problematic.
+  class Delegate {
+   public:
+    // Returns the URLLoaderFactory of the frame running the auction. Used to
+    // load the seller worklet in the context of the parent frame, since unlike
+    // other worklet types, it has no first party opt-in, and it's not a
+    // cross-origin leak if the parent from knows its URL, since the parent
+    // frame provided the URL in the first place.
+    virtual network::mojom::URLLoaderFactory* GetFrameURLLoaderFactory() = 0;
+
+    // Trusted URLLoaderFactory used to load bidder worklets.
+    virtual network::mojom::URLLoaderFactory* GetTrustedURLLoaderFactory() = 0;
+
+    // Returns the AuctionWorkletService.
+    virtual auction_worklet::mojom::AuctionWorkletService*
+    GetWorkletService() = 0;
+  };
+
+  explicit AuctionRunner(const AuctionRunner&) = delete;
+  AuctionRunner& operator=(const AuctionRunner&) = delete;
+
+  // Runs an entire FLEDGE auction.
+  //
+  // Arguments:
+  // `delegate` must remain valid until the AuctionRunner is destroyed.
+  //
+  // `auction_config` is the configuration provided by client JavaScript in
+  //  the renderer in order to initiate the auction.
+  //
+  // `bidders` includes definitions of the interest groups that are selected to
+  //  participate in this auction (initially added by client JS in the renderer,
+  //  but managed by the browser's interest group store), as well as some
+  //  bidding history collected by the interest group store. The bidding
+  //  worklets of these groups will be fetched and executed. `bidders` must not
+  //  be empty.
+  //
+  // `browser_signals` signals from the browser about the auction that are the
+  //  same for all worklets.
+  //
+  // `frame_origin` is the origin running the auction (not the top frame
+  // origin), used as the initiator in network requests.
+  static std::unique_ptr<AuctionRunner> CreateAndStart(
+      Delegate* delegate,
+      blink::mojom::AuctionAdConfigPtr auction_config,
+      std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders,
+      auction_worklet::mojom::BrowserSignalsPtr browser_signals,
+      const url::Origin& frame_origin,
+      RunAuctionCallback callback);
+
+  ~AuctionRunner();
+
+ private:
+  struct BidState {
+    BidState();
+    BidState(BidState&&);
+    ~BidState();
+
+    auction_worklet::mojom::BiddingInterestGroup* bidder = nullptr;
+
+    // URLLoaderFactory proxy class configured only to load the URLs the bidder
+    // needs.
+    std::unique_ptr<AuctionURLLoaderFactoryProxy> url_loader_factory_;
+
+    mojo::Remote<auction_worklet::mojom::BidderWorklet> bidder_worklet;
+    auction_worklet::mojom::BidderWorkletBidPtr bid_result;
+    // Points to the InterestGroupAd within `bidder` that won the auction. Only
+    // nullptr when `bid_result` is also nullptr.
+    blink::mojom::InterestGroupAd* bid_ad = nullptr;
+
+    double seller_score = 0;
+  };
+
+  AuctionRunner(
+      Delegate* delegate,
+      blink::mojom::AuctionAdConfigPtr auction_config,
+      std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders,
+      auction_worklet::mojom::BrowserSignalsPtr browser_signals,
+      const url::Origin& frame_origin,
+      RunAuctionCallback callback);
+
+  void StartBidding();
+  void OnGenerateBidCrashed(BidState* state);
+  void OnGenerateBidComplete(BidState* state,
+                             auction_worklet::mojom::BidderWorkletBidPtr bid,
+                             const std::vector<std::string>& errors);
+
+  // True if all bid results and the seller script load are complete.
+  bool ReadyToScore() const { return outstanding_bids_ == 0 && seller_loaded_; }
+  void OnSellerWorkletLoaded(bool load_result,
+                             const std::vector<std::string>& errors);
+
+  // Calls into the seller asynchronously to score each outstanding bid, in
+  // series. Once there are no outstanding bids, proceeds to selecting the
+  // winner and running the Worklets reporting methods.
+  void ScoreOne();
+  void ScoreBid(const BidState* state);
+  // Callback from ScoreBid().
+  void OnBidScored(double score, const std::vector<std::string>& errors);
+
+  std::string AdRenderFingerprint(const BidState* state);
+  absl::optional<std::string> PerBuyerSignals(const BidState* state);
+
+  // Completes the auction, invoking `callback_`. Consumer must be able to
+  // safely delete `this` when the callback is invoked.
+  void CompleteAuction();
+
+  // Sequence of asynchronous methods to call into the bidder/seller results to
+  // report a a win, Will ultimately invoke ReportSuccess(), which will delete
+  // the auction.
+  void ReportSellerResult(BidState* state);
+  void OnReportSellerResultComplete(
+      BidState* best_bid,
+      const absl::optional<std::string>& signals_for_winner,
+      const absl::optional<GURL>& seller_report_url,
+      const std::vector<std::string>& error_msgs);
+  void ReportBidWin(BidState* state);
+  void OnReportBidWinComplete(const BidState* best_bid,
+                              const absl::optional<GURL>& bidder_report_url,
+                              const std::vector<std::string>& error_msgs);
+
+  // These complete the auction, invoking `callback_` and preventing any future
+  // calls into `this` by closing mojo pipes and disposing of weak pointers. The
+  // owner must be able to safely delete `this` when the callback is invoked.
+  void FailAuction();
+  // Appends `error` to `errors_` before calling FailAuciton().
+  void FailAuctionWithError(std::string error);
+  void ReportSuccess(const BidState* state);
+
+  // Closes all open pipes, to avoid receiving any Mojo callbacks after
+  // completion.
+  void ClosePipes();
+
+  Delegate* const delegate_;
+
+  // Configuration.
+  blink::mojom::AuctionAdConfigPtr auction_config_;
+  std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders_;
+  auction_worklet::mojom::BrowserSignalsPtr browser_signals_;
+  const url::Origin frame_origin_;
+  RunAuctionCallback callback_;
+
+  // State for the bidding phase.
+  int outstanding_bids_;  // number of bids for which we're waiting on a fetch.
+  std::vector<BidState> bid_states_;  // parallel to `bidders_`.
+  // The time the auction started. Use a single base time for all Worklets, to
+  // present a more consistent view of the universe.
+  const base::Time auction_start_time_ = base::Time::Now();
+
+  // URLLoaderFactory proxy class configured only to load the URL the seller
+  // needs.
+  std::unique_ptr<AuctionURLLoaderFactoryProxy> seller_url_loader_factory_;
+
+  // State for the scoring phase.
+  mojo::Remote<auction_worklet::mojom::SellerWorklet> seller_worklet_;
+
+  // This is true if the seller script has been loaded successfully --- if the
+  // load failed, the entire process is aborted since there is nothing useful
+  // that can be done.
+  bool seller_loaded_ = false;
+  size_t seller_considering_ = 0;
+
+  // Seller script reportResult() results.
+  absl::optional<std::string> signals_for_winner_;
+  absl::optional<GURL> seller_report_url_;
+
+  // Bidder script reportWin() results.
+  absl::optional<GURL> bidder_report_url_;
+
+  // All errors reported by worklets thus far.
+  std::vector<std::string> errors_;
+
+  base::WeakPtrFactory<AuctionRunner> weak_ptr_factory_{this};
+};
+
+}  // namespace content
+
+#endif  // CONTENT_BROWSER_INTEREST_GROUP_AUCTION_RUNNER_H_
diff --git a/content/browser/interest_group/auction_runner_unittest.cc b/content/browser/interest_group/auction_runner_unittest.cc
new file mode 100644
index 0000000..3d912ca9
--- /dev/null
+++ b/content/browser/interest_group/auction_runner_unittest.cc
@@ -0,0 +1,1863 @@
+// 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 "content/browser/interest_group/auction_runner.h"
+
+#include <limits>
+#include <vector>
+
+#include "base/callback_helpers.h"
+#include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/bind.h"
+#include "base/test/task_environment.h"
+#include "base/time/time.h"
+#include "content/services/auction_worklet/auction_worklet_service_impl.h"
+#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
+#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
+#include "content/services/auction_worklet/public/mojom/seller_worklet.mojom.h"
+#include "content/services/auction_worklet/worklet_test_util.h"
+#include "mojo/public/cpp/system/functions.h"
+#include "net/http/http_status_code.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock-matchers.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+namespace {
+
+std::string MakeBidScript(const std::string& bid,
+                          const std::string& render_url,
+                          const url::Origin& interest_group_owner,
+                          const std::string& interest_group_name,
+                          bool has_signals,
+                          const std::string& signal_key,
+                          const std::string& signal_val) {
+  // TODO(morlovich): Use JsReplace.
+  constexpr char kBidScript[] = R"(
+    const bid = %s;
+    const renderUrl = "%s";
+    const interestGroupOwner = "%s";
+    const interestGroupName = "%s";
+    const hasSignals = %s;
+
+    function generateBid(interestGroup, auctionSignals, perBuyerSignals,
+                         trustedBiddingSignals, browserSignals) {
+      var result = {ad: {bidKey: "data for " + bid,
+                         groupName: interestGroupName},
+                    bid: bid, render: renderUrl};
+      if (interestGroup.name !== interestGroupName)
+        throw new Error("wrong interestGroupName");
+      if (interestGroup.owner !== interestGroupOwner)
+        throw new Error("wrong interestGroupOwner");
+      if (interestGroup.ads.length != 1)
+        throw new Error("wrong interestGroup.ads length");
+      if (interestGroup.ads[0].renderUrl != renderUrl)
+        throw new Error("wrong interestGroup.ads URL");
+      if (perBuyerSignals.signalsFor !== interestGroupName)
+        throw new Error("wrong perBuyerSignals");
+      if (!auctionSignals.isAuctionSignals)
+        throw new Error("wrong auctionSignals");
+      if (hasSignals) {
+        if ('extra' in trustedBiddingSignals)
+          throw new Error("why extra?");
+        if (trustedBiddingSignals["%s"] !== "%s")
+          throw new Error("wrong signals");
+      } else if (trustedBiddingSignals !== null) {
+        throw new Error("Expected null trustedBiddingSignals");
+      }
+      if (browserSignals.topWindowHostname !== 'publisher1.com')
+        throw new Error("wrong topWindowHostname");
+      if (browserSignals.seller != 'https://adstuff.publisher1.com')
+         throw new Error("wrong seller");
+      if (browserSignals.joinCount !== 3)
+        throw new Error("joinCount")
+      if (browserSignals.bidCount !== 5)
+        throw new Error("bidCount");
+      if (browserSignals.prevWins.length !== 3)
+        throw new Error("prevWins");
+      for (let i = 0; i < browserSignals.prevWins.length; ++i) {
+        if (!(browserSignals.prevWins[i] instanceof Array))
+          throw new Error("prevWins entry not an array");
+        if (typeof browserSignals.prevWins[i][0] != "number")
+          throw new Error("Not a Number in prevWin?");
+        if (browserSignals.prevWins[i][1].winner !== -i)
+          throw new Error("prevWin MD not what passed in");
+      }
+      return result;
+    }
+
+    function reportWin(auctionSignals, perBuyerSignals, sellerSignals,
+                       browserSignals) {
+      if (!auctionSignals.isAuctionSignals)
+        throw new Error("wrong auctionSignals");
+      if (perBuyerSignals.signalsFor !== interestGroupName)
+        throw new Error("wrong perBuyerSignals");
+
+      // sellerSignals in these tests is just sellers' browserSignals, since
+      // that's what reportResult passes through.
+      if (sellerSignals.topWindowHostname !== 'publisher1.com')
+        throw new Error("wrong topWindowHostname");
+      if (sellerSignals.interestGroupOwner !== interestGroupOwner)
+        throw new Error("wrong interestGroupOwner");
+      if (sellerSignals.renderUrl !== renderUrl)
+        throw new Error("wrong renderUrl");
+      if (sellerSignals.bid !== bid)
+        throw new Error("wrong bid");
+      if (sellerSignals.desirability !== (bid * 2))
+        throw new Error("wrong desirability");
+
+      if (browserSignals.topWindowHostname !== 'publisher1.com')
+        throw new Error("wrong browserSignals.topWindowHostname");
+      if ("desirability" in browserSignals)
+        throw new Error("why is desirability here?");
+      if (browserSignals.interestGroupName !== interestGroupName)
+        throw new Error("wrong browserSignals.interestGroupName");
+      if (browserSignals.interestGroupOwner !== interestGroupOwner)
+        throw new Error("wrong browserSignals.interestGroupOwner");
+
+      if (browserSignals.renderUrl !== renderUrl)
+        throw new Error("wrong browserSignals.renderUrl");
+      if (browserSignals.bid !== bid)
+        throw new Error("wrong browserSignals.bid");
+
+      sendReportTo("https://buyer-reporting.example.com");
+    }
+  )";
+  return base::StringPrintf(
+      kBidScript, bid.c_str(), render_url.c_str(),
+      interest_group_owner.Serialize().c_str(), interest_group_name.c_str(),
+      has_signals ? "true" : "false", signal_key.c_str(), signal_val.c_str());
+}
+
+// This can be appended to the standard script to override the function.
+constexpr char kReportWinNoUrl[] = R"(
+  function reportWin(auctionSignals, perBuyerSignals, sellerSignals,
+                     browserSignals) {
+  }
+)";
+
+constexpr char kCheckingAuctionScript[] = R"(
+  function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals,
+                   browserSignals) {
+    if (adMetadata.bidKey !== ("data for " + bid)) {
+      throw new Error("wrong data for bid:" +
+                      JSON.stringify(adMetadata) + "/" + bid);
+    }
+    if (auctionConfig.decisionLogicUrl
+        !== "https://adstuff.publisher1.com/auction.js") {
+      throw new Error("wrong auctionConfig");
+    }
+    if (auctionConfig.perBuyerSignals['adplatform.com'].signalsFor
+        !== 'Ad Platform') {
+      throw new Error("Wrong perBuyerSignals in auctionConfig");
+    }
+    if (!auctionConfig.sellerSignals.isSellerSignals)
+      throw new Error("Wrong sellerSignals");
+    if (browserSignals.topWindowHostname !== 'publisher1.com')
+      throw new Error("wrong topWindowHostname");
+    if ("joinCount" in browserSignals)
+      throw new Error("wrong kind of browser signals");
+    if (browserSignals.adRenderFingerprint !== "#####")
+      throw new Error("wrong adRenderFingerprint");
+    if (typeof browserSignals.biddingDurationMsec !== "number")
+      throw new Error("biddingDurationMsec is not a number. huh");
+    if (browserSignals.biddingDurationMsec < 0)
+      throw new Error("biddingDurationMsec should be non-negative.");
+
+    return bid * 2;
+  }
+)";
+
+constexpr char kReportResultScript[] = R"(
+  function reportResult(auctionConfig, browserSignals) {
+    if (auctionConfig.decisionLogicUrl
+        !== "https://adstuff.publisher1.com/auction.js") {
+      throw new Error("wrong auctionConfig");
+    }
+    if (browserSignals.topWindowHostname !== 'publisher1.com')
+      throw new Error("wrong topWindowHostname");
+    sendReportTo("https://reporting.example.com");
+    return browserSignals;
+  }
+)";
+
+constexpr char kReportResultScriptNoUrl[] = R"(
+  function reportResult(auctionConfig, browserSignals) {
+    return browserSignals;
+  }
+)";
+
+std::string MakeAuctionScript() {
+  return std::string(kCheckingAuctionScript) + kReportResultScript;
+}
+
+std::string MakeAuctionScriptNoReportUrl() {
+  return std::string(kCheckingAuctionScript) + kReportResultScriptNoUrl;
+}
+
+const char kAuctionScriptRejects2[] = R"(
+  function scoreAd(adMetadata, bid, auctionConfig, browserSignals) {
+    if (bid === 2)
+      return -1;
+    return bid + 1;
+  }
+)";
+
+std::string MakeAuctionScriptReject2() {
+  return std::string(kAuctionScriptRejects2) + kReportResultScript;
+}
+
+// BidderWorklet that holds onto passed in callbacks, to let the test fixture
+// invoke them.
+class MockBidderWorklet : public auction_worklet::mojom::BidderWorklet {
+ public:
+  explicit MockBidderWorklet(
+      mojo::PendingReceiver<auction_worklet::mojom::BidderWorklet>
+          pending_receiver,
+      mojo::PendingRemote<network::mojom::URLLoaderFactory>
+          pending_url_loader_factory,
+      auction_worklet::mojom::AuctionWorkletService::
+          LoadBidderWorkletAndGenerateBidCallback
+              load_bidder_worklet_and_generate_bid_callback)
+      : load_bidder_worklet_and_generate_bid_callback_(
+            std::move(load_bidder_worklet_and_generate_bid_callback)),
+        url_loader_factory_(std::move(pending_url_loader_factory)),
+        receiver_(this, std::move(pending_receiver)) {}
+
+  MockBidderWorklet(const MockBidderWorklet&) = delete;
+  const MockBidderWorklet& operator=(const MockBidderWorklet&) = delete;
+
+  ~MockBidderWorklet() override = default;
+
+  // auction_worklet::mojom::BidderWorklet implementation:
+  void ReportWin(const std::string& seller_signals_json,
+                 const GURL& browser_signal_render_url,
+                 const std::string& browser_signal_ad_render_fingerprint,
+                 double browser_signal_bid,
+                 ReportWinCallback report_win_callback) override {
+    report_win_callback_ = std::move(report_win_callback);
+    if (report_win_run_loop_)
+      report_win_run_loop_->Quit();
+  }
+
+  void CompleteLoadingAndBid(double bid,
+                             const GURL& render_url,
+                             base::TimeDelta duration = base::TimeDelta()) {
+    DCHECK(load_bidder_worklet_and_generate_bid_callback_);
+    std::move(load_bidder_worklet_and_generate_bid_callback_)
+        .Run(auction_worklet::mojom::BidderWorkletBid::New(
+                 "ad", bid, render_url, duration),
+             std::vector<std::string>() /* errors */);
+  }
+
+  void CompleteLoadingWithoutBid() {
+    DCHECK(load_bidder_worklet_and_generate_bid_callback_);
+    std::move(load_bidder_worklet_and_generate_bid_callback_)
+        .Run(nullptr /* bid */, std::vector<std::string>() /* errors */);
+  }
+
+  // Returns the LoadBidderWorkletAndGenerateBidCallback for a worklet. Needed
+  // for cases when the BidderWorklet is destroyed (to close its pipe) before
+  // the AuctionWorkletService is destroyed, since Mojo DCHECKs if a callback is
+  // destroyed when the pipe its over is still live.
+  //
+  // TODO(mmenke): To better simulate real crashes, give worklets their own
+  // AuctionWorkletService pipes, and remove this method.
+  auction_worklet::mojom::AuctionWorkletService::
+      LoadBidderWorkletAndGenerateBidCallback
+      TakeLoadCallback() {
+    return std::move(load_bidder_worklet_and_generate_bid_callback_);
+  }
+
+  void WaitForReportWin() {
+    DCHECK(!load_bidder_worklet_and_generate_bid_callback_);
+    DCHECK(!report_win_run_loop_);
+    if (!report_win_callback_) {
+      report_win_run_loop_ = std::make_unique<base::RunLoop>();
+      report_win_run_loop_->Run();
+      report_win_run_loop_.reset();
+      DCHECK(report_win_callback_);
+    }
+  }
+
+  void InvokeReportWinCallback(
+      absl::optional<GURL> report_url = absl::nullopt) {
+    DCHECK(report_win_callback_);
+    std::move(report_win_callback_)
+        .Run(report_url, std::vector<std::string>() /* errors */);
+  }
+
+  mojo::Remote<network::mojom::URLLoaderFactory>& url_loader_factory() {
+    return url_loader_factory_;
+  }
+
+ private:
+  auction_worklet::mojom::AuctionWorkletService::
+      LoadBidderWorkletAndGenerateBidCallback
+          load_bidder_worklet_and_generate_bid_callback_;
+
+  std::unique_ptr<base::RunLoop> report_win_run_loop_;
+  ReportWinCallback report_win_callback_;
+
+  mojo::Remote<network::mojom::URLLoaderFactory> url_loader_factory_;
+
+  // Receiver is last so that destroying `this` while there's a pending callback
+  // over the pipe will not DCHECK.
+  mojo::Receiver<auction_worklet::mojom::BidderWorklet> receiver_;
+};
+
+// SellerWorklet that holds onto passed in callbacks, to let the test fixture
+// invoke them.
+class MockSellerWorklet : public auction_worklet::mojom::SellerWorklet {
+ public:
+  // Subset of parameters passed to SellerWorklet's ScoreAd method.
+  struct ScoreAdParams {
+    double bid;
+    url::Origin interest_group_owner;
+  };
+
+  explicit MockSellerWorklet(
+      mojo::PendingReceiver<auction_worklet::mojom::SellerWorklet>
+          pending_receiver,
+      mojo::PendingRemote<network::mojom::URLLoaderFactory>
+          pending_url_loader_factory,
+      auction_worklet::mojom::AuctionWorkletService::LoadSellerWorkletCallback
+          load_worklet_callback)
+      : load_worklet_callback_(std::move(load_worklet_callback)),
+        url_loader_factory_(std::move(pending_url_loader_factory)),
+        receiver_(this, std::move(pending_receiver)) {}
+
+  MockSellerWorklet(const MockSellerWorklet&) = delete;
+  const MockSellerWorklet& operator=(const MockSellerWorklet&) = delete;
+
+  ~MockSellerWorklet() override = default;
+
+  // auction_worklet::mojom::SellerWorklet implementation:
+
+  void ScoreAd(const std::string& ad_metadata_json,
+               double bid,
+               blink::mojom::AuctionAdConfigPtr auction_config,
+               const url::Origin& browser_signal_top_window_origin,
+               const url::Origin& browser_signal_interest_group_owner,
+               const std::string& browser_signal_ad_render_fingerprint,
+               uint32_t browser_signal_bidding_duration_msecs,
+               ScoreAdCallback score_ad_callback) override {
+    score_ad_callback_ = std::move(score_ad_callback);
+    score_ad_params_ = std::make_unique<ScoreAdParams>();
+    score_ad_params_->bid = bid;
+    score_ad_params_->interest_group_owner =
+        browser_signal_interest_group_owner;
+    if (score_ad_run_loop_)
+      score_ad_run_loop_->Quit();
+  }
+
+  void ReportResult(blink::mojom::AuctionAdConfigPtr auction_config,
+                    const url::Origin& browser_signal_top_window_origin,
+                    const url::Origin& browser_signal_interest_group_owner,
+                    const GURL& browser_signal_render_url,
+                    const std::string& browser_signal_ad_render_fingerprint,
+                    double browser_signal_bid,
+                    double browser_signal_desirability,
+                    ReportResultCallback report_result_callback) override {
+    report_result_callback_ = std::move(report_result_callback);
+    if (report_result_run_loop_)
+      report_result_run_loop_->Quit();
+  }
+
+  // Informs the consumer that the seller worklet has successfully loaded.
+  void CompleteLoading() {
+    DCHECK(load_worklet_callback_);
+    std::move(load_worklet_callback_)
+        .Run(true /* success */, std::vector<std::string>() /* errors */);
+  }
+
+  // Waits until ScoreAd() has been invoked, if it hasn't been already.
+  std::unique_ptr<ScoreAdParams> WaitForScoreAd() {
+    DCHECK(!score_ad_run_loop_);
+    DCHECK(!load_worklet_callback_);
+    if (!score_ad_params_) {
+      score_ad_run_loop_ = std::make_unique<base::RunLoop>();
+      score_ad_run_loop_->Run();
+      score_ad_run_loop_.reset();
+      DCHECK(score_ad_params_);
+    }
+    return std::move(score_ad_params_);
+  }
+
+  // Invokes the ScoreAdCallback for the most recent ScoreAd() call with the
+  // provided score. WaitForScoreAd() must have been invoked first.
+  void InvokeScoreAdCallback(double score) {
+    DCHECK(score_ad_callback_);
+    DCHECK(!score_ad_params_);
+    std::move(score_ad_callback_)
+        .Run(score, std::vector<std::string>() /* errors */);
+  }
+
+  void WaitForReportResult() {
+    DCHECK(!report_result_run_loop_);
+    DCHECK(!load_worklet_callback_);
+    if (!report_result_callback_) {
+      report_result_run_loop_ = std::make_unique<base::RunLoop>();
+      report_result_run_loop_->Run();
+      report_result_run_loop_.reset();
+      DCHECK(report_result_callback_);
+    }
+  }
+
+  // Invokes the ReportResultCallback for the most recent ScoreAd() call with
+  // the provided score. WaitForReportResult() must have been invoked first.
+  void InvokeReportResultCallback(
+      absl::optional<GURL> report_url = absl::nullopt) {
+    DCHECK(report_result_callback_);
+    std::move(report_result_callback_)
+        .Run(absl::nullopt /* signals_for_winner */, std::move(report_url),
+             std::vector<std::string>() /* errors */);
+  }
+
+  mojo::Remote<network::mojom::URLLoaderFactory>& url_loader_factory() {
+    return url_loader_factory_;
+  }
+
+ private:
+  auction_worklet::mojom::AuctionWorkletService::LoadSellerWorkletCallback
+      load_worklet_callback_;
+
+  std::unique_ptr<base::RunLoop> score_ad_run_loop_;
+  std::unique_ptr<ScoreAdParams> score_ad_params_;
+  ScoreAdCallback score_ad_callback_;
+
+  std::unique_ptr<base::RunLoop> report_result_run_loop_;
+  ReportResultCallback report_result_callback_;
+
+  mojo::Remote<network::mojom::URLLoaderFactory> url_loader_factory_;
+
+  // Receiver is last so that destroying `this` while there's a pending callback
+  // over the pipe will not DCHECK.
+  mojo::Receiver<auction_worklet::mojom::SellerWorklet> receiver_;
+};
+
+// AuctionWorkletService that creates MockBidderWorklets and MockSellerWorklets
+// to hold onto passed in PendingReceivers and Callbacks.
+class MockAuctionWorkletService
+    : public auction_worklet::mojom::AuctionWorkletService {
+ public:
+  explicit MockAuctionWorkletService(
+      mojo::PendingReceiver<auction_worklet::mojom::AuctionWorkletService>
+          pending_receiver)
+      : receiver_(this, std::move(pending_receiver)) {}
+
+  ~MockAuctionWorkletService() override = default;
+
+  // auction_worklet::mojom::AuctionWorkletService implementation:
+
+  void LoadBidderWorkletAndGenerateBid(
+      mojo::PendingReceiver<auction_worklet::mojom::BidderWorklet>
+          bidder_worklet_receiver,
+      mojo::PendingRemote<network::mojom::URLLoaderFactory>
+          pending_url_loader_factory,
+      auction_worklet::mojom::BiddingInterestGroupPtr bidding_interest_group,
+      const absl::optional<std::string>& auction_signals_json,
+      const absl::optional<std::string>& per_buyer_signals_json,
+      const url::Origin& top_window_origin,
+      const url::Origin& seller_origin,
+      base::Time auction_start_time,
+      LoadBidderWorkletAndGenerateBidCallback
+          load_bidder_worklet_and_generate_bid_callback) override {
+    InterestGroupId interest_group_id(bidding_interest_group->group->owner,
+                                      bidding_interest_group->group->name);
+    EXPECT_EQ(0u, bidder_worklets_.count(interest_group_id));
+    bidder_worklets_.emplace(std::make_pair(
+        interest_group_id,
+        std::make_unique<MockBidderWorklet>(
+            std::move(bidder_worklet_receiver),
+            std::move(pending_url_loader_factory),
+            std::move(load_bidder_worklet_and_generate_bid_callback))));
+
+    ASSERT_GT(waiting_for_num_bidders_, 0);
+    --waiting_for_num_bidders_;
+    MaybeQuitRunLoop();
+  }
+
+  void LoadSellerWorklet(
+      mojo::PendingReceiver<auction_worklet::mojom::SellerWorklet>
+          seller_worklet_receiver,
+      mojo::PendingRemote<network::mojom::URLLoaderFactory>
+          pending_url_loader_factory,
+      const GURL& script_source_url,
+      LoadSellerWorkletCallback load_seller_worklet_callback) override {
+    DCHECK(!seller_worklet_);
+
+    seller_worklet_ = std::make_unique<MockSellerWorklet>(
+        std::move(seller_worklet_receiver),
+        std::move(pending_url_loader_factory),
+        std::move(load_seller_worklet_callback));
+
+    ASSERT_TRUE(waiting_on_seller_);
+    waiting_on_seller_ = false;
+    MaybeQuitRunLoop();
+  }
+
+  // Waits for a SellerWorklet and `num_bidders` bidder worklets to be created.
+  void WaitForWorklets(int num_bidders) {
+    waiting_on_seller_ = true;
+    waiting_for_num_bidders_ = num_bidders;
+    run_loop_ = std::make_unique<base::RunLoop>();
+    run_loop_->Run();
+    run_loop_.reset();
+  }
+
+  // Returns the MockBidderWorklet created for the specified interest group
+  // origin and name, if there is one.
+  std::unique_ptr<MockBidderWorklet> TakeBidderWorklet(
+      const url::Origin& interest_group_owner_origin,
+      const std::string& interest_group_name) {
+    InterestGroupId interest_group_id(interest_group_owner_origin,
+                                      interest_group_name);
+    auto it = bidder_worklets_.find(interest_group_id);
+    if (it == bidder_worklets_.end())
+      return nullptr;
+    std::unique_ptr<MockBidderWorklet> out = std::move(it->second);
+    bidder_worklets_.erase(it);
+    return out;
+  }
+
+  // Returns the MockSellerWorklet, if one has been created.
+  std::unique_ptr<MockSellerWorklet> TakeSellerWorklet() {
+    return std::move(seller_worklet_);
+  }
+
+  void Flush() { receiver_.FlushForTesting(); }
+
+ private:
+  void MaybeQuitRunLoop() {
+    if (!waiting_on_seller_ && waiting_for_num_bidders_ == 0)
+      run_loop_->Quit();
+  }
+
+  // An interest group is uniquely identified by its owner's origin and name.
+  using InterestGroupId = std::pair<url::Origin, std::string>;
+
+  std::map<InterestGroupId, std::unique_ptr<MockBidderWorklet>>
+      bidder_worklets_;
+
+  std::unique_ptr<MockSellerWorklet> seller_worklet_;
+
+  std::unique_ptr<base::RunLoop> run_loop_;
+  bool waiting_on_seller_ = false;
+  int waiting_for_num_bidders_ = 0;
+
+  // Receiver is last so that destroying `this` while there's a pending callback
+  // over the pipe will not DCHECK.
+  mojo::Receiver<auction_worklet::mojom::AuctionWorkletService> receiver_;
+};
+
+class AuctionRunnerTest : public testing::Test, public AuctionRunner::Delegate {
+ protected:
+  // Output of the RunAuctionCallback passed to AuctionRunner::CreateAndStart().
+  struct Result {
+    GURL ad_url;
+    std::string ad_metadata;
+    url::Origin interest_group_owner;
+    std::string interest_group_name;
+    GURL bidder_report_url;
+    GURL seller_report_url;
+    std::vector<std::string> errors;
+  };
+
+  AuctionRunnerTest()
+      : auction_worklet_service_(
+            auction_worklet_service_remote_.BindNewPipeAndPassReceiver()) {
+    mojo::SetDefaultProcessErrorHandler(base::BindRepeating(
+        &AuctionRunnerTest::OnBadMessage, base::Unretained(this)));
+  }
+
+  ~AuctionRunnerTest() override {
+    // Any bad message should have been inspected and cleared before the end of
+    // the test.
+    EXPECT_EQ(std::string(), bad_message_);
+    mojo::SetDefaultProcessErrorHandler(base::NullCallback());
+  }
+
+  void OnBadMessage(const std::string& reason) {
+    // No test expects multiple bad messages at a time
+    EXPECT_EQ(std::string(), bad_message_);
+    // Empty bad messages aren't expected. This check allows an empty
+    // `bad_message_` field to mean no bad message, avoiding using an optional,
+    // which has less helpful output on EXPECT failures.
+    EXPECT_FALSE(reason.empty());
+
+    bad_message_ = reason;
+  }
+
+  // Gets and clear most recent bad Mojo message.
+  std::string TakeBadMessage() { return std::move(bad_message_); }
+
+  // Starts an auction without waiting for it to complete. Useful when using
+  // MockAuctionWorkletService.
+  void StartAuction(
+      const GURL& seller_decision_logic_url,
+      std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders,
+      const std::string& auction_signals_json,
+      auction_worklet::mojom::BrowserSignalsPtr browser_signals) {
+    auction_complete_ = false;
+
+    blink::mojom::AuctionAdConfigPtr auction_config =
+        blink::mojom::AuctionAdConfig::New();
+    auction_config->seller = url::Origin::Create(seller_decision_logic_url);
+    auction_config->decision_logic_url = seller_decision_logic_url;
+    auction_config->interest_group_buyers =
+        blink::mojom::InterestGroupBuyers::NewAllBuyers(
+            blink::mojom::AllBuyers::New());
+    auction_config->auction_signals = auction_signals_json;
+    auction_config->seller_signals = R"({"isSellerSignals": true})";
+
+    base::flat_map<url::Origin, std::string> per_buyer_signals;
+    per_buyer_signals[kBidder1] = R"({"signalsFor": ")" + kBidder1Name + "\"}";
+    per_buyer_signals[kBidder2] = R"({"signalsFor": ")" + kBidder2Name + "\"}";
+    auction_config->per_buyer_signals = std::move(per_buyer_signals);
+
+    auction_run_loop_ = std::make_unique<base::RunLoop>();
+    Result result;
+    auction_runner_ = AuctionRunner::CreateAndStart(
+        this, std::move(auction_config), std::move(bidders),
+        std::move(browser_signals), frame_origin_,
+        base::BindOnce(&AuctionRunnerTest::OnAuctionComplete,
+                       base::Unretained(this)));
+  }
+
+  Result RunAuctionAndWait(
+      const GURL& seller_decision_logic_url,
+      std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders,
+      const std::string& auction_signals_json,
+      auction_worklet::mojom::BrowserSignalsPtr browser_signals) {
+    StartAuction(seller_decision_logic_url, std::move(bidders),
+                 auction_signals_json, std::move(browser_signals));
+    auction_run_loop_->Run();
+    return result_;
+  }
+
+  void OnAuctionComplete(const GURL& ad_url,
+                         const std::string& ad_metadata,
+                         const url::Origin& interest_group_owner,
+                         const std::string& interest_group_name,
+                         const GURL& bidder_report_url,
+                         const GURL& seller_report_url,
+                         const std::vector<std::string>& errors) {
+    auction_complete_ = true;
+    result_.ad_url = ad_url;
+    result_.ad_metadata = ad_metadata;
+    result_.interest_group_owner = interest_group_owner;
+    result_.interest_group_name = interest_group_name;
+    result_.bidder_report_url = bidder_report_url;
+    result_.seller_report_url = seller_report_url;
+    result_.errors = errors;
+    auction_run_loop_->Quit();
+  }
+
+  auction_worklet::mojom::BiddingInterestGroupPtr MakeInterestGroup(
+      const url::Origin& owner,
+      const std::string& name,
+      const GURL& bidding_url,
+      const absl::optional<GURL>& trusted_bidding_signals_url,
+      const std::vector<std::string>& trusted_bidding_signals_keys,
+      const GURL& ad_url) {
+    std::vector<blink::mojom::InterestGroupAdPtr> ads;
+    // Give only kBidder1 an InterestGroupAd ad with non-empty metadata, to
+    // better test the `ad_metadata` output.
+    if (owner == kBidder1) {
+      ads.push_back(
+          blink::mojom::InterestGroupAd::New(ad_url, R"({"ads": true})"));
+    } else {
+      ads.push_back(blink::mojom::InterestGroupAd::New(ad_url, absl::nullopt));
+    }
+
+    std::vector<auction_worklet::mojom::PreviousWinPtr> previous_wins;
+    previous_wins.push_back(auction_worklet::mojom::PreviousWin::New(
+        base::Time::Now(), R"({"winner": 0})"));
+    previous_wins.push_back(auction_worklet::mojom::PreviousWin::New(
+        base::Time::Now(), R"({"winner": -1})"));
+    previous_wins.push_back(auction_worklet::mojom::PreviousWin::New(
+        base::Time::Now(), R"({"winner": -2})"));
+
+    return auction_worklet::mojom::BiddingInterestGroup::New(
+        blink::mojom::InterestGroup::New(
+            base::Time::Max(), owner, name, bidding_url,
+            GURL() /* update_url */, trusted_bidding_signals_url,
+            trusted_bidding_signals_keys, absl::nullopt, std::move(ads)),
+        auction_worklet::mojom::BiddingBrowserSignals::New(
+            3, 5, std::move(previous_wins)));
+  }
+
+  void StartStandardAuction() {
+    std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders;
+    bidders.push_back(MakeInterestGroup(kBidder1, kBidder1Name, kBidder1Url,
+                                        kTrustedSignalsUrl, {"k1", "k2"},
+                                        GURL("https://ad1.com")));
+    bidders.push_back(MakeInterestGroup(kBidder2, kBidder2Name, kBidder2Url,
+                                        kTrustedSignalsUrl, {"l1", "l2"},
+                                        GURL("https://ad2.com")));
+
+    StartAuction(kSellerUrl, std::move(bidders),
+                 R"({"isAuctionSignals": true})", /* auction_signals_json */
+                 auction_worklet::mojom::BrowserSignals::New(
+                     url::Origin::Create(GURL("https://publisher1.com")),
+                     url::Origin::Create(kSellerUrl)));
+  }
+
+  Result RunStandardAuction() {
+    StartStandardAuction();
+    auction_run_loop_->Run();
+    return result_;
+  }
+
+  // Starts the standard auction with the mock worklet service, and waits for
+  // the service to receive the worklet construction calls.
+  void StartStandardAuctionWithMockService() {
+    use_mock_service_ = true;
+    StartStandardAuction();
+    mock_worklet_service_->WaitForWorklets(2 /* num_bidders */);
+  }
+
+  // AuctionRunner::Delegate implementation:
+  network::mojom::URLLoaderFactory* GetFrameURLLoaderFactory() override {
+    return &url_loader_factory_;
+  }
+  network::mojom::URLLoaderFactory* GetTrustedURLLoaderFactory() override {
+    return &url_loader_factory_;
+  }
+  auction_worklet::mojom::AuctionWorkletService* GetWorkletService() override {
+    if (use_mock_service_) {
+      if (!mock_worklet_service_) {
+        mock_worklet_service_remote_.reset();
+        mock_worklet_service_ = std::make_unique<MockAuctionWorkletService>(
+            mock_worklet_service_remote_.BindNewPipeAndPassReceiver());
+      }
+      return mock_worklet_service_remote_.get();
+    }
+    return auction_worklet_service_remote_.get();
+  }
+
+  const url::Origin frame_origin_ =
+      url::Origin::Create(GURL("https://frame.origin.test"));
+  const GURL kSellerUrl{"https://adstuff.publisher1.com/auction.js"};
+  const GURL kBidder1Url{"https://adplatform.com/offers.js"};
+  const url::Origin kBidder1 =
+      url::Origin::Create(GURL("https://adplatform.com"));
+  const std::string kBidder1Name{"Ad Platform"};
+  const GURL kBidder2Url{"https://anotheradthing.com/bids.js"};
+  const url::Origin kBidder2 =
+      url::Origin::Create(GURL("https://anotheradthing.com"));
+  const std::string kBidder2Name{"Another Ad Thing"};
+
+  const GURL kTrustedSignalsUrl{"https://trustedsignaller.org/signals"};
+
+  base::test::TaskEnvironment task_environment_;
+
+  bool use_mock_service_ = false;
+
+  // RunLoop that's quit on auction completion.
+  std::unique_ptr<base::RunLoop> auction_run_loop_;
+  // True if the most recently started auction has completed.
+  bool auction_complete_ = false;
+  // Result of the most recent auction.
+  Result result_;
+
+  network::TestURLLoaderFactory url_loader_factory_;
+  mojo::Remote<auction_worklet::mojom::AuctionWorkletService>
+      mock_worklet_service_remote_;
+  std::unique_ptr<MockAuctionWorkletService> mock_worklet_service_;
+
+  mojo::Remote<auction_worklet::mojom::AuctionWorkletService>
+      auction_worklet_service_remote_;
+  auction_worklet::AuctionWorkletServiceImpl auction_worklet_service_;
+
+  std::unique_ptr<AuctionRunner> auction_runner_;
+  // This should be inspected using TakeBadMessage(), which also clears it.
+  std::string bad_message_;
+};
+
+// An auction with two successful bids.
+TEST_F(AuctionRunnerTest, Basic) {
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
+                    true /* has_signals */, "k1", "a"));
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder2Url,
+      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
+                    true /* has_signals */, "l2", "b"));
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         MakeAuctionScript());
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
+      R"({"k1":"a", "k2": "b", "extra": "c"})");
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
+      R"({"l1":"a", "l2": "b", "extra": "c"})");
+
+  Result res = RunStandardAuction();
+  EXPECT_EQ("https://ad2.com/", res.ad_url.spec());
+  EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", res.ad_metadata);
+  EXPECT_EQ("https://anotheradthing.com", res.interest_group_owner.Serialize());
+  EXPECT_EQ("Another Ad Thing", res.interest_group_name);
+  EXPECT_EQ("https://reporting.example.com/", res.seller_report_url.spec());
+  EXPECT_EQ("https://buyer-reporting.example.com/",
+            res.bidder_report_url.spec());
+  EXPECT_THAT(res.errors, testing::ElementsAre());
+}
+
+// An auction where one bid is successful, another's script 404s.
+TEST_F(AuctionRunnerTest, OneBidOne404) {
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
+                    true /* has_signals */, "k1", "a"));
+  url_loader_factory_.AddResponse(kBidder2Url.spec(), "", net::HTTP_NOT_FOUND);
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         MakeAuctionScript());
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
+      R"({"k1":"a", "k2": "b", "extra": "c"})");
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
+      R"({"l1":"a", "l2": "b", "extra": "c"})");
+
+  Result res = RunStandardAuction();
+  EXPECT_EQ("https://ad1.com/", res.ad_url.spec());
+  EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})",
+            res.ad_metadata);
+  EXPECT_EQ("https://adplatform.com", res.interest_group_owner.Serialize());
+  EXPECT_EQ("Ad Platform", res.interest_group_name);
+  EXPECT_EQ("https://reporting.example.com/", res.seller_report_url.spec());
+  EXPECT_EQ("https://buyer-reporting.example.com/",
+            res.bidder_report_url.spec());
+  EXPECT_THAT(
+      res.errors,
+      testing::ElementsAre("Failed to load https://anotheradthing.com/bids.js "
+                           "HTTP status = 404 Not Found."));
+}
+
+// An auction where one bid is successful, another's script does not provide a
+// bidding function.
+TEST_F(AuctionRunnerTest, OneBidOneNotMade) {
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
+                    true /* has_signals */, "k1", "a"));
+
+  // The auction script doesn't make any bids.
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url,
+                                         MakeAuctionScript());
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         MakeAuctionScript());
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
+      R"({"k1":"a", "k2": "b", "extra": "c"})");
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
+      R"({"l1":"a", "l2": "b", "extra": "c"})");
+
+  Result res = RunStandardAuction();
+  EXPECT_EQ("https://ad1.com/", res.ad_url.spec());
+  EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})",
+            res.ad_metadata);
+  EXPECT_EQ("https://adplatform.com", res.interest_group_owner.Serialize());
+  EXPECT_EQ("Ad Platform", res.interest_group_name);
+  EXPECT_EQ("https://reporting.example.com/", res.seller_report_url.spec());
+  EXPECT_EQ("https://buyer-reporting.example.com/",
+            res.bidder_report_url.spec());
+  EXPECT_THAT(res.errors,
+              testing::ElementsAre("https://anotheradthing.com/bids.js "
+                                   "`generateBid` is not a function."));
+}
+
+// An auction where no bidding scripts load successfully.
+TEST_F(AuctionRunnerTest, NoBids) {
+  url_loader_factory_.AddResponse(kBidder1Url.spec(), "", net::HTTP_NOT_FOUND);
+  url_loader_factory_.AddResponse(kBidder2Url.spec(), "", net::HTTP_NOT_FOUND);
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         MakeAuctionScript());
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
+      R"({"k1":"a", "k2":"b", "extra":"c"})");
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
+      R"({"l1":"a", "l2": "b", "extra":"c"})");
+
+  Result res = RunStandardAuction();
+  EXPECT_TRUE(res.ad_url.is_empty());
+  EXPECT_TRUE(res.ad_metadata.empty());
+  EXPECT_TRUE(res.interest_group_owner.opaque());
+  EXPECT_EQ("", res.interest_group_name);
+  EXPECT_TRUE(res.seller_report_url.is_empty());
+  EXPECT_TRUE(res.bidder_report_url.is_empty());
+  EXPECT_THAT(
+      res.errors,
+      testing::ElementsAre("Failed to load https://adplatform.com/offers.js "
+                           "HTTP status = 404 Not Found.",
+                           "Failed to load https://anotheradthing.com/bids.js "
+                           "HTTP status = 404 Not Found."));
+}
+
+// An auction where none of the bidding scripts has a valid bidding function.
+TEST_F(AuctionRunnerTest, NoBidMadeByScript) {
+  // MakeAuctionScript() is a valid script that doesn't have a bidding function.
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url,
+                                         MakeAuctionScript());
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url,
+                                         MakeAuctionScript());
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         MakeAuctionScript());
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
+      R"({"k1":"a", "k2":"b", "extra":"c"})");
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
+      R"({"l1":"a", "l2": "b", "extra":"c"})");
+
+  Result res = RunStandardAuction();
+  EXPECT_TRUE(res.ad_url.is_empty());
+  EXPECT_TRUE(res.ad_metadata.empty());
+  EXPECT_TRUE(res.interest_group_owner.opaque());
+  EXPECT_EQ("", res.interest_group_name);
+  EXPECT_TRUE(res.seller_report_url.is_empty());
+  EXPECT_TRUE(res.bidder_report_url.is_empty());
+  EXPECT_THAT(
+      res.errors,
+      testing::ElementsAre(
+          "https://adplatform.com/offers.js `generateBid` is not a function.",
+          "https://anotheradthing.com/bids.js `generateBid` is not a "
+          "function."));
+}
+
+// An auction where the seller script doesn't have a scoring function.
+TEST_F(AuctionRunnerTest, SellerRejectsAll) {
+  std::string bid_script1 =
+      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
+                    true /* has_signals */, "k1", "a");
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url,
+                                         bid_script1);
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder2Url,
+      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
+                    true /* has_signals */, "l2", "b"));
+
+  // No seller scoring function in a bid script.
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         bid_script1);
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
+      R"({"k1":"a", "k2":"b", "extra":"c"})");
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
+      R"({"l1":"a", "l2": "b", "extra":"c"})");
+
+  Result res = RunStandardAuction();
+  EXPECT_TRUE(res.ad_url.is_empty());
+  EXPECT_TRUE(res.ad_metadata.empty());
+  EXPECT_TRUE(res.interest_group_owner.opaque());
+  EXPECT_EQ("", res.interest_group_name);
+  EXPECT_TRUE(res.seller_report_url.is_empty());
+  EXPECT_TRUE(res.bidder_report_url.is_empty());
+  EXPECT_THAT(res.errors,
+              testing::ElementsAre("https://adstuff.publisher1.com/auction.js "
+                                   "`scoreAd` is not a function.",
+                                   "https://adstuff.publisher1.com/auction.js "
+                                   "`scoreAd` is not a function."));
+}
+
+// An auction where seller rejects one bid when scoring.
+TEST_F(AuctionRunnerTest, SellerRejectsOne) {
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
+                    true /* has_signals */, "k1", "a"));
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder2Url,
+      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
+                    true /* has_signals */, "l2", "b"));
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         MakeAuctionScriptReject2());
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
+      R"({"k1":"a", "k2": "b", "extra": "c"})");
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
+      R"({"l1":"a", "l2": "b", "extra": "c"})");
+
+  Result res = RunStandardAuction();
+  EXPECT_EQ("https://ad1.com/", res.ad_url.spec());
+  EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})",
+            res.ad_metadata);
+  EXPECT_EQ("https://adplatform.com", res.interest_group_owner.Serialize());
+  EXPECT_EQ("Ad Platform", res.interest_group_name);
+  EXPECT_EQ("https://reporting.example.com/", res.seller_report_url.spec());
+  EXPECT_EQ("https://buyer-reporting.example.com/",
+            res.bidder_report_url.spec());
+}
+
+// An auction where the seller script fails to load.
+TEST_F(AuctionRunnerTest, NoSellerScript) {
+  // Tests to make sure that if seller script fails the other fetches are
+  // cancelled, too.
+  url_loader_factory_.AddResponse(kSellerUrl.spec(), "", net::HTTP_NOT_FOUND);
+  Result res = RunStandardAuction();
+  EXPECT_TRUE(res.ad_url.is_empty());
+  EXPECT_TRUE(res.ad_metadata.empty());
+  EXPECT_TRUE(res.interest_group_owner.opaque());
+  EXPECT_EQ("", res.interest_group_name);
+  EXPECT_TRUE(res.seller_report_url.is_empty());
+  EXPECT_TRUE(res.bidder_report_url.is_empty());
+
+  EXPECT_EQ(0, url_loader_factory_.NumPending());
+  EXPECT_THAT(res.errors,
+              testing::ElementsAre(
+                  "Failed to load https://adstuff.publisher1.com/auction.js "
+                  "HTTP status = 404 Not Found."));
+}
+
+// An auction where bidders don't requested trusted bidding signals.
+TEST_F(AuctionRunnerTest, NoTrustedBiddingSignals) {
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
+                    false /* has_signals */, "k1", "a"));
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder2Url,
+      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
+                    false /* has_signals */, "l2", "b"));
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         MakeAuctionScript());
+
+  std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders;
+  bidders.push_back(MakeInterestGroup(kBidder1, kBidder1Name, kBidder1Url,
+                                      absl::nullopt, {"k1", "k2"},
+                                      GURL("https://ad1.com")));
+  bidders.push_back(MakeInterestGroup(kBidder2, kBidder2Name, kBidder2Url,
+                                      absl::nullopt, {"l1", "l2"},
+                                      GURL("https://ad2.com")));
+
+  Result res = RunAuctionAndWait(
+      kSellerUrl, std::move(bidders),
+      R"({"isAuctionSignals": true})", /* auction_signals_json */
+      auction_worklet::mojom::BrowserSignals::New(
+          url::Origin::Create(GURL("https://publisher1.com")),
+          url::Origin::Create(kSellerUrl)));
+
+  EXPECT_EQ("https://ad2.com/", res.ad_url.spec());
+  EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", res.ad_metadata);
+  EXPECT_EQ("https://anotheradthing.com", res.interest_group_owner.Serialize());
+  EXPECT_EQ("Another Ad Thing", res.interest_group_name);
+  EXPECT_EQ("https://reporting.example.com/", res.seller_report_url.spec());
+  EXPECT_EQ("https://buyer-reporting.example.com/",
+            res.bidder_report_url.spec());
+  EXPECT_THAT(res.errors, testing::ElementsAre());
+}
+
+// An auction where trusted bidding signals are requested, but the fetch 404s.
+TEST_F(AuctionRunnerTest, TrustedBiddingSignals404) {
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
+                    false /* has_signals */, "k1", "a"));
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder2Url,
+      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
+                    false /* has_signals */, "l2", "b"));
+  url_loader_factory_.AddResponse(
+      kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2", "",
+      net::HTTP_NOT_FOUND);
+  url_loader_factory_.AddResponse(
+      kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2", "",
+      net::HTTP_NOT_FOUND);
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         MakeAuctionScript());
+
+  Result res = RunStandardAuction();
+  EXPECT_EQ("https://ad2.com/", res.ad_url.spec());
+  EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", res.ad_metadata);
+  EXPECT_EQ("https://anotheradthing.com", res.interest_group_owner.Serialize());
+  EXPECT_EQ("Another Ad Thing", res.interest_group_name);
+  EXPECT_EQ("https://reporting.example.com/", res.seller_report_url.spec());
+  EXPECT_EQ("https://buyer-reporting.example.com/",
+            res.bidder_report_url.spec());
+  EXPECT_THAT(res.errors,
+              testing::ElementsAre("Failed to load "
+                                   "https://trustedsignaller.org/"
+                                   "signals?hostname=publisher1.com&keys=k1,k2 "
+                                   "HTTP status = 404 Not Found.",
+                                   "Failed to load "
+                                   "https://trustedsignaller.org/"
+                                   "signals?hostname=publisher1.com&keys=l1,l2 "
+                                   "HTTP status = 404 Not Found."));
+}
+
+// A successful auction where seller reporting worklet doesn't set a URL.
+TEST_F(AuctionRunnerTest, NoReportResultUrl) {
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
+                    true /* has_signals */, "k1", "a"));
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder2Url,
+      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
+                    true /* has_signals */, "l2", "b"));
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         MakeAuctionScriptNoReportUrl());
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
+      R"({"k1":"a", "k2": "b", "extra": "c"})");
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
+      R"({"l1":"a", "l2": "b", "extra": "c"})");
+
+  Result res = RunStandardAuction();
+  EXPECT_EQ("https://ad2.com/", res.ad_url.spec());
+  EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", res.ad_metadata);
+  EXPECT_EQ("https://anotheradthing.com", res.interest_group_owner.Serialize());
+  EXPECT_EQ("Another Ad Thing", res.interest_group_name);
+  EXPECT_TRUE(res.seller_report_url.is_empty());
+  EXPECT_EQ("https://buyer-reporting.example.com/",
+            res.bidder_report_url.spec());
+  EXPECT_THAT(res.errors, testing::ElementsAre());
+}
+
+// A successful auction where bidder reporting worklet doesn't set a URL.
+TEST_F(AuctionRunnerTest, NoReportWinUrl) {
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
+                    true /* has_signals */, "k1", "a") +
+          kReportWinNoUrl);
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder2Url,
+      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
+                    true /* has_signals */, "l2", "b") +
+          kReportWinNoUrl);
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         MakeAuctionScript());
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
+      R"({"k1":"a", "k2": "b", "extra": "c"})");
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
+      R"({"l1":"a", "l2": "b", "extra": "c"})");
+
+  Result res = RunStandardAuction();
+  EXPECT_EQ("https://ad2.com/", res.ad_url.spec());
+  EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", res.ad_metadata);
+  EXPECT_EQ("https://anotheradthing.com", res.interest_group_owner.Serialize());
+  EXPECT_EQ("Another Ad Thing", res.interest_group_name);
+  EXPECT_EQ("https://reporting.example.com/", res.seller_report_url.spec());
+  EXPECT_TRUE(res.bidder_report_url.is_empty());
+  EXPECT_THAT(res.errors, testing::ElementsAre());
+}
+
+// A successful auction where neither reporting worklets sets a URL.
+TEST_F(AuctionRunnerTest, NeitherReportUrl) {
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
+                    true /* has_signals */, "k1", "a") +
+          kReportWinNoUrl);
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder2Url,
+      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
+                    true /* has_signals */, "l2", "b") +
+          kReportWinNoUrl);
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         MakeAuctionScriptNoReportUrl());
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
+      R"({"k1":"a", "k2": "b", "extra": "c"})");
+  auction_worklet::AddJsonResponse(
+      &url_loader_factory_,
+      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
+      R"({"l1":"a", "l2": "b", "extra": "c"})");
+
+  Result res = RunStandardAuction();
+  EXPECT_EQ("https://ad2.com/", res.ad_url.spec());
+  EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", res.ad_metadata);
+  EXPECT_EQ("https://anotheradthing.com", res.interest_group_owner.Serialize());
+  EXPECT_EQ("Another Ad Thing", res.interest_group_name);
+  EXPECT_TRUE(res.seller_report_url.is_empty());
+  EXPECT_TRUE(res.bidder_report_url.is_empty());
+  EXPECT_THAT(res.errors, testing::ElementsAre());
+}
+
+TEST_F(AuctionRunnerTest, AllBiddersCrashBeforeBidding) {
+  for (bool seller_worklet_loads_first : {false, true}) {
+    SCOPED_TRACE(seller_worklet_loads_first);
+
+    StartStandardAuctionWithMockService();
+    auto seller_worklet = mock_worklet_service_->TakeSellerWorklet();
+    ASSERT_TRUE(seller_worklet);
+    auto bidder1_worklet =
+        mock_worklet_service_->TakeBidderWorklet(kBidder1, kBidder1Name);
+    ASSERT_TRUE(bidder1_worklet);
+    auto bidder2_worklet =
+        mock_worklet_service_->TakeBidderWorklet(kBidder2, kBidder2Name);
+    ASSERT_TRUE(bidder2_worklet);
+
+    if (seller_worklet_loads_first) {
+      seller_worklet->CompleteLoading();
+      mock_worklet_service_->Flush();
+    }
+    EXPECT_FALSE(auction_complete_);
+
+    // Have to keep the callbacks around, since the AuctionWorkletService is
+    // still live. Closing it here causes more issues than its worth (seller
+    // worklet can't complete loading if it hasn't already). In production,
+    // there would be multiple service processes in this case, with different
+    // pipes.
+    auto bidder1_callback = bidder1_worklet->TakeLoadCallback();
+    auto bidder2_callback = bidder2_worklet->TakeLoadCallback();
+    bidder1_worklet.reset();
+    bidder2_worklet.reset();
+
+    base::RunLoop().RunUntilIdle();
+    // The auction isn't failed until the seller worklet has completed loading.
+    if (!seller_worklet_loads_first)
+      seller_worklet->CompleteLoading();
+
+    auction_run_loop_->Run();
+
+    EXPECT_FALSE(result_.ad_url.is_valid());
+    EXPECT_TRUE(result_.ad_metadata.empty());
+    EXPECT_TRUE(result_.ad_url.is_empty());
+    EXPECT_TRUE(result_.interest_group_owner.opaque());
+    EXPECT_EQ("", result_.interest_group_name);
+    EXPECT_TRUE(result_.seller_report_url.is_empty());
+    EXPECT_TRUE(result_.bidder_report_url.is_empty());
+    EXPECT_THAT(
+        result_.errors,
+        testing::UnorderedElementsAre(
+            base::StringPrintf("%s crashed while trying to run generateBid().",
+                               kBidder1Url.spec().c_str()),
+            base::StringPrintf("%s crashed while trying to run generateBid().",
+                               kBidder2Url.spec().c_str())));
+
+    // Reset the service, so callbacks can be destroyed without DCHECKing.
+    mock_worklet_service_.reset();
+  }
+}
+
+// Test the case a single bidder worklet crashes before bidding. The auction
+// should continue, without that bidder's bid.
+TEST_F(AuctionRunnerTest, BidderCrashBeforeBidding) {
+  for (bool other_bidder_finishes_first : {false, true}) {
+    SCOPED_TRACE(other_bidder_finishes_first);
+    for (bool seller_worklet_loads_first : {false, true}) {
+      SCOPED_TRACE(seller_worklet_loads_first);
+      StartStandardAuctionWithMockService();
+      auto seller_worklet = mock_worklet_service_->TakeSellerWorklet();
+      ASSERT_TRUE(seller_worklet);
+      auto bidder1_worklet =
+          mock_worklet_service_->TakeBidderWorklet(kBidder1, kBidder1Name);
+      ASSERT_TRUE(bidder1_worklet);
+      auto bidder2_worklet =
+          mock_worklet_service_->TakeBidderWorklet(kBidder2, kBidder2Name);
+      ASSERT_TRUE(bidder2_worklet);
+
+      ASSERT_FALSE(auction_complete_);
+      if (seller_worklet_loads_first)
+        seller_worklet->CompleteLoading();
+      if (other_bidder_finishes_first) {
+        bidder2_worklet->CompleteLoadingAndBid(7 /* bid */,
+                                               GURL("https://ad2.com/"));
+      }
+      mock_worklet_service_->Flush();
+
+      ASSERT_FALSE(auction_complete_);
+
+      // Close Bidder1's pipe, keeping its callback alive to avoid a DCHECK.
+      auto bidder1_callback = bidder1_worklet->TakeLoadCallback();
+      bidder1_worklet.reset();
+      // Can't flush the closed pipe without reaching into AuctionRunner, so use
+      // RunUntilIdle() instead.
+      base::RunLoop().RunUntilIdle();
+
+      if (!seller_worklet_loads_first)
+        seller_worklet->CompleteLoading();
+      if (!other_bidder_finishes_first) {
+        bidder2_worklet->CompleteLoadingAndBid(7 /* bid */,
+                                               GURL("https://ad2.com/"));
+      }
+      mock_worklet_service_->Flush();
+
+      // The auction should be scored without waiting on the crashed kBidder1.
+      auto score_ad_params = seller_worklet->WaitForScoreAd();
+      EXPECT_EQ(kBidder2, score_ad_params->interest_group_owner);
+      EXPECT_EQ(7, score_ad_params->bid);
+      seller_worklet->InvokeScoreAdCallback(11);
+
+      // Finish the auction.
+      seller_worklet->WaitForReportResult();
+      seller_worklet->InvokeReportResultCallback();
+      bidder2_worklet->WaitForReportWin();
+      bidder2_worklet->InvokeReportWinCallback();
+
+      // Bidder2 won, Bidder1 crashed.
+      auction_run_loop_->Run();
+      EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_url);
+      EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", result_.ad_metadata);
+      EXPECT_EQ(kBidder2, result_.interest_group_owner);
+      EXPECT_EQ(kBidder2Name, result_.interest_group_name);
+      EXPECT_TRUE(result_.seller_report_url.is_empty());
+      EXPECT_TRUE(result_.bidder_report_url.is_empty());
+      EXPECT_THAT(result_.errors,
+                  testing::ElementsAre(base::StringPrintf(
+                      "%s crashed while trying to run generateBid().",
+                      kBidder1Url.spec().c_str())));
+
+      // Reset the service, so `bidder1_callback` can be destroyed without
+      // DCHECKing.
+      mock_worklet_service_.reset();
+    }
+  }
+}
+
+// If a losing bidder crashes while scoring, the auction should succeed.
+TEST_F(AuctionRunnerTest, LosingBidderCrashWhileScoring) {
+  StartStandardAuctionWithMockService();
+
+  auto seller_worklet = mock_worklet_service_->TakeSellerWorklet();
+  ASSERT_TRUE(seller_worklet);
+  auto bidder1_worklet =
+      mock_worklet_service_->TakeBidderWorklet(kBidder1, kBidder1Name);
+  ASSERT_TRUE(bidder1_worklet);
+  auto bidder2_worklet =
+      mock_worklet_service_->TakeBidderWorklet(kBidder2, kBidder2Name);
+  ASSERT_TRUE(bidder2_worklet);
+
+  seller_worklet->CompleteLoading();
+  bidder1_worklet->CompleteLoadingAndBid(5 /* bid */, GURL("https://ad1.com/"));
+  bidder2_worklet->CompleteLoadingAndBid(7 /* bid */, GURL("https://ad2.com/"));
+
+  // Bidder1 crashes while the seller scores its bid.
+  auto score_ad_params = seller_worklet->WaitForScoreAd();
+  bidder1_worklet.reset();
+  EXPECT_EQ(kBidder1, score_ad_params->interest_group_owner);
+  EXPECT_EQ(5, score_ad_params->bid);
+  seller_worklet->InvokeScoreAdCallback(10 /* score */);
+
+  // Score Bidder2's bid.
+  score_ad_params = seller_worklet->WaitForScoreAd();
+  EXPECT_EQ(kBidder2, score_ad_params->interest_group_owner);
+  EXPECT_EQ(7, score_ad_params->bid);
+  seller_worklet->InvokeScoreAdCallback(11 /* score */);
+
+  // Finish the auction.
+  seller_worklet->WaitForReportResult();
+  seller_worklet->InvokeReportResultCallback();
+  bidder2_worklet->WaitForReportWin();
+  bidder2_worklet->InvokeReportWinCallback();
+  auction_run_loop_->Run();
+
+  // Bidder2 won.
+  EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_url);
+  EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", result_.ad_metadata);
+  EXPECT_EQ(kBidder2, result_.interest_group_owner);
+  EXPECT_EQ(kBidder2Name, result_.interest_group_name);
+  EXPECT_TRUE(result_.seller_report_url.is_empty());
+  EXPECT_TRUE(result_.bidder_report_url.is_empty());
+  // Since Bidder1 crashed after bidding, don't report anything.
+  EXPECT_THAT(result_.errors, testing::ElementsAre());
+}
+
+// If the winning bidder crashes while scoring, the auction should fail.
+TEST_F(AuctionRunnerTest, WinningBidderCrashWhileScoring) {
+  StartStandardAuctionWithMockService();
+
+  auto seller_worklet = mock_worklet_service_->TakeSellerWorklet();
+  ASSERT_TRUE(seller_worklet);
+  auto bidder1_worklet =
+      mock_worklet_service_->TakeBidderWorklet(kBidder1, kBidder1Name);
+  ASSERT_TRUE(bidder1_worklet);
+  auto bidder2_worklet =
+      mock_worklet_service_->TakeBidderWorklet(kBidder2, kBidder2Name);
+  ASSERT_TRUE(bidder2_worklet);
+
+  seller_worklet->CompleteLoading();
+  bidder1_worklet->CompleteLoadingAndBid(7 /* bid */, GURL("https://ad1.com/"));
+  bidder2_worklet->CompleteLoadingAndBid(5 /* bid */, GURL("https://ad2.com/"));
+
+  // Bidder1 crashes while scoring its bid.
+  auto score_ad_params = seller_worklet->WaitForScoreAd();
+  bidder1_worklet.reset();
+  EXPECT_EQ(kBidder1, score_ad_params->interest_group_owner);
+  EXPECT_EQ(7, score_ad_params->bid);
+  seller_worklet->InvokeScoreAdCallback(11 /* score */);
+
+  // Score Bidder2's bid.
+  score_ad_params = seller_worklet->WaitForScoreAd();
+  EXPECT_EQ(kBidder2, score_ad_params->interest_group_owner);
+  EXPECT_EQ(5, score_ad_params->bid);
+  seller_worklet->InvokeScoreAdCallback(10 /* score */);
+
+  // Finish the auction.
+  seller_worklet->WaitForReportResult();
+  seller_worklet->InvokeReportResultCallback();
+  // AuctionRunner discovered Bidder1 crashed before calling its ReportWin
+  // method.
+  auction_run_loop_->Run();
+
+  // No bidder won, Bidder1 crashed.
+  EXPECT_TRUE(result_.ad_url.is_empty());
+  EXPECT_TRUE(result_.ad_metadata.empty());
+  EXPECT_TRUE(result_.interest_group_owner.opaque());
+  EXPECT_EQ("", result_.interest_group_name);
+  EXPECT_TRUE(result_.seller_report_url.is_empty());
+  EXPECT_TRUE(result_.bidder_report_url.is_empty());
+  EXPECT_THAT(result_.errors,
+              testing::ElementsAre(base::StringPrintf(
+                  "%s crashed while idle.", kBidder1Url.spec().c_str())));
+}
+
+// If the winning bidder crashes while coming up with the reporting URL, the
+// auction should fail.
+TEST_F(AuctionRunnerTest, WinningBidderCrashWhileReporting) {
+  StartStandardAuctionWithMockService();
+
+  auto seller_worklet = mock_worklet_service_->TakeSellerWorklet();
+  ASSERT_TRUE(seller_worklet);
+  auto bidder1_worklet =
+      mock_worklet_service_->TakeBidderWorklet(kBidder1, kBidder1Name);
+  ASSERT_TRUE(bidder1_worklet);
+  auto bidder2_worklet =
+      mock_worklet_service_->TakeBidderWorklet(kBidder2, kBidder2Name);
+  ASSERT_TRUE(bidder2_worklet);
+
+  seller_worklet->CompleteLoading();
+  bidder1_worklet->CompleteLoadingAndBid(7 /* bid */, GURL("https://ad1.com/"));
+  bidder2_worklet->CompleteLoadingAndBid(5 /* bid */, GURL("https://ad2.com/"));
+
+  // Bidder1 crashes while scoring its bid.
+  auto score_ad_params = seller_worklet->WaitForScoreAd();
+  EXPECT_EQ(kBidder1, score_ad_params->interest_group_owner);
+  EXPECT_EQ(7, score_ad_params->bid);
+  seller_worklet->InvokeScoreAdCallback(11 /* score */);
+
+  // Score Bidder2's bid.
+  score_ad_params = seller_worklet->WaitForScoreAd();
+  EXPECT_EQ(kBidder2, score_ad_params->interest_group_owner);
+  EXPECT_EQ(5, score_ad_params->bid);
+  seller_worklet->InvokeScoreAdCallback(10 /* score */);
+
+  seller_worklet->WaitForReportResult();
+  seller_worklet->InvokeReportResultCallback();
+  bidder1_worklet->WaitForReportWin();
+  bidder1_worklet.reset();
+  auction_run_loop_->Run();
+
+  // No bidder won, Bidder1 crashed.
+  EXPECT_TRUE(result_.ad_url.is_empty());
+  EXPECT_TRUE(result_.ad_metadata.empty());
+  EXPECT_TRUE(result_.interest_group_owner.opaque());
+  EXPECT_EQ("", result_.interest_group_name);
+  EXPECT_TRUE(result_.seller_report_url.is_empty());
+  EXPECT_TRUE(result_.bidder_report_url.is_empty());
+  EXPECT_THAT(result_.errors, testing::ElementsAre(base::StringPrintf(
+                                  "%s crashed while trying to run reportWin().",
+                                  kBidder1Url.spec().c_str())));
+}
+
+// If the seller crashes at any point in the auction, the auction fails.
+TEST_F(AuctionRunnerTest, SellerCrash) {
+  enum class CrashPhase {
+    kLoad,
+    kLoadAfterBiddersLoaded,
+    kScoreBid,
+    kReportResult,
+  };
+  for (CrashPhase crash_phase :
+       {CrashPhase::kLoad, CrashPhase::kLoadAfterBiddersLoaded,
+        CrashPhase::kScoreBid, CrashPhase::kReportResult}) {
+    SCOPED_TRACE(static_cast<int>(crash_phase));
+
+    StartStandardAuctionWithMockService();
+
+    auto seller_worklet = mock_worklet_service_->TakeSellerWorklet();
+    ASSERT_TRUE(seller_worklet);
+    auto bidder1_worklet =
+        mock_worklet_service_->TakeBidderWorklet(kBidder1, kBidder1Name);
+    ASSERT_TRUE(bidder1_worklet);
+    auto bidder2_worklet =
+        mock_worklet_service_->TakeBidderWorklet(kBidder2, kBidder2Name);
+    ASSERT_TRUE(bidder2_worklet);
+
+    // While loop to allow breaking when the crash stage is reached.
+    while (true) {
+      if (crash_phase == CrashPhase::kLoad) {
+        // Close the service pipe to avoid a DCHECK when deleting the seller's
+        // load callback.
+        mock_worklet_service_.reset();
+        seller_worklet.reset();
+        break;
+      }
+
+      bidder1_worklet->CompleteLoadingAndBid(5 /* bid */,
+                                             GURL("https://ad1.com/"));
+      bidder2_worklet->CompleteLoadingAndBid(7 /* bid */,
+                                             GURL("https://ad2.com/"));
+      // Wait for bids to be received.
+      base::RunLoop().RunUntilIdle();
+
+      if (crash_phase == CrashPhase::kLoadAfterBiddersLoaded) {
+        // Close the service pipe to avoid a DCHECK when deleting the seller's
+        // load callback.
+        mock_worklet_service_.reset();
+        seller_worklet.reset();
+        break;
+      }
+
+      seller_worklet->CompleteLoading();
+
+      // Wait for Bidder1's bid.
+      auto score_ad_params = seller_worklet->WaitForScoreAd();
+      if (crash_phase == CrashPhase::kScoreBid) {
+        seller_worklet.reset();
+        break;
+      }
+      // Score Bidder1's bid.
+      bidder1_worklet.reset();
+      EXPECT_EQ(kBidder1, score_ad_params->interest_group_owner);
+      EXPECT_EQ(5, score_ad_params->bid);
+      seller_worklet->InvokeScoreAdCallback(10 /* score */);
+
+      // Score Bidder2's bid.
+      score_ad_params = seller_worklet->WaitForScoreAd();
+      EXPECT_EQ(kBidder2, score_ad_params->interest_group_owner);
+      EXPECT_EQ(7, score_ad_params->bid);
+      seller_worklet->InvokeScoreAdCallback(11 /* score */);
+
+      seller_worklet->WaitForReportResult();
+      DCHECK_EQ(CrashPhase::kReportResult, crash_phase);
+      seller_worklet.reset();
+      break;
+    }
+
+    // Wait for auction to complete.
+    auction_run_loop_->Run();
+
+    // No bidder won, seller crashed.
+    EXPECT_TRUE(result_.ad_url.is_empty());
+    EXPECT_TRUE(result_.ad_metadata.empty());
+    EXPECT_TRUE(result_.interest_group_owner.opaque());
+    EXPECT_EQ("", result_.interest_group_name);
+    EXPECT_TRUE(result_.seller_report_url.is_empty());
+    EXPECT_TRUE(result_.bidder_report_url.is_empty());
+    EXPECT_THAT(result_.errors, testing::ElementsAre(base::StringPrintf(
+                                    "%s crashed.", kSellerUrl.spec().c_str())));
+  }
+}
+
+// Test cases where a bad bid is received over Mojo. Bad bids should be rejected
+// in the Mojo process, so these are treated as security errors.
+TEST_F(AuctionRunnerTest, BadBid) {
+  const struct TestCase {
+    const char* expected_error_message;
+    double bid;
+    GURL render_url;
+    base::TimeDelta duration;
+  } kTestCases[] = {
+      // Bids that aren't positive integers.
+      {
+          "Invalid bid value",
+          -10,
+          GURL("https://ad1.com"),
+          base::TimeDelta(),
+      },
+      {
+          "Invalid bid value",
+          0,
+          GURL("https://ad1.com"),
+          base::TimeDelta(),
+      },
+      {
+          "Invalid bid value",
+          std::numeric_limits<double>::infinity(),
+          GURL("https://ad1.com"),
+          base::TimeDelta(),
+      },
+      {
+          "Invalid bid value",
+          std::numeric_limits<double>::quiet_NaN(),
+          GURL("https://ad1.com"),
+          base::TimeDelta(),
+      },
+
+      // Invalid URL.
+      {
+          "Invalid bid render URL",
+          1,
+          GURL(":"),
+          base::TimeDelta(),
+      },
+
+      // Non-HTTPS URLs.
+      {
+          "Invalid bid render URL",
+          1,
+          GURL("data:,foo"),
+          base::TimeDelta(),
+      },
+      {
+          "Invalid bid render URL",
+          1,
+          GURL("http://ad1.com"),
+          base::TimeDelta(),
+      },
+
+      // HTTPS URL that's not in the list of allowed renderUrls.
+      {
+          "Bid render URL must be an ad URL",
+          1,
+          GURL("https://ad2.com"),
+          base::TimeDelta(),
+      },
+
+      // Negative time.
+      {
+          "Invalid bid duration",
+          1,
+          GURL("https://ad2.com"),
+          base::TimeDelta::FromMilliseconds(-1),
+      },
+  };
+
+  for (const auto& test_case : kTestCases) {
+    StartStandardAuctionWithMockService();
+
+    auto seller_worklet = mock_worklet_service_->TakeSellerWorklet();
+    ASSERT_TRUE(seller_worklet);
+    auto bidder1_worklet =
+        mock_worklet_service_->TakeBidderWorklet(kBidder1, kBidder1Name);
+    ASSERT_TRUE(bidder1_worklet);
+    auto bidder2_worklet =
+        mock_worklet_service_->TakeBidderWorklet(kBidder2, kBidder2Name);
+    ASSERT_TRUE(bidder2_worklet);
+
+    seller_worklet->CompleteLoading();
+    bidder1_worklet->CompleteLoadingAndBid(test_case.bid, test_case.render_url,
+                                           test_case.duration);
+    // Bidder 2 doesn't bid..
+    bidder2_worklet->CompleteLoadingWithoutBid();
+
+    // Since there's no acceptable bid, the seller worklet is never asked to
+    // score a bid.
+    auction_run_loop_->Run();
+
+    EXPECT_EQ(test_case.expected_error_message, TakeBadMessage());
+
+    // No bidder won.
+    EXPECT_TRUE(result_.ad_url.is_empty());
+    EXPECT_TRUE(result_.ad_metadata.empty());
+    EXPECT_TRUE(result_.interest_group_owner.opaque());
+    EXPECT_EQ("", result_.interest_group_name);
+    EXPECT_TRUE(result_.seller_report_url.is_empty());
+    EXPECT_TRUE(result_.bidder_report_url.is_empty());
+    EXPECT_THAT(result_.errors, testing::ElementsAre());
+  }
+}
+
+// Test cases where bad a report URL is received over Mojo from the bidder
+// worklet. Bad report URLs should be rejected in the Mojo process, so these are
+// treated as security errors.
+TEST_F(AuctionRunnerTest, BadSellerReportUrl) {
+  StartStandardAuctionWithMockService();
+
+  auto seller_worklet = mock_worklet_service_->TakeSellerWorklet();
+  ASSERT_TRUE(seller_worklet);
+  auto bidder1_worklet =
+      mock_worklet_service_->TakeBidderWorklet(kBidder1, kBidder1Name);
+  ASSERT_TRUE(bidder1_worklet);
+  auto bidder2_worklet =
+      mock_worklet_service_->TakeBidderWorklet(kBidder2, kBidder2Name);
+  ASSERT_TRUE(bidder2_worklet);
+
+  seller_worklet->CompleteLoading();
+  // Only Bidder1 bids, to keep things simple.
+  bidder1_worklet->CompleteLoadingAndBid(5 /* bid */, GURL("https://ad1.com/"));
+  bidder2_worklet->CompleteLoadingWithoutBid();
+
+  auto score_ad_params = seller_worklet->WaitForScoreAd();
+  EXPECT_EQ(kBidder1, score_ad_params->interest_group_owner);
+  EXPECT_EQ(5, score_ad_params->bid);
+  seller_worklet->InvokeScoreAdCallback(10 /* score */);
+
+  // Bidder1 never gets to report anything, since the seller providing a bad
+  // report URL aborts the auction.
+  seller_worklet->WaitForReportResult();
+  seller_worklet->InvokeReportResultCallback(GURL("http://not.https.test/"));
+  auction_run_loop_->Run();
+
+  EXPECT_EQ("Invalid seller report URL", TakeBadMessage());
+
+  // No bidder won.
+  EXPECT_TRUE(result_.ad_url.is_empty());
+  EXPECT_TRUE(result_.ad_metadata.empty());
+  EXPECT_TRUE(result_.interest_group_owner.opaque());
+  EXPECT_EQ("", result_.interest_group_name);
+  EXPECT_TRUE(result_.seller_report_url.is_empty());
+  EXPECT_TRUE(result_.bidder_report_url.is_empty());
+  EXPECT_THAT(result_.errors, testing::ElementsAre());
+}
+
+// Test cases where bad a report URL is received over Mojo from the seller
+// worklet. Bad report URLs should be rejected in the Mojo process, so these are
+// treated as security errors.
+TEST_F(AuctionRunnerTest, BadBidderReportUrl) {
+  StartStandardAuctionWithMockService();
+
+  auto seller_worklet = mock_worklet_service_->TakeSellerWorklet();
+  ASSERT_TRUE(seller_worklet);
+  auto bidder1_worklet =
+      mock_worklet_service_->TakeBidderWorklet(kBidder1, kBidder1Name);
+  ASSERT_TRUE(bidder1_worklet);
+  auto bidder2_worklet =
+      mock_worklet_service_->TakeBidderWorklet(kBidder2, kBidder2Name);
+  ASSERT_TRUE(bidder2_worklet);
+
+  seller_worklet->CompleteLoading();
+  // Only Bidder1 bids, to keep things simple.
+  bidder1_worklet->CompleteLoadingAndBid(5 /* bid */, GURL("https://ad1.com/"));
+  bidder2_worklet->CompleteLoadingWithoutBid();
+
+  auto score_ad_params = seller_worklet->WaitForScoreAd();
+  EXPECT_EQ(kBidder1, score_ad_params->interest_group_owner);
+  EXPECT_EQ(5, score_ad_params->bid);
+  seller_worklet->InvokeScoreAdCallback(10 /* score */);
+
+  seller_worklet->WaitForReportResult();
+  seller_worklet->InvokeReportResultCallback(
+      GURL("https://valid.url.that.is.thrown.out.test/"));
+  bidder1_worklet->WaitForReportWin();
+  bidder1_worklet->InvokeReportWinCallback(GURL("http://not.https.test/"));
+  auction_run_loop_->Run();
+
+  EXPECT_EQ("Invalid bidder report URL", TakeBadMessage());
+
+  // No bidder won.
+  EXPECT_TRUE(result_.ad_url.is_empty());
+  EXPECT_TRUE(result_.ad_metadata.empty());
+  EXPECT_TRUE(result_.interest_group_owner.opaque());
+  EXPECT_EQ("", result_.interest_group_name);
+  EXPECT_TRUE(result_.seller_report_url.is_empty());
+  EXPECT_TRUE(result_.bidder_report_url.is_empty());
+  EXPECT_THAT(result_.errors, testing::ElementsAre());
+}
+
+// Make sure that requesting unexpected URLs is blocked.
+TEST_F(AuctionRunnerTest, UrlRequestProtection) {
+  StartStandardAuctionWithMockService();
+
+  auto seller_worklet = mock_worklet_service_->TakeSellerWorklet();
+  ASSERT_TRUE(seller_worklet);
+  auto bidder1_worklet =
+      mock_worklet_service_->TakeBidderWorklet(kBidder1, kBidder1Name);
+  ASSERT_TRUE(bidder1_worklet);
+  auto bidder2_worklet =
+      mock_worklet_service_->TakeBidderWorklet(kBidder2, kBidder2Name);
+  ASSERT_TRUE(bidder2_worklet);
+
+  // It should be possible to request the seller URL from the seller's
+  // URLLoaderFactory.
+  network::ResourceRequest request;
+  request.url = kSellerUrl;
+  request.headers.SetHeader(net::HttpRequestHeaders::kAccept,
+                            "application/javascript");
+  mojo::PendingRemote<network::mojom::URLLoader> receiver;
+  mojo::PendingReceiver<network::mojom::URLLoaderClient> client;
+  seller_worklet->url_loader_factory()->CreateLoaderAndStart(
+      receiver.InitWithNewPipeAndPassReceiver(), 0 /* request_id_ */,
+      0 /* options */, request, client.InitWithNewPipeAndPassRemote(),
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+  seller_worklet->url_loader_factory().FlushForTesting();
+  EXPECT_TRUE(seller_worklet->url_loader_factory().is_connected());
+  ASSERT_EQ(1u, url_loader_factory_.pending_requests()->size());
+  EXPECT_EQ(kSellerUrl,
+            (*url_loader_factory_.pending_requests())[0].request.url);
+  receiver.reset();
+  client.reset();
+
+  // A bidder's URLLoaderFactory should reject the seller URL, closing the Mojo
+  // pipe.
+  bidder1_worklet->url_loader_factory()->CreateLoaderAndStart(
+      receiver.InitWithNewPipeAndPassReceiver(), 0 /* request_id_ */,
+      0 /* options */, request, client.InitWithNewPipeAndPassRemote(),
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+  bidder1_worklet->url_loader_factory().FlushForTesting();
+  EXPECT_FALSE(bidder1_worklet->url_loader_factory().is_connected());
+  EXPECT_EQ(1u, url_loader_factory_.pending_requests()->size());
+  EXPECT_EQ("Unexpected request", TakeBadMessage());
+  receiver.reset();
+  client.reset();
+
+  // A bidder's URLLoaderFactory should allow the bidder's URL to be requested.
+  request.url = kBidder2Url;
+  bidder2_worklet->url_loader_factory()->CreateLoaderAndStart(
+      receiver.InitWithNewPipeAndPassReceiver(), 0 /* request_id_ */,
+      0 /* options */, request, client.InitWithNewPipeAndPassRemote(),
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+  bidder2_worklet->url_loader_factory().FlushForTesting();
+  EXPECT_TRUE(bidder2_worklet->url_loader_factory().is_connected());
+  ASSERT_EQ(2u, url_loader_factory_.pending_requests()->size());
+  EXPECT_EQ(kBidder2Url,
+            (*url_loader_factory_.pending_requests())[1].request.url);
+  receiver.reset();
+  client.reset();
+
+  // The seller's URLLoaderFactory should reject a bidder worklet's URL, closing
+  // the Mojo pipe.
+  seller_worklet->url_loader_factory()->CreateLoaderAndStart(
+      receiver.InitWithNewPipeAndPassReceiver(), 0 /* request_id_ */,
+      0 /* options */, request, client.InitWithNewPipeAndPassRemote(),
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+  seller_worklet->url_loader_factory().FlushForTesting();
+  EXPECT_FALSE(seller_worklet->url_loader_factory().is_connected());
+  EXPECT_EQ(2u, url_loader_factory_.pending_requests()->size());
+  EXPECT_EQ("Unexpected request", TakeBadMessage());
+  receiver.reset();
+  client.reset();
+
+  // A bidder's URLLoaderFactory should also reject the URL from another bidder.
+  request.url = kBidder1Url;
+  bidder2_worklet->url_loader_factory()->CreateLoaderAndStart(
+      receiver.InitWithNewPipeAndPassReceiver(), 0 /* request_id_ */,
+      0 /* options */, request, client.InitWithNewPipeAndPassRemote(),
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+  bidder2_worklet->url_loader_factory().FlushForTesting();
+  EXPECT_FALSE(bidder2_worklet->url_loader_factory().is_connected());
+  ASSERT_EQ(2u, url_loader_factory_.pending_requests()->size());
+  EXPECT_EQ("Unexpected request", TakeBadMessage());
+
+  // Reset the service, so the uninvoke worklet loading callbacks can be
+  // destroyed without DCHECKing.
+  mock_worklet_service_.reset();
+}
+
+}  // namespace
+}  // namespace content
diff --git a/content/browser/interest_group/auction_url_loader_factory_proxy.cc b/content/browser/interest_group/auction_url_loader_factory_proxy.cc
index 0dd1150e..0b5cea78 100644
--- a/content/browser/interest_group/auction_url_loader_factory_proxy.cc
+++ b/content/browser/interest_group/auction_url_loader_factory_proxy.cc
@@ -14,7 +14,6 @@
 #include "base/strings/string_piece.h"
 #include "base/strings/string_util.h"
 #include "content/public/browser/global_request_id.h"
-#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
 #include "mojo/public/cpp/bindings/message.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
@@ -26,7 +25,6 @@
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/mojom/url_loader_factory.mojom.h"
-#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
 #include "url/gurl.h"
 #include "url/origin.h"
 
@@ -34,33 +32,17 @@
 
 AuctionURLLoaderFactoryProxy::AuctionURLLoaderFactoryProxy(
     mojo::PendingReceiver<network::mojom::URLLoaderFactory> pending_receiver,
-    GetUrlLoaderFactoryCallback get_publisher_frame_url_loader_factory,
-    GetUrlLoaderFactoryCallback get_trusted_url_loader_factory,
+    GetUrlLoaderFactoryCallback get_url_loader_factory,
     const url::Origin& frame_origin,
-    const blink::mojom::AuctionAdConfig& auction_config,
-    const std::vector<auction_worklet::mojom::BiddingInterestGroupPtr>& bidders)
+    bool use_cors,
+    const GURL& script_url,
+    const absl::optional<GURL>& trusted_signals_url)
     : receiver_(this, std::move(pending_receiver)),
-      get_publisher_frame_url_loader_factory_(
-          std::move(get_publisher_frame_url_loader_factory)),
-      get_trusted_url_loader_factory_(
-          std::move(get_trusted_url_loader_factory)),
+      get_url_loader_factory_(std::move(get_url_loader_factory)),
       frame_origin_(frame_origin),
-      expected_query_prefix_(
-          "hostname=" + net::EscapeQueryParamValue(frame_origin.host(), true) +
-          "&keys=") {
-  decision_logic_url_ = auction_config.decision_logic_url;
-  for (const auto& bidder : bidders) {
-    if (bidder->group->bidding_url)
-      bidding_urls_.insert(*bidder->group->bidding_url);
-    if (bidder->group->trusted_bidding_signals_url) {
-      // Base trusted bidding signals URLs can't have query strings, since
-      // running an auction will create URLs by adding query strings to them.
-      DCHECK(!bidder->group->trusted_bidding_signals_url->has_query());
-
-      realtime_data_urls_.insert(*bidder->group->trusted_bidding_signals_url);
-    }
-  }
-}
+      use_cors_(use_cors),
+      script_url_(script_url),
+      trusted_signals_url_(trusted_signals_url) {}
 
 AuctionURLLoaderFactoryProxy::~AuctionURLLoaderFactoryProxy() = default;
 
@@ -79,56 +61,18 @@
     return;
   }
 
-  // True if the more restricted publisher RenderFrameHost's URLLoader should be
-  // used to load a resource. False if the global factory should be used
-  // instead, setting the ResourceRequest::TrustedParams field to use the
-  // correct network shard.
-  bool use_publisher_frame_loader = true;
+  bool is_request_allowed = false;
 
-  if (accept_header == "application/javascript") {
-    // Only script_urls may be requested with the Javascript Accept header.
-    if (url_request.url == decision_logic_url_) {
-      // Nothing more to do.
-    } else if (bidding_urls_.find(url_request.url) != bidding_urls_.end()) {
-      // This is safe, because `bidding_urls_` can only be registered by
-      // calling `joinAdInterestGroup` from the URL's origin.
-      use_publisher_frame_loader = false;
-    } else {
-      receiver_.ReportBadMessage("Unexpected Javascript request url");
-      return;
-    }
-  } else if (accept_header == "application/json") {
-    GURL::Replacements replacements;
-    replacements.ClearQuery();
-    GURL url_without_query = url_request.url.ReplaceComponents(replacements);
-    // Only `realtime_data_urls_` may be requested with the JSON Accept header.
-    if (realtime_data_urls_.find(url_without_query) ==
-        realtime_data_urls_.end()) {
-      receiver_.ReportBadMessage("Unexpected JSON request url");
-      return;
-    }
+  if (url_request.url == script_url_ &&
+      accept_header == "application/javascript") {
+    is_request_allowed = true;
+  } else if (trusted_signals_url_ && url_request.url == *trusted_signals_url_ &&
+             accept_header == "application/json") {
+    is_request_allowed = true;
+  }
 
-    // Make sure the query string starts with the correct prefix.
-    if (!base::StartsWith(url_request.url.query_piece(),
-                          expected_query_prefix_)) {
-      receiver_.ReportBadMessage("JSON query string missing expected prefix");
-      return;
-    }
-
-    // This should contain the keys value of the query string.
-    base::StringPiece keys =
-        url_request.url.query_piece().substr(expected_query_prefix_.size());
-    // The keys value should be the last value of the query string.
-    if (keys.find('&') != base::StringPiece::npos) {
-      receiver_.ReportBadMessage(
-          "JSON query string has unexpected additional parameter");
-      return;
-    }
-    // This is safe, because `realtime_data_urls_` can only be registered by
-    // calling `joinAdInterestGroup` from the URL's origin.
-    use_publisher_frame_loader = false;
-  } else {
-    receiver_.ReportBadMessage("Accept header has unexpected value");
+  if (!is_request_allowed) {
+    receiver_.ReportBadMessage("Unexpected request");
     return;
   }
 
@@ -145,9 +89,7 @@
   new_request.credentials_mode = network::mojom::CredentialsMode::kOmit;
   new_request.request_initiator = frame_origin_;
 
-  network::mojom::URLLoaderFactory* url_loader_factory = nullptr;
-  if (use_publisher_frame_loader) {
-    url_loader_factory = get_publisher_frame_url_loader_factory_.Run();
+  if (use_cors_) {
     new_request.mode = network::mojom::RequestMode::kCors;
   } else {
     // Treat this as a subresource request from the owner's origin, using the
@@ -156,7 +98,6 @@
     // TODO(mmenke): This leaks information to the third party that made the
     // request (both the URL itself leaks information, and using the origin's
     // NIK leaks information). These leaks need to be fixed.
-    url_loader_factory = get_trusted_url_loader_factory_.Run();
     new_request.mode = network::mojom::RequestMode::kNoCors;
     new_request.trusted_params = network::ResourceRequest::TrustedParams();
     url::Origin origin = url::Origin::Create(url_request.url);
@@ -171,7 +112,7 @@
   // TODO(mmenke): Investigate whether `devtools_observer` or
   // `report_raw_headers` should be set when devtools is open.
 
-  url_loader_factory->CreateLoaderAndStart(
+  get_url_loader_factory_.Run()->CreateLoaderAndStart(
       std::move(receiver),
       // These are browser-initiated requests, so give them a browser request
       // ID. Extension APIs may expect these to be unique.
@@ -182,7 +123,6 @@
 
 void AuctionURLLoaderFactoryProxy::Clone(
     mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver) {
-  // Not currently needed.
   NOTREACHED();
 }
 
diff --git a/content/browser/interest_group/auction_url_loader_factory_proxy.h b/content/browser/interest_group/auction_url_loader_factory_proxy.h
index b5ee615b..c830072af 100644
--- a/content/browser/interest_group/auction_url_loader_factory_proxy.h
+++ b/content/browser/interest_group/auction_url_loader_factory_proxy.h
@@ -7,19 +7,14 @@
 
 #include <stdint.h>
 
-#include <set>
-#include <vector>
-
 #include "base/callback_forward.h"
 #include "base/strings/string_piece_forward.h"
 #include "content/common/content_export.h"
-#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom-forward.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "services/network/public/mojom/url_loader_factory.mojom.h"
-#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom-forward.h"
 #include "url/gurl.h"
 
 namespace content {
@@ -35,36 +30,25 @@
   // Passed in callbacks must be safe to call at any time during the lifetime of
   // the AuctionURLLoaderFactoryProxy.
   //
-  // `get_publisher_frame_url_loader_factory` returns a URLLoaderFactory
-  // configured to behave like the URLLoaderFactory in use by the frame running
-  // the auction. It uses the same network partition, request initiator lock
-  // etc. This is used to request resources specified by the publisher page
-  // (currently, just the the `decision_logic_url`). This is needed to protect
-  // against a V8 compromise being used to access arbitrary resources by setting
-  // the `decision_logic_url` to a target site. URLs associated with interest
-  // groups already have first-party opt in, so don't need this, but the seller
-  // URLs do not. If `decision_logic_url` matches any bidding script URL, the
-  // frame factory is used for all requests for that URL. Bidder JSON requests
-  // are distinguishable via their accept header, so always use the trusted
-  // factory.
+  // `get_url_loader_factory` returns the URLLoaderFactory to use. Must be safe
+  // to call at any point until `this` has been destroyed.
   //
-  // `get_trusted_url_loader_factory` returns a trusted URLLoaderFactory that
-  // can request arbitrary URLs. This is used to request interest groups with
-  // the appropriate network partition. Each interest group URL request needs to
-  // use the partition of the associated interest group to avoid leaking the
-  // fetched URLs to the publisher, since interest groups are roughly analogous
-  // to more restricted third party cookies.
+  // `frame_origin` is the origin of the frame running the auction. Used as the
+  // initiator.
   //
-  // URLs that may be requested are extracted from `auction_config` and
-  // `bidders`. Any other requested URL will result in failure.
+  // `use_cors` indicates if requests should use CORS or not. Should be true for
+  // seller worklets.
+  //
+  // `script_url` is the Javascript URL for the worklet, and
+  // `trusted_signals_url` is the optional JSON url for additional input to the
+  // script. No other URLs may be requested.
   AuctionURLLoaderFactoryProxy(
       mojo::PendingReceiver<network::mojom::URLLoaderFactory> pending_receiver,
-      GetUrlLoaderFactoryCallback get_publisher_frame_url_loader_factory,
-      GetUrlLoaderFactoryCallback get_trusted_url_loader_factory,
+      GetUrlLoaderFactoryCallback get_url_loader_factory,
       const url::Origin& frame_origin,
-      const blink::mojom::AuctionAdConfig& auction_config,
-      const std::vector<auction_worklet::mojom::BiddingInterestGroupPtr>&
-          bidders);
+      bool use_cors,
+      const GURL& script_url,
+      const absl::optional<GURL>& trusted_signals_url = absl::nullopt);
   AuctionURLLoaderFactoryProxy(const AuctionURLLoaderFactoryProxy&) = delete;
   AuctionURLLoaderFactoryProxy& operator=(const AuctionURLLoaderFactoryProxy&) =
       delete;
@@ -85,26 +69,13 @@
  private:
   mojo::Receiver<network::mojom::URLLoaderFactory> receiver_;
 
-  const GetUrlLoaderFactoryCallback get_publisher_frame_url_loader_factory_;
-  const GetUrlLoaderFactoryCallback get_trusted_url_loader_factory_;
+  const GetUrlLoaderFactoryCallback get_url_loader_factory_;
 
   const url::Origin frame_origin_;
+  const bool use_cors_;
 
-  // URL of the seller script. Requested URLs may match these URLs exactly.
-  // Unlike `bidding_urls_`, requests for this URL must use the publisher
-  // frame's more restricted URLLoaderFactory. See constructor for more details.
-  GURL decision_logic_url_;
-
-  // URLs of worklet bidder scripts. Requested URLs may match these URLs
-  // exactly.
-  std::set<GURL> bidding_urls_;
-
-  // URLs for real-time bidding data. Requests may match these with the query
-  // parameter removed.
-  std::set<GURL> realtime_data_urls_;
-
-  // Expected prefix for all realtime data URL query strings.
-  const std::string expected_query_prefix_;
+  const GURL script_url_;
+  const absl::optional<GURL> trusted_signals_url_;
 };
 
 }  // namespace content
diff --git a/content/browser/interest_group/auction_url_loader_factory_proxy_unittest.cc b/content/browser/interest_group/auction_url_loader_factory_proxy_unittest.cc
index ca5bbf0e..c04e7f4 100644
--- a/content/browser/interest_group/auction_url_loader_factory_proxy_unittest.cc
+++ b/content/browser/interest_group/auction_url_loader_factory_proxy_unittest.cc
@@ -14,7 +14,6 @@
 #include "base/run_loop.h"
 #include "base/test/bind.h"
 #include "base/test/task_environment.h"
-#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/system/functions.h"
@@ -26,39 +25,25 @@
 #include "services/network/public/mojom/url_loader_factory.mojom.h"
 #include "services/network/test/test_url_loader_factory.h"
 #include "testing/gtest/include/gtest/gtest.h"
-#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
 #include "url/gurl.h"
 #include "url/origin.h"
 
 namespace content {
 
-// URL for the scoring worklet.
-const char kScoringWorkletUrl[] = "https://decision_logic_url.test/foo";
-
-// URLs for bidding worklets and their trusted bidding signals. Third worklet
-// has no trusted bidding signals URL.
-
-const char kBiddingWorkletUrl1[] = "https://bidding_url1.test/";
-const char kTrustedBiddingSignalsUrl1[] =
-    "https://trusted_bidding_signals_url1.test/";
-
-const char kBiddingWorkletUrl2[] = "https://bidding_url2.test/foo";
-const char kTrustedBiddingSignalsUrl2[] =
-    "https://trusted_bidding_signals_url2.test/bar";
-
-const char kBiddingWorkletUrl3[] = "https://bidding_url3.test/baz?foobar";
+const char kScriptUrl[] = "https://host.test/script";
+const char kTrustedSignalsUrl[] = "https://host.test/trusted_signals";
 
 // Values for the Accept header.
 const char kAcceptJavascript[] = "application/javascript";
 const char kAcceptJson[] = "application/json";
+const char kAcceptOther[] = "binary/ocelot-stream";
 
 class ActionUrlLoaderFactoryProxyTest : public testing::Test {
  public:
   // Ways the proxy can behave in response to a request.
   enum class ExpectedResponse {
     kReject,
-    kUseFrameFactory,
-    kUseTrustedFactory,
+    kAllow,
   };
 
   ActionUrlLoaderFactoryProxyTest() { CreateUrlLoaderFactoryProxy(); }
@@ -72,37 +57,14 @@
     // old one, or the old one's pipe was closed.
     DCHECK(!remote_url_loader_factory_ ||
            !remote_url_loader_factory_.is_connected());
-    blink::mojom::AuctionAdConfigPtr auction_config =
-        blink::mojom::AuctionAdConfig::New();
-    auction_config->decision_logic_url = GURL(kScoringWorkletUrl);
-    std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders;
-
-    bidders.emplace_back(auction_worklet::mojom::BiddingInterestGroup::New());
-    bidders.back()->group = blink::mojom::InterestGroup::New();
-    bidders.back()->group->bidding_url = GURL(kBiddingWorkletUrl1);
-    bidders.back()->group->trusted_bidding_signals_url =
-        GURL(kTrustedBiddingSignalsUrl1);
-
-    bidders.emplace_back(auction_worklet::mojom::BiddingInterestGroup::New());
-    bidders.back()->group = blink::mojom::InterestGroup::New();
-    bidders.back()->group->bidding_url = GURL(kBiddingWorkletUrl2);
-    bidders.back()->group->trusted_bidding_signals_url =
-        GURL(kTrustedBiddingSignalsUrl2);
-
-    bidders.emplace_back(auction_worklet::mojom::BiddingInterestGroup::New());
-    bidders.back()->group = blink::mojom::InterestGroup::New();
-    bidders.back()->group->bidding_url = GURL(kBiddingWorkletUrl3);
 
     remote_url_loader_factory_.reset();
     url_loader_factory_proxy_ = std::make_unique<AuctionURLLoaderFactoryProxy>(
         remote_url_loader_factory_.BindNewPipeAndPassReceiver(),
         base::BindRepeating(
             [](network::mojom::URLLoaderFactory* factory) { return factory; },
-            &proxied_frame_url_loader_factory_),
-        base::BindRepeating(
-            [](network::mojom::URLLoaderFactory* factory) { return factory; },
-            &proxied_trusted_url_loader_factory_),
-        frame_origin_, *auction_config, bidders);
+            &proxied_url_loader_factory_),
+        frame_origin_, use_cors_, GURL(kScriptUrl), trusted_signals_url_);
   }
 
   // Attempts to make a request for `request`.
@@ -112,10 +74,7 @@
     if (!remote_url_loader_factory_.is_connected())
       CreateUrlLoaderFactoryProxy();
 
-    int initial_num_frame_requests =
-        proxied_frame_url_loader_factory_.NumPending();
-    int initial_num_trusted_requests =
-        proxied_trusted_url_loader_factory_.NumPending();
+    int initial_num_requests = proxied_url_loader_factory_.NumPending();
 
     // Try to send a request. Requests are never run to completion, instead,
     // requests that make it to the nested `url_loader_factory_` are tracked in
@@ -137,31 +96,17 @@
     network::TestURLLoaderFactory::PendingRequest const* pending_request;
     switch (expected_response) {
       case ExpectedResponse::kReject:
-        // A request being rejected closes the receiver.
-        EXPECT_EQ(initial_num_frame_requests,
-                  proxied_frame_url_loader_factory_.NumPending());
-        EXPECT_EQ(initial_num_trusted_requests,
-                  proxied_trusted_url_loader_factory_.NumPending());
+        EXPECT_EQ(initial_num_requests,
+                  proxied_url_loader_factory_.NumPending());
         // Rejecting a request should result in closing the factory mojo pipe.
         EXPECT_FALSE(remote_url_loader_factory_.is_connected());
         return;
-      case ExpectedResponse::kUseFrameFactory:
-        ASSERT_EQ(initial_num_frame_requests + 1,
-                  proxied_frame_url_loader_factory_.NumPending());
-        ASSERT_EQ(initial_num_trusted_requests,
-                  proxied_trusted_url_loader_factory_.NumPending());
+      case ExpectedResponse::kAllow:
+        EXPECT_EQ(initial_num_requests + 1,
+                  proxied_url_loader_factory_.NumPending());
         EXPECT_TRUE(remote_url_loader_factory_.is_connected());
         pending_request =
-            &proxied_frame_url_loader_factory_.pending_requests()->back();
-        break;
-      case ExpectedResponse::kUseTrustedFactory:
-        ASSERT_EQ(initial_num_frame_requests,
-                  proxied_frame_url_loader_factory_.NumPending());
-        ASSERT_EQ(initial_num_trusted_requests + 1,
-                  proxied_trusted_url_loader_factory_.NumPending());
-        EXPECT_TRUE(remote_url_loader_factory_.is_connected());
-        pending_request =
-            &proxied_trusted_url_loader_factory_.pending_requests()->back();
+            &proxied_url_loader_factory_.pending_requests()->back();
         break;
     }
 
@@ -175,13 +120,7 @@
     // unique within the browser process, not just among requests using the
     // AuctionURLLoaderFactoryProxy.
     for (const auto& other_pending_request :
-         *proxied_frame_url_loader_factory_.pending_requests()) {
-      if (&other_pending_request == pending_request)
-        continue;
-      EXPECT_NE(other_pending_request.request_id, pending_request->request_id);
-    }
-    for (const auto& other_pending_request :
-         *proxied_trusted_url_loader_factory_.pending_requests()) {
+         *proxied_url_loader_factory_.pending_requests()) {
       if (&other_pending_request == pending_request)
         continue;
       EXPECT_NE(other_pending_request.request_id, pending_request->request_id);
@@ -213,7 +152,7 @@
     // The initiator should be set.
     EXPECT_EQ(frame_origin_, observed_request.request_initiator);
 
-    if (expected_response == ExpectedResponse::kUseFrameFactory) {
+    if (use_cors_) {
       EXPECT_EQ(network::mojom::RequestMode::kCors, observed_request.mode);
       EXPECT_FALSE(observed_request.trusted_params);
     } else {
@@ -246,170 +185,107 @@
  protected:
   base::test::TaskEnvironment task_environment_;
 
+  bool use_cors_ = false;
+  absl::optional<GURL> trusted_signals_url_ = GURL(kTrustedSignalsUrl);
+
   url::Origin frame_origin_ = url::Origin::Create(GURL("https://foo.test/"));
-  network::TestURLLoaderFactory proxied_frame_url_loader_factory_;
-  network::TestURLLoaderFactory proxied_trusted_url_loader_factory_;
+  network::TestURLLoaderFactory proxied_url_loader_factory_;
   std::unique_ptr<AuctionURLLoaderFactoryProxy> url_loader_factory_proxy_;
   mojo::Remote<network::mojom::URLLoaderFactory> remote_url_loader_factory_;
 };
 
-// Test exact URL matches. Trusted bidding signals URLs should be rejected
-// unless that have a valid query string appended.
-TEST_F(ActionUrlLoaderFactoryProxyTest, ExactURLMatch) {
-  TryMakeRequest(kScoringWorkletUrl, kAcceptJavascript,
-                 ExpectedResponse::kUseFrameFactory);
-  TryMakeRequest(kScoringWorkletUrl, kAcceptJson, ExpectedResponse::kReject);
-  TryMakeRequest(kScoringWorkletUrl, "Unknown/Unknown",
-                 ExpectedResponse::kReject);
-  TryMakeRequest(kScoringWorkletUrl, absl::nullopt, ExpectedResponse::kReject);
+TEST_F(ActionUrlLoaderFactoryProxyTest, Basic) {
+  for (bool use_cors : {false, true}) {
+    use_cors_ = use_cors;
+    // Force creation of a new proxy, with correct CORS value.
+    remote_url_loader_factory_.reset();
+    CreateUrlLoaderFactoryProxy();
 
-  TryMakeRequest(kBiddingWorkletUrl1, kAcceptJavascript,
-                 ExpectedResponse::kUseTrustedFactory);
-  TryMakeRequest(kBiddingWorkletUrl1, kAcceptJson, ExpectedResponse::kReject);
-  TryMakeRequest(kBiddingWorkletUrl1, "Unknown/Unknown",
-                 ExpectedResponse::kReject);
-  TryMakeRequest(kBiddingWorkletUrl1, absl::nullopt, ExpectedResponse::kReject);
-  TryMakeRequest(kTrustedBiddingSignalsUrl1, kAcceptJavascript,
-                 ExpectedResponse::kReject);
-  TryMakeRequest(kTrustedBiddingSignalsUrl1, kAcceptJson,
-                 ExpectedResponse::kReject);
-  TryMakeRequest(kTrustedBiddingSignalsUrl1, "Unknown/Unknown",
-                 ExpectedResponse::kReject);
-  TryMakeRequest(kTrustedBiddingSignalsUrl1, absl::nullopt,
-                 ExpectedResponse::kReject);
+    TryMakeRequest(kScriptUrl, kAcceptJavascript, ExpectedResponse::kAllow);
+    TryMakeRequest(kScriptUrl, kAcceptJson, ExpectedResponse::kReject);
+    TryMakeRequest(kScriptUrl, kAcceptOther, ExpectedResponse::kReject);
+    TryMakeRequest(kScriptUrl, absl::nullopt, ExpectedResponse::kReject);
 
-  TryMakeRequest(kBiddingWorkletUrl2, kAcceptJavascript,
-                 ExpectedResponse::kUseTrustedFactory);
-  TryMakeRequest(kBiddingWorkletUrl2, kAcceptJson, ExpectedResponse::kReject);
-  TryMakeRequest(kTrustedBiddingSignalsUrl2, kAcceptJavascript,
-                 ExpectedResponse::kReject);
-  TryMakeRequest(kTrustedBiddingSignalsUrl2, kAcceptJson,
-                 ExpectedResponse::kReject);
-
-  TryMakeRequest(kBiddingWorkletUrl3, kAcceptJavascript,
-                 ExpectedResponse::kUseTrustedFactory);
-  TryMakeRequest(kBiddingWorkletUrl3, kAcceptJson, ExpectedResponse::kReject);
-}
-
-TEST_F(ActionUrlLoaderFactoryProxyTest, QueryStrings) {
-  const char* kValidBiddingSignalsQueryStrings[] = {
-      "?hostname=foo.test&keys=bar,baz",
-      "?hostname=foo.test&keys=bar",
-      // Escaped &.
-      "?hostname=foo.test&keys=bar%26",
-  };
-
-  const char* kInvalidBiddingSignalsQueryStrings[] = {
-      "?hostname=bar.test&keys=bar,baz",
-      "?hostname=foo.test&keys=bar&hats=foo",
-      "?hostname=foo.test&keys=bar&hats",
-      "?hostname=foo.test",
-      "?hostname=foo.test&keys",
-      "?bar=foo.test&keys=bar,baz",
-      "?keys=bar,baz",
-      "?",
-  };
-
-  for (std::string query_string : kValidBiddingSignalsQueryStrings) {
-    SCOPED_TRACE(query_string);
-    TryMakeRequest(kScoringWorkletUrl + query_string, kAcceptJavascript,
+    TryMakeRequest(kTrustedSignalsUrl, kAcceptJavascript,
+                   ExpectedResponse::kReject);
+    TryMakeRequest(kTrustedSignalsUrl, kAcceptJson, ExpectedResponse::kAllow);
+    TryMakeRequest(kTrustedSignalsUrl, kAcceptOther, ExpectedResponse::kReject);
+    TryMakeRequest(kTrustedSignalsUrl, absl::nullopt,
                    ExpectedResponse::kReject);
 
-    TryMakeRequest(kBiddingWorkletUrl1 + query_string, kAcceptJavascript,
+    TryMakeRequest("https://host.test/", kAcceptJavascript,
                    ExpectedResponse::kReject);
-    TryMakeRequest(kTrustedBiddingSignalsUrl1 + query_string, kAcceptJson,
-                   ExpectedResponse::kUseTrustedFactory);
-
-    TryMakeRequest(kBiddingWorkletUrl2 + query_string, kAcceptJavascript,
+    TryMakeRequest("https://host.test/", kAcceptJson,
                    ExpectedResponse::kReject);
-    TryMakeRequest(kTrustedBiddingSignalsUrl2 + query_string, kAcceptJson,
-                   ExpectedResponse::kUseTrustedFactory);
-
-    TryMakeRequest(kBiddingWorkletUrl3 + query_string, kAcceptJavascript,
+    TryMakeRequest("https://host.test/", kAcceptOther,
                    ExpectedResponse::kReject);
-  }
-
-  for (std::string query_string : kInvalidBiddingSignalsQueryStrings) {
-    SCOPED_TRACE(query_string);
-    TryMakeRequest(kScoringWorkletUrl + query_string, kAcceptJavascript,
-                   ExpectedResponse::kReject);
-
-    TryMakeRequest(kBiddingWorkletUrl1 + query_string, kAcceptJavascript,
-                   ExpectedResponse::kReject);
-    TryMakeRequest(kTrustedBiddingSignalsUrl1 + query_string, kAcceptJson,
-                   ExpectedResponse::kReject);
-
-    TryMakeRequest(kBiddingWorkletUrl2 + query_string, kAcceptJavascript,
-                   ExpectedResponse::kReject);
-    TryMakeRequest(kTrustedBiddingSignalsUrl2 + query_string, kAcceptJson,
-                   ExpectedResponse::kReject);
-
-    TryMakeRequest(kBiddingWorkletUrl3 + query_string, kAcceptJavascript,
+    TryMakeRequest("https://host.test/", absl::nullopt,
                    ExpectedResponse::kReject);
   }
 }
 
-// Set a number of extra parameters on the ResourceRequest, which the
-// AuctionURLLoaderFactoryProxy should ignore. This test relies heavily on the
-// validity checks in TryMakeRequest().
-TEST_F(ActionUrlLoaderFactoryProxyTest, ExtraParametersIgnored) {
-  network::ResourceRequest request;
+TEST_F(ActionUrlLoaderFactoryProxyTest, NoTrustedSignalsUrl) {
+  trusted_signals_url_ = absl::nullopt;
 
-  request.credentials_mode = network::mojom::CredentialsMode::kInclude;
-  request.trusted_params = network::ResourceRequest::TrustedParams();
+  for (bool use_cors : {false, true}) {
+    use_cors_ = use_cors;
+    // Force creation of a new proxy, with correct CORS value.
+    remote_url_loader_factory_.reset();
+    CreateUrlLoaderFactoryProxy();
 
-  url::Origin other_origin =
-      url::Origin::Create(GURL("https://somewhere.else.text"));
-  request.trusted_params->isolation_info = net::IsolationInfo::Create(
-      net::IsolationInfo::RequestType::kMainFrame, other_origin, other_origin,
-      net::SiteForCookies::FromOrigin(other_origin));
-  request.trusted_params->disable_secure_dns = true;
+    TryMakeRequest(kScriptUrl, kAcceptJavascript, ExpectedResponse::kAllow);
+    TryMakeRequest(kScriptUrl, kAcceptJson, ExpectedResponse::kReject);
+    TryMakeRequest(kScriptUrl, kAcceptOther, ExpectedResponse::kReject);
+    TryMakeRequest(kScriptUrl, absl::nullopt, ExpectedResponse::kReject);
 
-  request.headers.SetHeader("Flux-Capacitor", "Y");
-  request.headers.SetHeader("Host", "Fred");
-  request.headers.SetHeader(net::HttpRequestHeaders::kAccept,
-                            kAcceptJavascript);
+    TryMakeRequest(kTrustedSignalsUrl, kAcceptJavascript,
+                   ExpectedResponse::kReject);
+    TryMakeRequest(kTrustedSignalsUrl, kAcceptJson, ExpectedResponse::kReject);
+    TryMakeRequest(kTrustedSignalsUrl, kAcceptOther, ExpectedResponse::kReject);
+    TryMakeRequest(kTrustedSignalsUrl, absl::nullopt,
+                   ExpectedResponse::kReject);
 
-  request.url = GURL(kScoringWorkletUrl);
-  TryMakeRequest(request, ExpectedResponse::kUseFrameFactory);
-
-  request.url = GURL(kBiddingWorkletUrl1);
-  TryMakeRequest(request, ExpectedResponse::kUseTrustedFactory);
-
-  request.url = GURL(std::string(kTrustedBiddingSignalsUrl1) +
-                     "?hostname=foo.test&keys=bar");
-  request.headers.SetHeader(net::HttpRequestHeaders::kAccept, kAcceptJson);
-  TryMakeRequest(request, ExpectedResponse::kUseTrustedFactory);
+    TryMakeRequest("https://host.test/", kAcceptJavascript,
+                   ExpectedResponse::kReject);
+    TryMakeRequest("https://host.test/", kAcceptJson,
+                   ExpectedResponse::kReject);
+    TryMakeRequest("https://host.test/", kAcceptOther,
+                   ExpectedResponse::kReject);
+    TryMakeRequest("https://host.test/", absl::nullopt,
+                   ExpectedResponse::kReject);
+  }
 }
 
-// If a bidder URL matches the scoring URL, the publisher frame's
-// URLLoaderFactory should be used instead of the trusted one.
-TEST_F(ActionUrlLoaderFactoryProxyTest, BidderUrlMatchesScoringUrl) {
-  blink::mojom::AuctionAdConfigPtr auction_config =
-      blink::mojom::AuctionAdConfig::New();
-  auction_config->decision_logic_url = GURL(kScoringWorkletUrl);
-  std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders;
+TEST_F(ActionUrlLoaderFactoryProxyTest, SameUrl) {
+  trusted_signals_url_ = GURL(kScriptUrl);
 
-  bidders.emplace_back(auction_worklet::mojom::BiddingInterestGroup::New());
-  bidders.back()->group = blink::mojom::InterestGroup::New();
-  bidders.back()->group->bidding_url = GURL(kScoringWorkletUrl);
+  for (bool use_cors : {false, true}) {
+    use_cors_ = use_cors;
+    // Force creation of a new proxy, with correct CORS value.
+    remote_url_loader_factory_.reset();
+    CreateUrlLoaderFactoryProxy();
 
-  remote_url_loader_factory_.reset();
-  url_loader_factory_proxy_ = std::make_unique<AuctionURLLoaderFactoryProxy>(
-      remote_url_loader_factory_.BindNewPipeAndPassReceiver(),
-      base::BindRepeating(
-          [](network::mojom::URLLoaderFactory* factory) { return factory; },
-          &proxied_frame_url_loader_factory_),
-      base::BindRepeating(
-          [](network::mojom::URLLoaderFactory* factory) { return factory; },
-          &proxied_trusted_url_loader_factory_),
-      frame_origin_, *auction_config, bidders);
+    TryMakeRequest(kScriptUrl, kAcceptJavascript, ExpectedResponse::kAllow);
+    TryMakeRequest(kScriptUrl, kAcceptJson, ExpectedResponse::kAllow);
+    TryMakeRequest(kScriptUrl, kAcceptOther, ExpectedResponse::kReject);
+    TryMakeRequest(kScriptUrl, absl::nullopt, ExpectedResponse::kReject);
 
-  // Make request twice, as will actually happen in this case.
-  TryMakeRequest(kScoringWorkletUrl, kAcceptJavascript,
-                 ExpectedResponse::kUseFrameFactory);
-  TryMakeRequest(kScoringWorkletUrl, kAcceptJavascript,
-                 ExpectedResponse::kUseFrameFactory);
+    TryMakeRequest(kTrustedSignalsUrl, kAcceptJavascript,
+                   ExpectedResponse::kReject);
+    TryMakeRequest(kTrustedSignalsUrl, kAcceptJson, ExpectedResponse::kReject);
+    TryMakeRequest(kTrustedSignalsUrl, kAcceptOther, ExpectedResponse::kReject);
+    TryMakeRequest(kTrustedSignalsUrl, absl::nullopt,
+                   ExpectedResponse::kReject);
+
+    TryMakeRequest("https://host.test/", kAcceptJavascript,
+                   ExpectedResponse::kReject);
+    TryMakeRequest("https://host.test/", kAcceptJson,
+                   ExpectedResponse::kReject);
+    TryMakeRequest("https://host.test/", kAcceptOther,
+                   ExpectedResponse::kReject);
+    TryMakeRequest("https://host.test/", absl::nullopt,
+                   ExpectedResponse::kReject);
+  }
 }
 
 }  // namespace content
diff --git a/content/browser/media/media_source_browsertest.cc b/content/browser/media/media_source_browsertest.cc
index 2372cdec..27d92d4 100644
--- a/content/browser/media/media_source_browsertest.cc
+++ b/content/browser/media/media_source_browsertest.cc
@@ -74,6 +74,20 @@
   TestSimplePlayback("bear-320x240-audio-only.webm", media::kEnded);
 }
 
+IN_PROC_BROWSER_TEST_F(MediaSourceTest, Playback_AudioOnly_MP3) {
+  TestSimplePlayback("sfx.mp3", media::kEnded);
+}
+
+IN_PROC_BROWSER_TEST_F(
+    MediaSourceTest,
+    Playback_AudioOnly_MP3_With_Codecs_Parameter_Should_Fail) {
+  // We override the correct media type for this file with one which erroneously
+  // includes a codecs parameter that is valid for progressive but invalid for
+  // MSE type support.
+  DCHECK_EQ(media::GetMimeTypeForFile("sfx.mp3"), "audio/mpeg");
+  TestSimplePlayback("sfx.mp3", "audio/mpeg; codecs=\"mp3\"", media::kFailed);
+}
+
 // Test the case where test file and mime type mismatch.
 IN_PROC_BROWSER_TEST_F(MediaSourceTest, Playback_Type_Error) {
   const char kWebMAudioOnly[] = "audio/webm; codecs=\"vorbis\"";
diff --git a/content/browser/process_internals/process_internals.mojom b/content/browser/process_internals/process_internals.mojom
index 0b2f4ef..8ae0f5f 100644
--- a/content/browser/process_internals/process_internals.mojom
+++ b/content/browser/process_internals/process_internals.mojom
@@ -70,8 +70,8 @@
   // added in response to heuristics triggered directly by web sites, such
   // as headers that suggest the site might benefit from isolation.  Like
   // user-triggered isolated origins, these isolated origins apply within
-  // the current profile only, though currently they aren't preserved across
-  // restarts.
+  // the current profile only, they are preserved across
+  // restarts, and they are cleared when the user clears browsing data.
   GetWebTriggeredIsolatedOrigins() => (array<string> isolated_origins);
 
   // Returns a list of isolated origins that apply globally in all profiles.
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index 5561074..f900b91 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -2423,8 +2423,10 @@
     return false;
 
   // There's no need for additional isolation if the site already requires a
-  // dedicated process (e.g., if the site was isolated via other isolation
-  // mechanisms or from a prior visit to the site with COOP headers).
+  // dedicated process via other isolation mechanisms.  However, we still
+  // return true if the site has been isolated due to COOP previously, so that
+  // we can go through the COOP isolation flow to update the timestamp of when
+  // the COOP isolation for this site was last used.
   //
   // Note: we can use `site_info_` here, since that has been assigned at
   // request start time and updated by redirects, but it is not (currently)
@@ -2437,9 +2439,14 @@
   DCHECK(!site_info_.does_site_request_dedicated_process_for_coop());
   if (site_info_.RequiresDedicatedProcess(
           GetStartingSiteInstance()->GetIsolationContext())) {
-    return false;
+    // Note: use process_lock_url() to check isolation on the actual site
+    // rather than the effective URL in the case of hosted apps.
+    bool is_already_isolated_due_to_coop =
+        ChildProcessSecurityPolicyImpl::GetInstance()->IsIsolatedSiteFromSource(
+            url::Origin::Create(site_info_.process_lock_url()),
+            ChildProcessSecurityPolicy::IsolatedOriginSource::WEB_TRIGGERED);
+    return is_already_isolated_due_to_coop;
   }
-
   return true;
 }
 
diff --git a/content/browser/renderer_host/render_frame_host_delegate.h b/content/browser/renderer_host/render_frame_host_delegate.h
index 2a956cb6..854c364 100644
--- a/content/browser/renderer_host/render_frame_host_delegate.h
+++ b/content/browser/renderer_host/render_frame_host_delegate.h
@@ -43,6 +43,7 @@
 #include "third_party/blink/public/mojom/frame/blocked_navigation_types.mojom.h"
 #include "third_party/blink/public/mojom/frame/frame.mojom-forward.h"
 #include "third_party/blink/public/mojom/loader/resource_load_info.mojom.h"
+#include "third_party/blink/public/mojom/media/capture_handle_config.mojom.h"
 #include "third_party/skia/include/core/SkColor.h"
 #include "ui/accessibility/ax_mode.h"
 #include "ui/base/window_open_disposition.h"
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 9708cea3..24eb7cb 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -5311,7 +5311,7 @@
           GetSiteInstance()->GetBrowserContext(),
           GetMainFrame()->GetLastCommittedURL(),
           ChildProcessSecurityPolicy::IsolatedOriginSource::WEB_TRIGGERED,
-          false /* should_persist */);
+          SiteIsolationPolicy::ShouldPersistIsolatedCOOPSites());
     }
   }
 }
diff --git a/content/browser/resources/process/process_internals.js b/content/browser/resources/process/process_internals.js
index fcd0e6d..959ec00 100644
--- a/content/browser/resources/process/process_internals.js
+++ b/content/browser/resources/process/process_internals.js
@@ -238,7 +238,8 @@
     $('web-triggered-isolated-origins').textContent =
         'The following origins are isolated based on runtime heuristics ' +
         'triggered directly by web pages, such as Cross-Origin-Opener-Policy ' +
-        'headers. This list is cleared after a restart.';
+        'headers. Clear cookies or history to wipe this list; this takes ' +
+        'effect after a restart.';
 
     const list = document.createElement('ul');
     for (const origin of response.isolatedOrigins) {
diff --git a/content/browser/sandbox_parameters_mac.mm b/content/browser/sandbox_parameters_mac.mm
index b9e6d18a..a3c412f 100644
--- a/content/browser/sandbox_parameters_mac.mm
+++ b/content/browser/sandbox_parameters_mac.mm
@@ -191,11 +191,6 @@
 }
 #endif
 
-void SetupUtilitySandboxParameters(sandbox::SeatbeltExecClient* client,
-                                   const base::CommandLine& command_line) {
-  SetupCommonSandboxParameters(client);
-}
-
 void SetupGpuSandboxParameters(sandbox::SeatbeltExecClient* client,
                                const base::CommandLine& command_line) {
   SetupCommonSandboxParameters(client);
@@ -214,10 +209,12 @@
   switch (sandbox_type) {
     case sandbox::policy::SandboxType::kAudio:
     case sandbox::policy::SandboxType::kCdm:
+    case sandbox::policy::SandboxType::kMirroring:
     case sandbox::policy::SandboxType::kNaClLoader:
     case sandbox::policy::SandboxType::kPrintBackend:
     case sandbox::policy::SandboxType::kPrintCompositor:
     case sandbox::policy::SandboxType::kRenderer:
+    case sandbox::policy::SandboxType::kUtility:
       SetupCommonSandboxParameters(client);
       break;
     case sandbox::policy::SandboxType::kGpu: {
@@ -232,9 +229,6 @@
       SetupPPAPISandboxParameters(client);
 #endif
       break;
-    case sandbox::policy::SandboxType::kUtility:
-      SetupUtilitySandboxParameters(client, command_line);
-      break;
     case sandbox::policy::SandboxType::kNoSandbox:
     case sandbox::policy::SandboxType::kVideoCapture:
       CHECK(false) << "Unhandled parameters for sandbox_type "
diff --git a/content/browser/service_worker/service_worker_context_wrapper.h b/content/browser/service_worker/service_worker_context_wrapper.h
index a4b1516..0f9b8fe 100644
--- a/content/browser/service_worker/service_worker_context_wrapper.h
+++ b/content/browser/service_worker/service_worker_context_wrapper.h
@@ -429,7 +429,6 @@
       ServiceWorkerContext::ResultCallback result_callback,
       blink::ServiceWorkerStatusCode service_worker_status);
 
-  // Called when ServiceWorkerImportedScriptUpdateCheck is enabled.
   std::unique_ptr<blink::PendingURLLoaderFactoryBundle>
   CreateNonNetworkPendingURLLoaderFactoryBundleForUpdateCheck(
       BrowserContext* browser_context);
diff --git a/content/browser/service_worker/service_worker_job_unittest.cc b/content/browser/service_worker/service_worker_job_unittest.cc
index a94e64e..cc2478e 100644
--- a/content/browser/service_worker/service_worker_job_unittest.cc
+++ b/content/browser/service_worker/service_worker_job_unittest.cc
@@ -1441,7 +1441,6 @@
   bool force_start_worker_failure_ = false;
   absl::optional<bool> will_be_terminated_;
 
-  // These are used only when ServiceWorkerImportedScriptUpdateCheck is enabled.
   FakeNetwork fake_network_;
   std::unique_ptr<URLLoaderInterceptor> interceptor_;
 
diff --git a/content/browser/service_worker/service_worker_script_loader_factory_unittest.cc b/content/browser/service_worker/service_worker_script_loader_factory_unittest.cc
index 918eaad..ae7a655 100644
--- a/content/browser/service_worker/service_worker_script_loader_factory_unittest.cc
+++ b/content/browser/service_worker/service_worker_script_loader_factory_unittest.cc
@@ -122,8 +122,7 @@
 }
 
 // This tests copying script and creating resume type
-// ServiceWorkerNewScriptLoaders when ServiceWorkerImportedScriptUpdateCheck
-// is enabled.
+// ServiceWorkerNewScriptLoaders.
 class ServiceWorkerScriptLoaderFactoryCopyResumeTest
     : public ServiceWorkerScriptLoaderFactoryTest {
  public:
diff --git a/content/browser/service_worker/service_worker_update_checker.h b/content/browser/service_worker/service_worker_update_checker.h
index 058e8489ab..78cb84b 100644
--- a/content/browser/service_worker/service_worker_update_checker.h
+++ b/content/browser/service_worker/service_worker_update_checker.h
@@ -22,8 +22,6 @@
 class ServiceWorkerContextCore;
 class ServiceWorkerVersion;
 
-// Used only when ServiceWorkerImportedScriptUpdateCheck is enabled.
-//
 // This is responsible for byte-for-byte update checking. Mostly corresponding
 // to step 1-9 in [[Update]] in the spec, but this stops to fetch scripts after
 // any changes found.
diff --git a/content/browser/service_worker/service_worker_version.h b/content/browser/service_worker/service_worker_version.h
index 54f27ccd..f3748a2d 100644
--- a/content/browser/service_worker/service_worker_version.h
+++ b/content/browser/service_worker/service_worker_version.h
@@ -571,7 +571,6 @@
   void IncrementPendingUpdateHintCount();
   void DecrementPendingUpdateHintCount();
 
-  // ServiceWorkerImportedScriptUpdateCheck:
   // Called on versions created for an update check. Called if the check
   // determined an update exists before starting the worker for an install
   // event.
@@ -1094,12 +1093,10 @@
 
   std::unique_ptr<blink::TrialTokenValidator> const validator_;
 
-  // Stores the result of byte-to-byte update check for each script. Used only
-  // when ServiceWorkerImportedScriptUpdateCheck is enabled.
+  // Stores the result of byte-to-byte update check for each script.
   std::map<GURL, ServiceWorkerUpdateChecker::ComparedScriptInfo>
       compared_script_info_map_;
 
-  // ServiceWorkerImportedScriptUpdateCheck:
   // If this version was created for an update check that found an update,
   // |updated_script_url_| is the URL of the script for which a byte-for-byte
   // change was found. Otherwise, it's the empty GURL.
diff --git a/content/browser/utility_sandbox_delegate.cc b/content/browser/utility_sandbox_delegate.cc
index 5413414..03d0af2 100644
--- a/content/browser/utility_sandbox_delegate.cc
+++ b/content/browser/utility_sandbox_delegate.cc
@@ -45,6 +45,9 @@
       sandbox_type_ == sandbox::policy::SandboxType::kIconReader ||
       sandbox_type_ == sandbox::policy::SandboxType::kMediaFoundationCdm ||
 #endif
+#if defined(OS_MAC)
+      sandbox_type_ == sandbox::policy::SandboxType::kMirroring ||
+#endif
       sandbox_type_ == sandbox::policy::SandboxType::kUtility ||
       sandbox_type_ == sandbox::policy::SandboxType::kNetwork ||
       sandbox_type_ == sandbox::policy::SandboxType::kCdm ||
diff --git a/content/browser/web_contents/web_contents_impl.h b/content/browser/web_contents/web_contents_impl.h
index 7e86720..fccd17ed 100644
--- a/content/browser/web_contents/web_contents_impl.h
+++ b/content/browser/web_contents/web_contents_impl.h
@@ -71,6 +71,7 @@
 #include "third_party/blink/public/mojom/frame/blocked_navigation_types.mojom.h"
 #include "third_party/blink/public/mojom/frame/frame.mojom-forward.h"
 #include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h"
+#include "third_party/blink/public/mojom/media/capture_handle_config.mojom.h"
 #include "third_party/blink/public/mojom/page/display_cutout.mojom.h"
 #include "third_party/blink/public/mojom/page/page_visibility_state.mojom.h"
 #include "ui/accessibility/ax_mode.h"
diff --git a/content/browser/webid/federated_auth_request_impl.cc b/content/browser/webid/federated_auth_request_impl.cc
index 586d043d..629f94c8 100644
--- a/content/browser/webid/federated_auth_request_impl.cc
+++ b/content/browser/webid/federated_auth_request_impl.cc
@@ -370,7 +370,7 @@
       DCHECK(!idp_web_contents_);
       idp_web_contents_ = CreateIdpWebContents();
       request_dialog_controller_->ShowAccountsDialog(
-          rp_web_contents, idp_web_contents_.get(), GURL(), accounts,
+          rp_web_contents, idp_web_contents_.get(), provider_, accounts,
           base::BindOnce(&FederatedAuthRequestImpl::OnAccountSelected,
                          weak_ptr_factory_.GetWeakPtr()));
       return;
diff --git a/content/child/runtime_features.cc b/content/child/runtime_features.cc
index f5c6782..a7851b3 100644
--- a/content/child/runtime_features.cc
+++ b/content/child/runtime_features.cc
@@ -354,6 +354,8 @@
           {"COLRV1Fonts", blink::features::kCOLRV1Fonts},
           {"CSSContainerQueries", blink::features::kCSSContainerQueries},
           {"CompositeAfterPaint", blink::features::kCompositeAfterPaint},
+          {"ComputePressure", blink::features::kComputePressure,
+           kSetOnlyIfOverridden},
           {"CSSColorSchemeUARendering", features::kCSSColorSchemeUARendering},
           {"DeclarativeShadowDOM", blink::features::kDeclarativeShadowDOM},
           {"DocumentTransition", blink::features::kDocumentTransition},
diff --git a/content/public/android/java/src/org/chromium/content/browser/accessibility/BrowserAccessibilityState.java b/content/public/android/java/src/org/chromium/content/browser/accessibility/BrowserAccessibilityState.java
index ad1a717..be5b2ad 100644
--- a/content/public/android/java/src/org/chromium/content/browser/accessibility/BrowserAccessibilityState.java
+++ b/content/public/android/java/src/org/chromium/content/browser/accessibility/BrowserAccessibilityState.java
@@ -14,6 +14,7 @@
 import android.os.Build;
 import android.os.Handler;
 import android.provider.Settings;
+import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 
 import org.chromium.base.ContextUtils;
@@ -26,6 +27,8 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
+import java.util.WeakHashMap;
 
 /**
  * Provides utility methods relating to measuring accessibility state on the current platform (i.e.
@@ -33,8 +36,23 @@
  */
 @JNINamespace("content")
 public class BrowserAccessibilityState {
+    /**
+     * An interface for classes that want to be notified whenever the accessibility
+     * state has changed, which can happen when accessibility services start or stop.
+     */
+    public interface Listener {
+        public void onBrowserAccessibilityStateChanged();
+    }
+
     private static final String TAG = "Accessibility";
 
+    // Analysis of the most popular accessibility services on Android suggests
+    // that any service that requests any of these three events is a screen reader
+    // or other complete assistive technology. If none of these events are requested,
+    // we can enable some optimizations.
+    private static final int SCREEN_READER_EVENT_TYPE_MASK = AccessibilityEvent.TYPE_VIEW_SELECTED
+            | AccessibilityEvent.TYPE_VIEW_SCROLLED | AccessibilityEvent.TYPE_ANNOUNCEMENT;
+
     private static boolean sInitialized;
 
     // A bitmask containing the union of all event types, feedback types, flags,
@@ -44,11 +62,21 @@
     private static int sFlagsMask;
     private static int sCapabilitiesMask;
 
+    // Whether we determine that genuine assistive technology such as a screen reader
+    // is running, based on the information from running accessibility services.
+    private static boolean sScreenReader;
+
     // The IDs of all running accessibility services.
     private static String[] sServiceIds;
 
     private static Handler sHandler;
 
+    // The set of listeners of BrowserAccessibilityState, implemented using
+    // a WeakHashSet behind the scenes so that listeners can be garbage-collected
+    // and will be automatically removed from this set.
+    private static Set<Listener> sListeners =
+            Collections.newSetFromMap(new WeakHashMap<Listener, Boolean>());
+
     // The number of milliseconds to wait before checking the set of running
     // accessibility services again, when we think it changed. Uses an exponential
     // back-off until it's greater than MAX_DELAY_MILLIS.
@@ -56,6 +84,16 @@
     private static final int MAX_DELAY_MILLIS = 60000;
     private static int sNextDelayMillis = MIN_DELAY_MILLIS;
 
+    public static void addListener(Listener listener) {
+        sListeners.add(listener);
+    }
+
+    public static boolean screenReaderMode() {
+        if (!sInitialized) updateAccessibilityServices();
+
+        return sScreenReader;
+    }
+
     private static class AnimatorDurationScaleObserver extends ContentObserver {
         public AnimatorDurationScaleObserver(Handler handler) {
             super(handler);
@@ -164,6 +202,14 @@
             getHandler().postDelayed(() -> { updateAccessibilityServices(); }, sNextDelayMillis);
             if (sNextDelayMillis < MAX_DELAY_MILLIS) sNextDelayMillis *= 2;
         }
+
+        boolean oldScreenReader = sScreenReader;
+        sScreenReader = (0 != (sEventTypeMask & SCREEN_READER_EVENT_TYPE_MASK));
+        if (sScreenReader != oldScreenReader) {
+            for (Listener listener : sListeners) {
+                listener.onBrowserAccessibilityStateChanged();
+            }
+        }
     }
 
     static Handler getHandler() {
diff --git a/content/public/android/java/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityImpl.java b/content/public/android/java/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityImpl.java
index 5d64827..e470001 100644
--- a/content/public/android/java/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityImpl.java
+++ b/content/public/android/java/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityImpl.java
@@ -72,7 +72,7 @@
 @JNINamespace("content")
 public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
         implements AccessibilityStateChangeListener, WebContentsAccessibility, WindowEventObserver,
-                   UserData {
+                   UserData, BrowserAccessibilityState.Listener {
     // The following constants have been hard coded so we can support actions newer than our
     // minimum SDK without having to break methods into a series of subclasses.
     // Constants defined by AccessibilityNodeInfo per SDK
@@ -248,6 +248,8 @@
         mDelegate.setOnScrollPositionChangedCallback(
                 () -> handleScrollPositionChanged(mAccessibilityFocusId));
 
+        BrowserAccessibilityState.addListener(this);
+
         // Define our delays on a per event type basis.
         Map<Integer, Integer> eventThrottleDelays = new HashMap<Integer, Integer>();
         eventThrottleDelays.put(
@@ -469,8 +471,9 @@
             onNativeInit();
         }
         if (!isEnabled()) {
+            boolean screenReaderMode = BrowserAccessibilityState.screenReaderMode();
             WebContentsAccessibilityImplJni.get().enable(
-                    mNativeObj, WebContentsAccessibilityImpl.this);
+                    mNativeObj, WebContentsAccessibilityImpl.this, screenReaderMode);
             return null;
         }
         return this;
@@ -566,12 +569,25 @@
     }
 
     // AccessibilityStateChangeListener
+    // TODO(dmazzoni): have BrowserAccessibilityState monitor this and merge
+    // into BrowserAccessibilityStateListener.
 
     @Override
     public void onAccessibilityStateChanged(boolean enabled) {
         setState(enabled);
     }
 
+    // BrowserAccessibilityStateListener
+
+    @Override
+    public void onBrowserAccessibilityStateChanged() {
+        if (!isAccessibilityEnabled()) return;
+
+        boolean screenReaderMode = BrowserAccessibilityState.screenReaderMode();
+        WebContentsAccessibilityImplJni.get().setAXMode(
+                mNativeObj, WebContentsAccessibilityImpl.this, screenReaderMode);
+    }
+
     // WebContentsAccessibility
 
     @Override
@@ -2096,8 +2112,10 @@
                 WebContentsAccessibilityImpl caller, int id);
         boolean isEnabled(
                 long nativeWebContentsAccessibilityAndroid, WebContentsAccessibilityImpl caller);
-        void enable(
-                long nativeWebContentsAccessibilityAndroid, WebContentsAccessibilityImpl caller);
+        void enable(long nativeWebContentsAccessibilityAndroid, WebContentsAccessibilityImpl caller,
+                boolean screenReaderMode);
+        void setAXMode(long nativeWebContentsAccessibilityAndroid,
+                WebContentsAccessibilityImpl caller, boolean screenReaderMode);
         boolean areInlineTextBoxesLoaded(long nativeWebContentsAccessibilityAndroid,
                 WebContentsAccessibilityImpl caller, int id);
         void loadInlineTextBoxes(long nativeWebContentsAccessibilityAndroid,
diff --git a/content/public/browser/content_browser_client.h b/content/public/browser/content_browser_client.h
index 0b1a60b..b482481 100644
--- a/content/public/browser/content_browser_client.h
+++ b/content/public/browser/content_browser_client.h
@@ -1307,8 +1307,7 @@
   // Allows the embedder to register per-scheme URLLoaderFactory
   // implementations to handle service worker main/imported script requests
   // initiated by the browser process for schemes not handled by the Network
-  // Service. Only called for service worker update check when
-  // ServiceWorkerImportedScriptUpdateCheck is enabled.
+  // Service. Only called for service worker update check.
   // The resulting |factories| must be used only by the browser process. The
   // caller must not send any of |factories| to any other process.
   virtual void RegisterNonNetworkServiceWorkerUpdateURLLoaderFactories(
diff --git a/content/public/browser/site_isolation_policy.cc b/content/public/browser/site_isolation_policy.cc
index 375c3ed7..0c3ca3e 100644
--- a/content/public/browser/site_isolation_policy.cc
+++ b/content/public/browser/site_isolation_policy.cc
@@ -181,6 +181,15 @@
 }
 
 // static
+bool SiteIsolationPolicy::ShouldPersistIsolatedCOOPSites() {
+  if (!IsSiteIsolationForCOOPEnabled())
+    return false;
+
+  return features::kSiteIsolationForCrossOriginOpenerPolicyShouldPersistParam
+      .Get();
+}
+
+// static
 std::string SiteIsolationPolicy::GetIsolatedOriginsFromCommandLine() {
   std::string cmdline_arg =
       base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
diff --git a/content/public/browser/site_isolation_policy.h b/content/public/browser/site_isolation_policy.h
index 9c06f9b..e201e25 100644
--- a/content/public/browser/site_isolation_policy.h
+++ b/content/public/browser/site_isolation_policy.h
@@ -60,6 +60,10 @@
   // heuristics for turning on site isolation.
   static bool IsSiteIsolationForCOOPEnabled();
 
+  // Return true if sites that were isolated due to COOP headers should be
+  // persisted across restarts.
+  static bool ShouldPersistIsolatedCOOPSites();
+
   // Applies isolated origins from all available sources, including the
   // command-line switch, field trials, enterprise policy, and the embedder.
   // See also AreIsolatedOriginsEnabled. These origins apply globally to the
diff --git a/content/public/browser/web_contents.h b/content/public/browser/web_contents.h
index 6428522..1fd0d64 100644
--- a/content/public/browser/web_contents.h
+++ b/content/public/browser/web_contents.h
@@ -33,7 +33,7 @@
 #include "third_party/blink/public/mojom/favicon/favicon_url.mojom-forward.h"
 #include "third_party/blink/public/mojom/frame/find_in_page.mojom-forward.h"
 #include "third_party/blink/public/mojom/input/pointer_lock_result.mojom.h"
-#include "third_party/blink/public/mojom/mediastream/media_devices.mojom.h"
+#include "third_party/blink/public/mojom/media/capture_handle_config.mojom-forward.h"
 #include "third_party/perfetto/include/perfetto/tracing/traced_value_forward.h"
 #include "third_party/skia/include/core/SkColor.h"
 #include "ui/accessibility/ax_mode.h"
diff --git a/content/public/browser/web_contents_observer.h b/content/public/browser/web_contents_observer.h
index c87466e..16ff713 100644
--- a/content/public/browser/web_contents_observer.h
+++ b/content/public/browser/web_contents_observer.h
@@ -28,7 +28,7 @@
 #include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
 #include "third_party/blink/public/mojom/favicon/favicon_url.mojom-forward.h"
 #include "third_party/blink/public/mojom/loader/resource_load_info.mojom-forward.h"
-#include "third_party/blink/public/mojom/media/capture_handle_config.mojom.h"
+#include "third_party/blink/public/mojom/media/capture_handle_config.mojom-forward.h"
 #include "third_party/skia/include/core/SkColor.h"
 #include "ui/base/page_transition_types.h"
 #include "ui/base/window_open_disposition.h"
diff --git a/content/public/common/content_features.cc b/content/public/common/content_features.cc
index 3164947..6df11ea 100644
--- a/content/public/common/content_features.cc
+++ b/content/public/common/content_features.cc
@@ -723,6 +723,18 @@
 const base::Feature kSiteIsolationForCrossOriginOpenerPolicy{
     "SiteIsolationForCrossOriginOpenerPolicy",
     base::FEATURE_DISABLED_BY_DEFAULT};
+// This feature param (true by default) controls whether sites are persisted
+// across restarts.
+const base::FeatureParam<bool>
+    kSiteIsolationForCrossOriginOpenerPolicyShouldPersistParam{
+        &kSiteIsolationForCrossOriginOpenerPolicy,
+        "should_persist_across_restarts", true};
+// This feature param controls the maximum size of stored sites.  Only used
+// when persistence is also enabled.
+const base::FeatureParam<int>
+    kSiteIsolationForCrossOriginOpenerPolicyMaxSitesParam{
+        &kSiteIsolationForCrossOriginOpenerPolicy, "stored_sites_max_size",
+        100};
 
 // Controls whether SpareRenderProcessHostManager tries to always have a warm
 // spare renderer process around for the most recently requested BrowserContext.
diff --git a/content/public/common/content_features.h b/content/public/common/content_features.h
index 2450380..3e88614 100644
--- a/content/public/common/content_features.h
+++ b/content/public/common/content_features.h
@@ -165,6 +165,10 @@
 CONTENT_EXPORT extern const base::Feature kSignedHTTPExchangePingValidity;
 CONTENT_EXPORT extern const base::Feature
     kSiteIsolationForCrossOriginOpenerPolicy;
+CONTENT_EXPORT extern const base::FeatureParam<bool>
+    kSiteIsolationForCrossOriginOpenerPolicyShouldPersistParam;
+CONTENT_EXPORT extern const base::FeatureParam<int>
+    kSiteIsolationForCrossOriginOpenerPolicyMaxSitesParam;
 CONTENT_EXPORT extern const base::Feature
     kSkipEarlyCommitPendingForCrashedFrame;
 CONTENT_EXPORT extern const base::Feature kWebOTP;
diff --git a/content/public/test/browser_test_base.cc b/content/public/test/browser_test_base.cc
index f411c6a..83f5ed9c 100644
--- a/content/public/test/browser_test_base.cc
+++ b/content/public/test/browser_test_base.cc
@@ -682,13 +682,13 @@
     discardable_shared_memory_manager.reset();
   }
 
+  // Like in BrowserMainLoop::ShutdownThreadsAndCleanUp(), allow IO during main
+  // thread tear down.
+  base::ThreadRestrictions::SetIOAllowed(true);
+
   base::PostTaskAndroid::SignalNativeSchedulerShutdownForTesting();
   BrowserTaskExecutor::Shutdown();
 
-  // Normally the BrowserMainLoop does this during shutdown but on Android we
-  // don't go through shutdown, so this doesn't happen there. We do need it
-  // for the test harness to be able to delete temp dirs.
-  base::ThreadRestrictions::SetIOAllowed(true);
 #else   // defined(OS_ANDROID)
   auto ui_task = std::make_unique<base::OnceClosure>(base::BindOnce(
       &BrowserTestBase::ProxyRunTestOnMainThreadLoop, base::Unretained(this)));
@@ -697,6 +697,7 @@
       created_main_parts_closure.release();
   EXPECT_EQ(expected_exit_code_, ContentMain(*GetContentMainParams()));
 #endif  // defined(OS_ANDROID)
+
   TearDownInProcessBrowserTestFixture();
 }
 
diff --git a/content/services/auction_worklet/BUILD.gn b/content/services/auction_worklet/BUILD.gn
index 63275a4..0f89dae 100644
--- a/content/services/auction_worklet/BUILD.gn
+++ b/content/services/auction_worklet/BUILD.gn
@@ -8,8 +8,6 @@
   sources = [
     "auction_downloader.cc",
     "auction_downloader.h",
-    "auction_runner.cc",
-    "auction_runner.h",
     "auction_v8_helper.cc",
     "auction_v8_helper.h",
     "auction_worklet_service_impl.cc",
@@ -50,7 +48,6 @@
 
   sources = [
     "auction_downloader_unittest.cc",
-    "auction_runner_unittest.cc",
     "auction_v8_helper_unittest.cc",
     "bidder_worklet_unittest.cc",
     "seller_worklet_unittest.cc",
@@ -70,6 +67,7 @@
     "//services/service_manager/public/cpp/test:test_support",
     "//testing/gmock",
     "//testing/gtest",
+    "//v8",
   ]
 
   if (v8_use_external_startup_data) {
diff --git a/content/services/auction_worklet/auction_runner.cc b/content/services/auction_worklet/auction_runner.cc
deleted file mode 100644
index 85cdc074..0000000
--- a/content/services/auction_worklet/auction_runner.cc
+++ /dev/null
@@ -1,260 +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 "content/services/auction_worklet/auction_runner.h"
-
-#include "base/callback_forward.h"
-#include "base/time/time.h"
-#include "content/services/auction_worklet/auction_v8_helper.h"
-#include "content/services/auction_worklet/bidder_worklet.h"
-#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
-#include "content/services/auction_worklet/seller_worklet.h"
-#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
-#include "third_party/abseil-cpp/absl/types/optional.h"
-#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
-#include "url/gurl.h"
-
-namespace auction_worklet {
-
-AuctionRunner::BidState::BidState() = default;
-AuctionRunner::BidState::~BidState() = default;
-AuctionRunner::BidState::BidState(BidState&&) = default;
-
-void AuctionRunner::CreateAndStart(
-    mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory,
-    blink::mojom::AuctionAdConfigPtr auction_config,
-    std::vector<mojom::BiddingInterestGroupPtr> bidders,
-    mojom::BrowserSignalsPtr browser_signals,
-    mojom::AuctionWorkletService::RunAuctionCallback callback) {
-  AuctionRunner* instance = new AuctionRunner(
-      std::move(url_loader_factory), std::move(auction_config),
-      std::move(bidders), std::move(browser_signals), std::move(callback));
-  instance->StartBidding();
-}
-
-AuctionRunner::AuctionRunner(
-    mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory,
-    blink::mojom::AuctionAdConfigPtr auction_config,
-    std::vector<mojom::BiddingInterestGroupPtr> bidders,
-    mojom::BrowserSignalsPtr browser_signals,
-    mojom::AuctionWorkletService::RunAuctionCallback callback)
-    : url_loader_factory_(std::move(url_loader_factory)),
-      auction_config_(std::move(auction_config)),
-      bidders_(std::move(bidders)),
-      browser_signals_(std::move(browser_signals)),
-      callback_(std::move(callback)) {}
-
-AuctionRunner::~AuctionRunner() {
-  DCHECK(callback_.is_null());
-}
-
-void AuctionRunner::StartBidding() {
-  outstanding_bids_ = bidders_.size();
-  bid_states_.resize(outstanding_bids_);
-
-  for (int bid_index = 0; bid_index < outstanding_bids_; ++bid_index) {
-    const mojom::BiddingInterestGroupPtr& bidder = bidders_[bid_index];
-    BidState* bid_state = &bid_states_[bid_index];
-    bid_state->bidder = bidder.get();
-    // TODO(morlovich): Straight skip if URL is missing.
-    bid_state->bidder_worklet = std::make_unique<BidderWorklet>(
-        &auction_v8_helper_, url_loader_factory_.get(), bidder->Clone(),
-        auction_config_->auction_signals, PerBuyerSignals(bid_state),
-        browser_signals_->top_frame_origin, browser_signals_->seller,
-        auction_start_time_,
-        base::BindOnce(&AuctionRunner::OnGenerateBidComplete,
-                       base::Unretained(this), bid_state));
-  }
-
-  // Also initiate the script fetch for the seller script.
-  seller_worklet_ = std::make_unique<SellerWorklet>(
-      &auction_v8_helper_, url_loader_factory_.get(),
-      auction_config_->decision_logic_url,
-      base::BindOnce(&AuctionRunner::OnSellerWorkletLoaded,
-                     base::Unretained(this)));
-}
-
-void AuctionRunner::OnGenerateBidComplete(
-    BidState* state,
-    absl::optional<BidderWorklet::Bid> bid,
-    const std::vector<std::string>& errors) {
-  DCHECK(!state->bid_generate_complete);
-  DCHECK_GT(outstanding_bids_, 0);
-
-  --outstanding_bids_;
-
-  errors_.insert(errors_.end(), errors.begin(), errors.end());
-  state->bid_generate_complete = true;
-  state->bid_result = std::move(bid);
-
-  if (ReadyToScore())
-    ScoreOne();
-}
-
-void AuctionRunner::OnSellerWorkletLoaded(
-    bool load_result,
-    const std::vector<std::string>& errors) {
-  errors_.insert(errors_.end(), errors.begin(), errors.end());
-
-  if (load_result) {
-    seller_loaded_ = true;
-    if (ReadyToScore())
-      ScoreOne();
-  } else {
-    // Failed to load the seller/auction script --- nothing useful can be done,
-    // so abort, possibly cancelling other fetches, so we don't waste time.
-    FailAuction();
-  }
-}
-
-void AuctionRunner::ScoreOne() {
-  size_t num_bidders = bid_states_.size();
-
-  // Find next valid bid to score, if any.
-  while (seller_considering_ < num_bidders) {
-    BidState* bid_state = &bid_states_[seller_considering_];
-
-    // Skip over bidders that produced no valid bid.
-    if (!bid_state->bid_result) {
-      ++seller_considering_;
-      continue;
-    }
-
-    ScoreBid(bid_state);
-    return;
-  }
-
-  DCHECK_EQ(seller_considering_, num_bidders);
-  CompleteAuction();
-}
-
-void AuctionRunner::ScoreBid(const BidState* state) {
-  seller_worklet_->ScoreAd(
-      state->bid_result->ad, state->bid_result->bid, *auction_config_,
-      browser_signals_->top_frame_origin, state->bidder->group->owner,
-      AdRenderFingerprint(state),
-      state->bid_result->bid_duration.InMilliseconds(),
-      base::BindOnce(&AuctionRunner::OnBidScored, base::Unretained(this)));
-}
-
-void AuctionRunner::OnBidScored(double score,
-                                const std::vector<std::string>& errors) {
-  bid_states_[seller_considering_].seller_score = score;
-  errors_.insert(errors_.end(), errors.begin(), errors.end());
-  ++seller_considering_;
-  ScoreOne();
-}
-
-std::string AuctionRunner::AdRenderFingerprint(const BidState* state) {
-  // TODO(morlovich): "Eventually this fingerprint can be a hash of the ad web
-  // bundle, but while rendering still uses the network, it should just be a
-  // hash of the rendering URL."
-  //
-  return "#####";
-}
-
-absl::optional<std::string> AuctionRunner::PerBuyerSignals(
-    const BidState* state) {
-  if (auction_config_->per_buyer_signals.has_value()) {
-    auto it = auction_config_->per_buyer_signals.value().find(
-        state->bidder->group->owner);
-    if (it != auction_config_->per_buyer_signals.value().end())
-      return it->second;
-  }
-  return absl::nullopt;
-}
-
-void AuctionRunner::CompleteAuction() {
-  double best_bid_score = 0.0;
-  const BidState* best_bid = nullptr;
-  // TODO(morlovich): What if there is a tie?
-  for (const BidState& bid_state : bid_states_) {
-    if (bid_state.seller_score > best_bid_score) {
-      best_bid_score = bid_state.seller_score;
-      best_bid = &bid_state;
-    }
-  }
-
-  if (best_bid) {
-    // Will eventually send a report to the seller and clean up `this`.
-    ReportSellerResult(best_bid);
-  } else {
-    FailAuction();
-  }
-}
-
-void AuctionRunner::ReportSellerResult(const BidState* best_bid) {
-  DCHECK(best_bid->bid_result);
-  DCHECK_GT(best_bid->seller_score, 0);
-  seller_worklet_->ReportResult(
-      *auction_config_, browser_signals_->top_frame_origin,
-      best_bid->bidder->group->owner, best_bid->bid_result->render_url,
-      AdRenderFingerprint(best_bid), best_bid->bid_result->bid,
-      best_bid->seller_score,
-      base::BindOnce(&AuctionRunner::OnReportSellerResultComplete,
-                     base::Unretained(this), best_bid));
-}
-
-void AuctionRunner::OnReportSellerResultComplete(
-    const BidState* best_bid,
-    const absl::optional<std::string>& signals_for_winner,
-    const absl::optional<GURL>& seller_report_url,
-    const std::vector<std::string>& errors) {
-  signals_for_winner_ = signals_for_winner;
-  seller_report_url_ = seller_report_url;
-  errors_.insert(errors_.end(), errors.begin(), errors.end());
-
-  ReportBidWin(best_bid);
-}
-
-void AuctionRunner::ReportBidWin(const BidState* best_bid) {
-  CHECK(best_bid->bid_result);
-  std::string signals_for_winner_arg;
-  // TODO(mmenke): It's unclear what should happen here if `signals_for_winner_`
-  // is null. As-is, an empty string will result in the BidderWorklet's
-  // ReportWin() method failing, since it's not valid JSON.
-  if (signals_for_winner_)
-    signals_for_winner_arg = *signals_for_winner_;
-  best_bid->bidder_worklet->ReportWin(
-      signals_for_winner_arg, best_bid->bid_result->render_url,
-      AdRenderFingerprint(best_bid), best_bid->bid_result->bid,
-      base::BindOnce(&AuctionRunner::OnReportBidWinComplete,
-                     base::Unretained(this), best_bid));
-}
-
-void AuctionRunner::OnReportBidWinComplete(
-    const BidState* best_bid,
-    const absl::optional<GURL>& bidder_report_url,
-    const std::vector<std::string>& errors) {
-  bidder_report_url_ = bidder_report_url;
-  errors_.insert(errors_.end(), errors.begin(), errors.end());
-  ReportSuccess(best_bid);
-}
-
-void AuctionRunner::FailAuction() {
-  std::move(callback_).Run(
-      GURL(), url::Origin(), std::string(),
-      mojom::WinningBidderReport::New(false /* success */, GURL()),
-      mojom::SellerReport::New(false /* success */, "", GURL()), errors_);
-  delete this;
-}
-
-void AuctionRunner::ReportSuccess(const BidState* state) {
-  DCHECK(state->bid_result);
-
-  std::move(callback_).Run(
-      state->bid_result->render_url, state->bidder->group->owner,
-      state->bidder->group->name,
-      mojom::WinningBidderReport::New(
-          bidder_report_url_.has_value(),
-          bidder_report_url_.has_value() ? *bidder_report_url_ : GURL()),
-      mojom::SellerReport::New(
-          signals_for_winner_.has_value(),
-          "<TODO: Remove this. Currently ignored>",
-          seller_report_url_.has_value() ? *seller_report_url_ : GURL()),
-      errors_);
-  delete this;
-}
-
-}  // namespace auction_worklet
diff --git a/content/services/auction_worklet/auction_runner.h b/content/services/auction_worklet/auction_runner.h
deleted file mode 100644
index d164bda..0000000
--- a/content/services/auction_worklet/auction_runner.h
+++ /dev/null
@@ -1,161 +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 CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_RUNNER_H_
-#define CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_RUNNER_H_
-
-#include <string>
-#include <vector>
-
-#include "base/callback_forward.h"
-#include "base/logging.h"
-#include "base/time/time.h"
-#include "content/services/auction_worklet/auction_v8_helper.h"
-#include "content/services/auction_worklet/bidder_worklet.h"
-#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
-#include "content/services/auction_worklet/seller_worklet.h"
-#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
-#include "third_party/abseil-cpp/absl/types/optional.h"
-#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
-#include "url/gurl.h"
-
-namespace auction_worklet {
-
-// An AuctionRunner loads and runs the bidder and seller worklets, along with
-// their reporting phases and produces the result via a callback.
-//
-// This is a self-owned object. It destroys itself after invoking the callback
-// passed to CreateAndStart.
-//
-// At present it initiates all fetches in parallel, running all bidder scripts
-// once they and any trusted signals they need are ready, then when all bids are
-// in runs all the scoring, and finally the reporting worklets.
-//
-// TODO(morlovich): There is no need to wait for all bidders to finish to start
-// scoring.
-class AuctionRunner {
- public:
-  explicit AuctionRunner(const AuctionRunner&) = delete;
-  AuctionRunner& operator=(const AuctionRunner&) = delete;
-
-  static void CreateAndStart(
-      mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory,
-      blink::mojom::AuctionAdConfigPtr auction_config,
-      std::vector<mojom::BiddingInterestGroupPtr> bidders,
-      mojom::BrowserSignalsPtr browser_signals,
-      mojom::AuctionWorkletService::RunAuctionCallback callback);
-
- private:
-  struct BidState {
-    BidState();
-    BidState(BidState&&);
-    ~BidState();
-
-    mojom::BiddingInterestGroup* bidder = nullptr;
-
-    // true if the generateBid() callback passed to the BidderWorklet's
-    // constructor has been invoked. This may indicated either successful
-    // generation of a bid, or failure to load or run the script.
-    bool bid_generate_complete = false;
-
-    std::unique_ptr<BidderWorklet> bidder_worklet;
-    absl::optional<BidderWorklet::Bid> bid_result;
-    double seller_score = 0;
-  };
-
-  AuctionRunner(
-      mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory,
-      blink::mojom::AuctionAdConfigPtr auction_config,
-      std::vector<mojom::BiddingInterestGroupPtr> bidders,
-      mojom::BrowserSignalsPtr browser_signals,
-      mojom::AuctionWorkletService::RunAuctionCallback callback);
-  ~AuctionRunner();
-
-  void StartBidding();
-  void OnGenerateBidComplete(BidState* state,
-                             absl::optional<BidderWorklet::Bid> bid,
-                             const std::vector<std::string>& errors);
-
-  // True if all bid results and the seller script load are complete.
-  bool ReadyToScore() const { return outstanding_bids_ == 0 && seller_loaded_; }
-  void OnSellerWorkletLoaded(bool load_result,
-                             const std::vector<std::string>& errors);
-
-  // Calls into the seller to asynchronously each of outstanding bids, in
-  // series. Once there are no outstanding bids, proceeds to selecting the
-  // winner and running the Worklets reporting methods.
-  //
-  // Destroys `this` (indirectly), upon wrapping up the auction if all bids have
-  // been scored (including if there were none).
-  void ScoreOne();
-  void ScoreBid(const BidState* state);
-  // Callback from ScoreBid().
-  void OnBidScored(double score, const std::vector<std::string>& errors);
-
-  std::string AdRenderFingerprint(const BidState* state);
-  absl::optional<std::string> PerBuyerSignals(const BidState* state);
-
-  void CompleteAuction();  // Indirectly deletes `this`, if there's no winner.
-
-  // Sequence of asynchronous methods to call into the bidder/seller results to
-  // report a a win, Will ultimately invoke ReportSuccess(), which will delete
-  // the auction.
-  void ReportSellerResult(const BidState* state);
-  void OnReportSellerResultComplete(
-      const BidState* best_bid,
-      const absl::optional<std::string>& signals_for_winner,
-      const absl::optional<GURL>& seller_report_url,
-      const std::vector<std::string>& errors);
-  void ReportBidWin(const BidState* state);
-  void OnReportBidWinComplete(const BidState* best_bid,
-                              const absl::optional<GURL>& bidder_report_url,
-                              const std::vector<std::string>& error_msgs);
-
-  // Destroys `this`.
-  void FailAuction();
-
-  // Destroys `this`.
-  void ReportSuccess(const BidState* state);
-
-  // `auction_v8_helper_` needs to be before the worklets, since they refer to
-  // it.
-  AuctionV8Helper auction_v8_helper_;
-
-  // Configuration.
-  mojo::Remote<network::mojom::URLLoaderFactory> url_loader_factory_;
-  blink::mojom::AuctionAdConfigPtr auction_config_;
-  std::vector<mojom::BiddingInterestGroupPtr> bidders_;
-  mojom::BrowserSignalsPtr browser_signals_;
-  mojom::AuctionWorkletService::RunAuctionCallback callback_;
-
-  // State for the bidding phase.
-  int outstanding_bids_;  // number of bids for which we're waiting on a fetch.
-  std::vector<BidState> bid_states_;  // parallel to `bidders_`.
-  // The time the auction started. Use a single base time for all Worklets, to
-  // present a more consistent view of the universe.
-  const base::Time auction_start_time_ = base::Time::Now();
-
-  // State for the scoring phase.
-  std::unique_ptr<SellerWorklet> seller_worklet_;
-
-  // This is true if the seller script has been loaded successfully --- if the
-  // load failed, the entire process is aborted since there is nothing useful
-  // that can be done.
-  bool seller_loaded_ = false;
-  size_t seller_considering_ = 0;
-
-  // Seller script reportResult() results.
-  absl::optional<std::string> signals_for_winner_;
-  absl::optional<GURL> seller_report_url_;
-
-  // Bidder script reportWin() results.
-  absl::optional<GURL> bidder_report_url_;
-
-  // All errors reported by worklets thus far.
-  std::vector<std::string> errors_;
-};
-
-}  // namespace auction_worklet
-
-#endif  // CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_RUNNER_H_
diff --git a/content/services/auction_worklet/auction_runner_unittest.cc b/content/services/auction_worklet/auction_runner_unittest.cc
deleted file mode 100644
index cb62811..0000000
--- a/content/services/auction_worklet/auction_runner_unittest.cc
+++ /dev/null
@@ -1,750 +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 "content/services/auction_worklet/auction_runner.h"
-
-#include <vector>
-
-#include "base/run_loop.h"
-#include "base/strings/stringprintf.h"
-#include "base/test/bind.h"
-#include "base/test/task_environment.h"
-#include "base/time/time.h"
-#include "content/services/auction_worklet/worklet_test_util.h"
-#include "net/http/http_status_code.h"
-#include "services/network/test/test_url_loader_factory.h"
-#include "testing/gmock/include/gmock/gmock-matchers.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace auction_worklet {
-namespace {
-
-std::string MakeBidScript(const std::string& bid,
-                          const std::string& render_url,
-                          const url::Origin& interest_group_owner,
-                          const std::string& interest_group_name,
-                          bool has_signals,
-                          const std::string& signal_key,
-                          const std::string& signal_val) {
-  // TODO(morlovich): Use JsReplace.
-  constexpr char kBidScript[] = R"(
-    const bid = %s;
-    const renderUrl = "%s";
-    const interestGroupOwner = "%s";
-    const interestGroupName = "%s";
-    const hasSignals = %s;
-
-    function generateBid(interestGroup, auctionSignals, perBuyerSignals,
-                          trustedBiddingSignals, browserSignals) {
-      var result = {ad: {bidKey: "data for " + bid,
-                         groupName: interestGroupName},
-                    bid: bid, render: renderUrl};
-      if (interestGroup.name !== interestGroupName)
-        throw new Error("wrong interestGroupName");
-      if (interestGroup.owner !== interestGroupOwner)
-        throw new Error("wrong interestGroupOwner");
-      if (perBuyerSignals.signalsFor !== interestGroupName)
-        throw new Error("wrong perBuyerSignals");
-      if (!auctionSignals.isAuctionSignals)
-        throw new Error("wrong auctionSignals");
-      if (hasSignals) {
-        if ('extra' in trustedBiddingSignals)
-          throw new Error("why extra?");
-        if (trustedBiddingSignals["%s"] !== "%s")
-          throw new Error("wrong signals");
-      } else if (trustedBiddingSignals !== null) {
-        throw new Error("Expected null trustedBiddingSignals");
-      }
-      if (browserSignals.topWindowHostname !== 'publisher1.com')
-        throw new Error("wrong topWindowHostname");
-      if (browserSignals.seller != 'https://adstuff.publisher1.com')
-         throw new Error("wrong seller");
-      if (browserSignals.joinCount !== 3)
-        throw new Error("joinCount")
-      if (browserSignals.bidCount !== 5)
-        throw new Error("bidCount");
-      if (browserSignals.prevWins.length !== 3)
-        throw new Error("prevWins");
-      for (let i = 0; i < browserSignals.prevWins.length; ++i) {
-        if (!(browserSignals.prevWins[i] instanceof Array))
-          throw new Error("prevWins entry not an array");
-        if (typeof browserSignals.prevWins[i][0] != "number")
-          throw new Error("Not a Number in prevWin?");
-        if (browserSignals.prevWins[i][1].winner !== -i)
-          throw new Error("prevWin MD not what passed in");
-      }
-      return result;
-    }
-
-    function reportWin(auctionSignals, perBuyerSignals, sellerSignals,
-                       browserSignals) {
-      if (!auctionSignals.isAuctionSignals)
-        throw new Error("wrong auctionSignals");
-      if (perBuyerSignals.signalsFor !== interestGroupName)
-        throw new Error("wrong perBuyerSignals");
-
-      // sellerSignals in these tests is just sellers' browserSignals, since
-      // that's what reportResult passes through.
-      if (sellerSignals.topWindowHostname !== 'publisher1.com')
-        throw new Error("wrong topWindowHostname");
-      if (sellerSignals.interestGroupOwner !== interestGroupOwner)
-        throw new Error("wrong interestGroupOwner");
-      if (sellerSignals.renderUrl !== renderUrl)
-        throw new Error("wrong renderUrl");
-      if (sellerSignals.bid !== bid)
-        throw new Error("wrong bid");
-      if (sellerSignals.desirability !== (bid * 2))
-        throw new Error("wrong desirability");
-
-      if (browserSignals.topWindowHostname !== 'publisher1.com')
-        throw new Error("wrong browserSignals.topWindowHostname");
-      if ("desirability" in browserSignals)
-        throw new Error("why is desirability here?");
-      if (browserSignals.interestGroupName !== interestGroupName)
-        throw new Error("wrong browserSignals.interestGroupName");
-      if (browserSignals.interestGroupOwner !== interestGroupOwner)
-        throw new Error("wrong browserSignals.interestGroupOwner");
-
-      if (browserSignals.renderUrl !== renderUrl)
-        throw new Error("wrong browserSignals.renderUrl");
-      if (browserSignals.bid !== bid)
-        throw new Error("wrong browserSignals.bid");
-
-      sendReportTo("https://buyer-reporting.example.com");
-    }
-  )";
-  return base::StringPrintf(
-      kBidScript, bid.c_str(), render_url.c_str(),
-      interest_group_owner.Serialize().c_str(), interest_group_name.c_str(),
-      has_signals ? "true" : "false", signal_key.c_str(), signal_val.c_str());
-}
-
-// This can be appended to the standard script to override the function.
-constexpr char kReportWinNoUrl[] = R"(
-  function reportWin(auctionSignals, perBuyerSignals, sellerSignals,
-                      browserSignals) {
-  }
-)";
-
-constexpr char kCheckingAuctionScript[] = R"(
-  function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals,
-      browserSignals) {
-    if (adMetadata.bidKey !== ("data for " + bid)) {
-      throw new Error("wrong data for bid:" +
-                      JSON.stringify(adMetadata) + "/" + bid);
-    }
-    if (auctionConfig.decisionLogicUrl
-        !== "https://adstuff.publisher1.com/auction.js") {
-      throw new Error("wrong auctionConfig");
-    }
-    if (auctionConfig.perBuyerSignals['adplatform.com'].signalsFor
-        !== 'Ad Platform') {
-      throw new Error("Wrong perBuyerSignals in auctionConfig");
-    }
-    if (!auctionConfig.sellerSignals.isSellerSignals)
-      throw new Error("Wrong sellerSignals");
-    if (browserSignals.topWindowHostname !== 'publisher1.com')
-      throw new Error("wrong topWindowHostname");
-    if ("joinCount" in browserSignals)
-      throw new Error("wrong kind of browser signals");
-    if (browserSignals.adRenderFingerprint !== "#####")
-      throw new Error("wrong adRenderFingerprint");
-    if (typeof browserSignals.biddingDurationMsec !== "number")
-      throw new Error("biddingDurationMsec is not a number. huh");
-    if (browserSignals.biddingDurationMsec < 0)
-      throw new Error("biddingDurationMsec should be non-negative.");
-
-    return bid * 2;
-  }
-)";
-
-constexpr char kReportResultScript[] = R"(
-  function reportResult(auctionConfig, browserSignals) {
-    if (auctionConfig.decisionLogicUrl
-        !== "https://adstuff.publisher1.com/auction.js") {
-      throw new Error("wrong auctionConfig");
-    }
-    if (browserSignals.topWindowHostname !== 'publisher1.com')
-      throw new Error("wrong topWindowHostname");
-    sendReportTo("https://reporting.example.com");
-    return browserSignals;
-  }
-)";
-
-constexpr char kReportResultScriptNoUrl[] = R"(
-  function reportResult(auctionConfig, browserSignals) {
-    return browserSignals;
-  }
-)";
-
-std::string MakeAuctionScript() {
-  return std::string(kCheckingAuctionScript) + kReportResultScript;
-}
-
-std::string MakeAuctionScriptNoReportUrl() {
-  return std::string(kCheckingAuctionScript) + kReportResultScriptNoUrl;
-}
-
-const char kAuctionScriptRejects2[] = R"(
-  function scoreAd(adMetadata, bid, auctionConfig, browserSignals) {
-    if (bid === 2)
-      return -1;
-    return bid + 1;
-  }
-)";
-
-std::string MakeAuctionScriptReject2() {
-  return std::string(kAuctionScriptRejects2) + kReportResultScript;
-}
-
-class AuctionRunnerTest : public testing::Test {
- protected:
-  struct Result {
-    GURL ad_url;
-    url::Origin interest_group_owner;
-    std::string interest_group_name;
-    mojom::WinningBidderReportPtr bidder_report;
-    mojom::SellerReportPtr seller_report;
-    std::vector<std::string> errors;
-  };
-
-  Result RunAuctionAndWait(const GURL& seller_decision_logic_url,
-                           std::vector<mojom::BiddingInterestGroupPtr> bidders,
-                           const std::string& auction_signals_json,
-                           mojom::BrowserSignalsPtr browser_signals) {
-    mojo::Receiver<network::mojom::URLLoaderFactory> factory_receiver{
-        &url_loader_factory_};
-
-    blink::mojom::AuctionAdConfigPtr auction_config =
-        blink::mojom::AuctionAdConfig::New();
-    auction_config->seller = url::Origin::Create(seller_decision_logic_url);
-    auction_config->decision_logic_url = seller_decision_logic_url;
-    auction_config->interest_group_buyers =
-        blink::mojom::InterestGroupBuyers::NewAllBuyers(
-            blink::mojom::AllBuyers::New());
-    auction_config->auction_signals = auction_signals_json;
-    auction_config->seller_signals = R"({"isSellerSignals": true})";
-
-    base::flat_map<url::Origin, std::string> per_buyer_signals;
-    per_buyer_signals[kBidder1] = R"({"signalsFor": ")" + kBidder1Name + "\"}";
-    per_buyer_signals[kBidder2] = R"({"signalsFor": ")" + kBidder2Name + "\"}";
-    auction_config->per_buyer_signals = std::move(per_buyer_signals);
-
-    base::RunLoop run_loop;
-    Result result;
-    AuctionRunner::CreateAndStart(
-        factory_receiver.BindNewPipeAndPassRemote(), std::move(auction_config),
-        std::move(bidders), std::move(browser_signals),
-        base::BindLambdaForTesting([&](const GURL& ad_url,
-                                       const url::Origin& interest_group_owner,
-                                       const std::string& interest_group_name,
-                                       mojom::WinningBidderReportPtr bid_report,
-                                       mojom::SellerReportPtr seller_report,
-                                       const std::vector<std::string>& errors) {
-          result.ad_url = ad_url;
-          result.interest_group_owner = interest_group_owner;
-          result.interest_group_name = interest_group_name;
-          result.bidder_report = std::move(bid_report);
-          result.seller_report = std::move(seller_report);
-          result.errors = errors;
-          run_loop.Quit();
-        }));
-    run_loop.Run();
-    return result;
-  }
-
-  mojom::BiddingInterestGroupPtr MakeInterestGroup(
-      const url::Origin& owner,
-      const std::string& name,
-      const GURL& bidding_url,
-      const absl::optional<GURL>& trusted_bidding_signals_url,
-      const std::vector<std::string>& trusted_bidding_signals_keys,
-      const GURL& ad_url) {
-    std::vector<blink::mojom::InterestGroupAdPtr> ads;
-    ads.push_back(
-        blink::mojom::InterestGroupAd::New(ad_url, R"({"ads": true})"));
-
-    std::vector<mojom::PreviousWinPtr> previous_wins;
-    previous_wins.push_back(
-        mojom::PreviousWin::New(base::Time::Now(), R"({"winner": 0})"));
-    previous_wins.push_back(
-        mojom::PreviousWin::New(base::Time::Now(), R"({"winner": -1})"));
-    previous_wins.push_back(
-        mojom::PreviousWin::New(base::Time::Now(), R"({"winner": -2})"));
-
-    return mojom::BiddingInterestGroup::New(
-        blink::mojom::InterestGroup::New(
-            base::Time::Max(), owner, name, bidding_url,
-            GURL() /* update_url */, trusted_bidding_signals_url,
-            trusted_bidding_signals_keys, absl::nullopt, std::move(ads)),
-        mojom::BiddingBrowserSignals::New(3, 5, std::move(previous_wins)));
-  }
-
-  Result RunStandardAuction() {
-    std::vector<mojom::BiddingInterestGroupPtr> bidders;
-    bidders.push_back(MakeInterestGroup(kBidder1, kBidder1Name, kBidder1Url,
-                                        kTrustedSignalsUrl, {"k1", "k2"},
-                                        GURL("https://ad1.com")));
-    bidders.push_back(MakeInterestGroup(kBidder2, kBidder2Name, kBidder2Url,
-                                        kTrustedSignalsUrl, {"l1", "l2"},
-                                        GURL("https://ad2.com")));
-
-    return RunAuctionAndWait(
-        kSellerUrl, std::move(bidders),
-        R"({"isAuctionSignals": true})", /* auction_signals_json */
-        mojom::BrowserSignals::New(
-            url::Origin::Create(GURL("https://publisher1.com")),
-            url::Origin::Create(kSellerUrl)));
-  }
-
-  const GURL kSellerUrl{"https://adstuff.publisher1.com/auction.js"};
-  const GURL kBidder1Url{"https://adplatform.com/offers.js"};
-  const url::Origin kBidder1 =
-      url::Origin::Create(GURL("https://adplatform.com"));
-  const std::string kBidder1Name{"Ad Platform"};
-  const GURL kBidder2Url{"https://anotheradthing.com/bids.js"};
-  const url::Origin kBidder2 =
-      url::Origin::Create(GURL("https://anotheradthing.com"));
-  const std::string kBidder2Name{"Another Ad Thing"};
-
-  const GURL kTrustedSignalsUrl{"https://trustedsignaller.org/signals"};
-
-  base::test::TaskEnvironment task_environment_;
-  network::TestURLLoaderFactory url_loader_factory_;
-};
-
-// An auction with two successful bids.
-TEST_F(AuctionRunnerTest, Basic) {
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder1Url,
-      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
-                    true /* has_signals */, "k1", "a"));
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder2Url,
-      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
-                    true /* has_signals */, "l2", "b"));
-  AddJavascriptResponse(&url_loader_factory_, kSellerUrl, MakeAuctionScript());
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
-      R"({"k1":"a", "k2": "b", "extra": "c"})");
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
-      R"({"l1":"a", "l2": "b", "extra": "c"})");
-
-  Result res = RunStandardAuction();
-  EXPECT_EQ("https://ad2.com/", res.ad_url.spec());
-  EXPECT_EQ("https://anotheradthing.com", res.interest_group_owner.Serialize());
-  EXPECT_EQ("Another Ad Thing", res.interest_group_name);
-  EXPECT_TRUE(res.seller_report->success);
-  EXPECT_EQ("https://reporting.example.com/",
-            res.seller_report->report_url.spec());
-  EXPECT_TRUE(res.bidder_report->report_requested);
-  EXPECT_EQ("https://buyer-reporting.example.com/",
-            res.bidder_report->report_url.spec());
-  EXPECT_THAT(res.errors, testing::ElementsAre());
-}
-
-// An auction where one bid is successful, another's script 404s.
-TEST_F(AuctionRunnerTest, OneBidOne404) {
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder1Url,
-      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
-                    true /* has_signals */, "k1", "a"));
-  url_loader_factory_.AddResponse(kBidder2Url.spec(), "", net::HTTP_NOT_FOUND);
-  AddJavascriptResponse(&url_loader_factory_, kSellerUrl, MakeAuctionScript());
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
-      R"({"k1":"a", "k2": "b", "extra": "c"})");
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
-      R"({"l1":"a", "l2": "b", "extra": "c"})");
-
-  Result res = RunStandardAuction();
-  EXPECT_EQ("https://ad1.com/", res.ad_url.spec());
-  EXPECT_EQ("https://adplatform.com", res.interest_group_owner.Serialize());
-  EXPECT_EQ("Ad Platform", res.interest_group_name);
-  EXPECT_TRUE(res.seller_report->success);
-  EXPECT_EQ("https://reporting.example.com/",
-            res.seller_report->report_url.spec());
-  EXPECT_TRUE(res.bidder_report->report_requested);
-  EXPECT_EQ("https://buyer-reporting.example.com/",
-            res.bidder_report->report_url.spec());
-  EXPECT_THAT(
-      res.errors,
-      testing::ElementsAre("Failed to load https://anotheradthing.com/bids.js "
-                           "HTTP status = 404 Not Found."));
-}
-
-// An auction where one bid is successful, another's script does not provide a
-// bidding function.
-TEST_F(AuctionRunnerTest, OneBidOneNotMade) {
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder1Url,
-      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
-                    true /* has_signals */, "k1", "a"));
-
-  // The auction script doesn't make any bids.
-  AddJavascriptResponse(&url_loader_factory_, kBidder2Url, MakeAuctionScript());
-  AddJavascriptResponse(&url_loader_factory_, kSellerUrl, MakeAuctionScript());
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
-      R"({"k1":"a", "k2": "b", "extra": "c"})");
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
-      R"({"l1":"a", "l2": "b", "extra": "c"})");
-
-  Result res = RunStandardAuction();
-  EXPECT_EQ("https://ad1.com/", res.ad_url.spec());
-  EXPECT_EQ("https://adplatform.com", res.interest_group_owner.Serialize());
-  EXPECT_EQ("Ad Platform", res.interest_group_name);
-  EXPECT_TRUE(res.seller_report->success);
-  EXPECT_EQ("https://reporting.example.com/",
-            res.seller_report->report_url.spec());
-  EXPECT_TRUE(res.bidder_report->report_requested);
-  EXPECT_EQ("https://buyer-reporting.example.com/",
-            res.bidder_report->report_url.spec());
-  EXPECT_THAT(res.errors,
-              testing::ElementsAre("https://anotheradthing.com/bids.js "
-                                   "`generateBid` is not a function."));
-}
-
-// An auction where no bidding scripts load successfully.
-TEST_F(AuctionRunnerTest, NoBids) {
-  url_loader_factory_.AddResponse(kBidder1Url.spec(), "", net::HTTP_NOT_FOUND);
-  url_loader_factory_.AddResponse(kBidder2Url.spec(), "", net::HTTP_NOT_FOUND);
-  AddJavascriptResponse(&url_loader_factory_, kSellerUrl, MakeAuctionScript());
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
-      R"({"k1":"a", "k2":"b", "extra":"c"})");
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
-      R"({"l1":"a", "l2": "b", "extra":"c"})");
-
-  Result res = RunStandardAuction();
-  EXPECT_TRUE(res.ad_url.is_empty());
-  EXPECT_TRUE(res.interest_group_owner.opaque());
-  EXPECT_EQ("", res.interest_group_name);
-  EXPECT_FALSE(res.seller_report->success);
-  EXPECT_TRUE(res.seller_report->report_url.is_empty());
-  EXPECT_FALSE(res.bidder_report->report_requested);
-  EXPECT_TRUE(res.bidder_report->report_url.is_empty());
-  EXPECT_THAT(
-      res.errors,
-      testing::ElementsAre("Failed to load https://adplatform.com/offers.js "
-                           "HTTP status = 404 Not Found.",
-                           "Failed to load https://anotheradthing.com/bids.js "
-                           "HTTP status = 404 Not Found."));
-}
-
-// An auction where none of the bidding scripts has a valid bidding function.
-TEST_F(AuctionRunnerTest, NoBidMadeByScript) {
-  // MakeAuctionScript() is a valid script that doesn't have a bidding function.
-  AddJavascriptResponse(&url_loader_factory_, kBidder1Url, MakeAuctionScript());
-  AddJavascriptResponse(&url_loader_factory_, kBidder2Url, MakeAuctionScript());
-  AddJavascriptResponse(&url_loader_factory_, kSellerUrl, MakeAuctionScript());
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
-      R"({"k1":"a", "k2":"b", "extra":"c"})");
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
-      R"({"l1":"a", "l2": "b", "extra":"c"})");
-
-  Result res = RunStandardAuction();
-  EXPECT_TRUE(res.ad_url.is_empty());
-  EXPECT_TRUE(res.interest_group_owner.opaque());
-  EXPECT_EQ("", res.interest_group_name);
-  EXPECT_FALSE(res.seller_report->success);
-  EXPECT_TRUE(res.seller_report->report_url.is_empty());
-  EXPECT_FALSE(res.bidder_report->report_requested);
-  EXPECT_TRUE(res.bidder_report->report_url.is_empty());
-  EXPECT_THAT(
-      res.errors,
-      testing::ElementsAre(
-          "https://adplatform.com/offers.js `generateBid` is not a function.",
-          "https://anotheradthing.com/bids.js `generateBid` is not a "
-          "function."));
-}
-
-// An auction where the seller script doesn't have a scoring function.
-TEST_F(AuctionRunnerTest, SellerRejectsAll) {
-  std::string bid_script1 =
-      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
-                    true /* has_signals */, "k1", "a");
-  AddJavascriptResponse(&url_loader_factory_, kBidder1Url, bid_script1);
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder2Url,
-      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
-                    true /* has_signals */, "l2", "b"));
-
-  // No seller scoring function in a bid script.
-  AddJavascriptResponse(&url_loader_factory_, kSellerUrl, bid_script1);
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
-      R"({"k1":"a", "k2":"b", "extra":"c"})");
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
-      R"({"l1":"a", "l2": "b", "extra":"c"})");
-
-  Result res = RunStandardAuction();
-  EXPECT_TRUE(res.ad_url.is_empty());
-  EXPECT_TRUE(res.interest_group_owner.opaque());
-  EXPECT_EQ("", res.interest_group_name);
-  EXPECT_FALSE(res.seller_report->success);
-  EXPECT_TRUE(res.seller_report->report_url.is_empty());
-  EXPECT_FALSE(res.bidder_report->report_requested);
-  EXPECT_TRUE(res.bidder_report->report_url.is_empty());
-  EXPECT_THAT(res.errors,
-              testing::ElementsAre("https://adstuff.publisher1.com/auction.js "
-                                   "`scoreAd` is not a function.",
-                                   "https://adstuff.publisher1.com/auction.js "
-                                   "`scoreAd` is not a function."));
-}
-
-// An auction where seller rejects one bid when scoring.
-TEST_F(AuctionRunnerTest, SellerRejectsOne) {
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder1Url,
-      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
-                    true /* has_signals */, "k1", "a"));
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder2Url,
-      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
-                    true /* has_signals */, "l2", "b"));
-  AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
-                        MakeAuctionScriptReject2());
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
-      R"({"k1":"a", "k2": "b", "extra": "c"})");
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
-      R"({"l1":"a", "l2": "b", "extra": "c"})");
-
-  Result res = RunStandardAuction();
-  EXPECT_EQ("https://ad1.com/", res.ad_url.spec());
-  EXPECT_EQ("https://adplatform.com", res.interest_group_owner.Serialize());
-  EXPECT_EQ("Ad Platform", res.interest_group_name);
-  EXPECT_TRUE(res.seller_report->success);
-  EXPECT_EQ("https://reporting.example.com/",
-            res.seller_report->report_url.spec());
-  EXPECT_TRUE(res.bidder_report->report_requested);
-  EXPECT_EQ("https://buyer-reporting.example.com/",
-            res.bidder_report->report_url.spec());
-}
-
-// An auction where the seller script fails to load.
-TEST_F(AuctionRunnerTest, NoSellerScript) {
-  // Tests to make sure that if seller script fails the other fetches are
-  // cancelled, too.
-  url_loader_factory_.AddResponse(kSellerUrl.spec(), "", net::HTTP_NOT_FOUND);
-  Result res = RunStandardAuction();
-  EXPECT_TRUE(res.ad_url.is_empty());
-  EXPECT_TRUE(res.interest_group_owner.opaque());
-  EXPECT_EQ("", res.interest_group_name);
-  EXPECT_FALSE(res.seller_report->success);
-  EXPECT_TRUE(res.seller_report->report_url.is_empty());
-  EXPECT_FALSE(res.bidder_report->report_requested);
-  EXPECT_TRUE(res.bidder_report->report_url.is_empty());
-
-  EXPECT_EQ(0, url_loader_factory_.NumPending());
-  EXPECT_THAT(res.errors,
-              testing::ElementsAre(
-                  "Failed to load https://adstuff.publisher1.com/auction.js "
-                  "HTTP status = 404 Not Found."));
-}
-
-// An auction where bidders don't requested trusted bidding signals.
-TEST_F(AuctionRunnerTest, NoTrustedBiddingSignals) {
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder1Url,
-      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
-                    false /* has_signals */, "k1", "a"));
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder2Url,
-      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
-                    false /* has_signals */, "l2", "b"));
-  AddJavascriptResponse(&url_loader_factory_, kSellerUrl, MakeAuctionScript());
-
-  std::vector<mojom::BiddingInterestGroupPtr> bidders;
-  bidders.push_back(MakeInterestGroup(kBidder1, kBidder1Name, kBidder1Url,
-                                      absl::nullopt, {"k1", "k2"},
-                                      GURL("https://ad1.com")));
-  bidders.push_back(MakeInterestGroup(kBidder2, kBidder2Name, kBidder2Url,
-                                      absl::nullopt, {"l1", "l2"},
-                                      GURL("https://ad2.com")));
-
-  Result res = RunAuctionAndWait(
-      kSellerUrl, std::move(bidders),
-      R"({"isAuctionSignals": true})", /* auction_signals_json */
-      mojom::BrowserSignals::New(
-          url::Origin::Create(GURL("https://publisher1.com")),
-          url::Origin::Create(kSellerUrl)));
-
-  EXPECT_EQ("https://ad2.com/", res.ad_url.spec());
-  EXPECT_EQ("https://anotheradthing.com", res.interest_group_owner.Serialize());
-  EXPECT_EQ("Another Ad Thing", res.interest_group_name);
-  EXPECT_TRUE(res.seller_report->success);
-  EXPECT_EQ("https://reporting.example.com/",
-            res.seller_report->report_url.spec());
-  EXPECT_TRUE(res.bidder_report->report_requested);
-  EXPECT_EQ("https://buyer-reporting.example.com/",
-            res.bidder_report->report_url.spec());
-  EXPECT_THAT(res.errors, testing::ElementsAre());
-}
-
-// An auction where trusted bidding signals are requested, but the fetch 404s.
-TEST_F(AuctionRunnerTest, TrustedBiddingSignals404) {
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder1Url,
-      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
-                    false /* has_signals */, "k1", "a"));
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder2Url,
-      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
-                    false /* has_signals */, "l2", "b"));
-  url_loader_factory_.AddResponse(
-      kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2", "",
-      net::HTTP_NOT_FOUND);
-  url_loader_factory_.AddResponse(
-      kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2", "",
-      net::HTTP_NOT_FOUND);
-  AddJavascriptResponse(&url_loader_factory_, kSellerUrl, MakeAuctionScript());
-
-  Result res = RunStandardAuction();
-  EXPECT_EQ("https://ad2.com/", res.ad_url.spec());
-  EXPECT_EQ("https://anotheradthing.com", res.interest_group_owner.Serialize());
-  EXPECT_EQ("Another Ad Thing", res.interest_group_name);
-  EXPECT_TRUE(res.seller_report->success);
-  EXPECT_EQ("https://reporting.example.com/",
-            res.seller_report->report_url.spec());
-  EXPECT_TRUE(res.bidder_report->report_requested);
-  EXPECT_EQ("https://buyer-reporting.example.com/",
-            res.bidder_report->report_url.spec());
-  EXPECT_THAT(res.errors,
-              testing::ElementsAre("Failed to load "
-                                   "https://trustedsignaller.org/"
-                                   "signals?hostname=publisher1.com&keys=k1,k2 "
-                                   "HTTP status = 404 Not Found.",
-                                   "Failed to load "
-                                   "https://trustedsignaller.org/"
-                                   "signals?hostname=publisher1.com&keys=l1,l2 "
-                                   "HTTP status = 404 Not Found."));
-}
-
-// A successful auction where seller reporting worklet doesn't set a URL.
-TEST_F(AuctionRunnerTest, NoReportResultUrl) {
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder1Url,
-      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
-                    true /* has_signals */, "k1", "a"));
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder2Url,
-      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
-                    true /* has_signals */, "l2", "b"));
-  AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
-                        MakeAuctionScriptNoReportUrl());
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
-      R"({"k1":"a", "k2": "b", "extra": "c"})");
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
-      R"({"l1":"a", "l2": "b", "extra": "c"})");
-
-  Result res = RunStandardAuction();
-  EXPECT_EQ("https://ad2.com/", res.ad_url.spec());
-  EXPECT_EQ("https://anotheradthing.com", res.interest_group_owner.Serialize());
-  EXPECT_EQ("Another Ad Thing", res.interest_group_name);
-  EXPECT_TRUE(res.seller_report->success);
-  EXPECT_TRUE(res.seller_report->report_url.is_empty());
-  EXPECT_TRUE(res.bidder_report->report_requested);
-  EXPECT_EQ("https://buyer-reporting.example.com/",
-            res.bidder_report->report_url.spec());
-  EXPECT_THAT(res.errors, testing::ElementsAre());
-}
-
-// A successful auction where bidder reporting worklet doesn't set a URL.
-TEST_F(AuctionRunnerTest, NoReportWinUrl) {
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder1Url,
-      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
-                    true /* has_signals */, "k1", "a") +
-          kReportWinNoUrl);
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder2Url,
-      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
-                    true /* has_signals */, "l2", "b") +
-          kReportWinNoUrl);
-  AddJavascriptResponse(&url_loader_factory_, kSellerUrl, MakeAuctionScript());
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
-      R"({"k1":"a", "k2": "b", "extra": "c"})");
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
-      R"({"l1":"a", "l2": "b", "extra": "c"})");
-
-  Result res = RunStandardAuction();
-  EXPECT_EQ("https://ad2.com/", res.ad_url.spec());
-  EXPECT_EQ("https://anotheradthing.com", res.interest_group_owner.Serialize());
-  EXPECT_EQ("Another Ad Thing", res.interest_group_name);
-  EXPECT_TRUE(res.seller_report->success);
-  EXPECT_EQ("https://reporting.example.com/",
-            res.seller_report->report_url.spec());
-  EXPECT_FALSE(res.bidder_report->report_requested);
-  EXPECT_TRUE(res.bidder_report->report_url.is_empty());
-  EXPECT_THAT(res.errors, testing::ElementsAre());
-}
-
-// A successful auction where neither reporting worklets sets a URL.
-TEST_F(AuctionRunnerTest, NeitherReportUrl) {
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder1Url,
-      MakeBidScript("1", "https://ad1.com/", kBidder1, kBidder1Name,
-                    true /* has_signals */, "k1", "a") +
-          kReportWinNoUrl);
-  AddJavascriptResponse(
-      &url_loader_factory_, kBidder2Url,
-      MakeBidScript("2", "https://ad2.com/", kBidder2, kBidder2Name,
-                    true /* has_signals */, "l2", "b") +
-          kReportWinNoUrl);
-  AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
-                        MakeAuctionScriptNoReportUrl());
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=k1,k2"),
-      R"({"k1":"a", "k2": "b", "extra": "c"})");
-  AddJsonResponse(
-      &url_loader_factory_,
-      GURL(kTrustedSignalsUrl.spec() + "?hostname=publisher1.com&keys=l1,l2"),
-      R"({"l1":"a", "l2": "b", "extra": "c"})");
-
-  Result res = RunStandardAuction();
-  EXPECT_EQ("https://ad2.com/", res.ad_url.spec());
-  EXPECT_EQ("https://anotheradthing.com", res.interest_group_owner.Serialize());
-  EXPECT_EQ("Another Ad Thing", res.interest_group_name);
-  EXPECT_TRUE(res.seller_report->success);
-  EXPECT_TRUE(res.seller_report->report_url.is_empty());
-  EXPECT_FALSE(res.bidder_report->report_requested);
-  EXPECT_TRUE(res.bidder_report->report_url.is_empty());
-  EXPECT_THAT(res.errors, testing::ElementsAre());
-}
-
-}  // namespace
-}  // namespace auction_worklet
diff --git a/content/services/auction_worklet/auction_worklet_service_impl.cc b/content/services/auction_worklet/auction_worklet_service_impl.cc
index d2f66d7c..b3c3b14 100644
--- a/content/services/auction_worklet/auction_worklet_service_impl.cc
+++ b/content/services/auction_worklet/auction_worklet_service_impl.cc
@@ -4,11 +4,14 @@
 
 #include "content/services/auction_worklet/auction_worklet_service_impl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
-#include "content/services/auction_worklet/auction_runner.h"
+#include "content/services/auction_worklet/auction_v8_helper.h"
+#include "content/services/auction_worklet/bidder_worklet.h"
 #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
+#include "content/services/auction_worklet/seller_worklet.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
@@ -21,15 +24,39 @@
 
 AuctionWorkletServiceImpl::~AuctionWorkletServiceImpl() = default;
 
-void AuctionWorkletServiceImpl::RunAuction(
-    mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory,
-    blink::mojom::AuctionAdConfigPtr auction_config,
-    std::vector<mojom::BiddingInterestGroupPtr> bidders,
-    mojom::BrowserSignalsPtr browser_signals,
-    mojom::AuctionWorkletService::RunAuctionCallback callback) {
-  AuctionRunner::CreateAndStart(
-      std::move(url_loader_factory), std::move(auction_config),
-      std::move(bidders), std::move(browser_signals), std::move(callback));
+void AuctionWorkletServiceImpl::LoadBidderWorkletAndGenerateBid(
+    mojo::PendingReceiver<mojom::BidderWorklet> bidder_worklet_receiver,
+    mojo::PendingRemote<network::mojom::URLLoaderFactory>
+        pending_url_loader_factory,
+    mojom::BiddingInterestGroupPtr bidding_interest_group,
+    const absl::optional<std::string>& auction_signals_json,
+    const absl::optional<std::string>& per_buyer_signals_json,
+    const url::Origin& top_window_origin,
+    const url::Origin& seller_origin,
+    base::Time auction_start_time,
+    LoadBidderWorkletAndGenerateBidCallback
+        load_bidder_worklet_and_generate_bid_callback) {
+  bidder_worklets_.Add(
+      std::make_unique<BidderWorklet>(
+          &auction_v8_helper_, std::move(pending_url_loader_factory),
+          std::move(bidding_interest_group), auction_signals_json,
+          per_buyer_signals_json, top_window_origin, seller_origin,
+          auction_start_time,
+          std::move(load_bidder_worklet_and_generate_bid_callback)),
+      std::move(bidder_worklet_receiver));
+}
+
+void AuctionWorkletServiceImpl::LoadSellerWorklet(
+    mojo::PendingReceiver<mojom::SellerWorklet> seller_worklet_receiver,
+    mojo::PendingRemote<network::mojom::URLLoaderFactory>
+        pending_url_loader_factory,
+    const GURL& script_source_url,
+    LoadSellerWorkletCallback load_seller_worklet_callback) {
+  seller_worklets_.Add(
+      std::make_unique<SellerWorklet>(
+          &auction_v8_helper_, std::move(pending_url_loader_factory),
+          script_source_url, std::move(load_seller_worklet_callback)),
+      std::move(seller_worklet_receiver));
 }
 
 }  // namespace auction_worklet
diff --git a/content/services/auction_worklet/auction_worklet_service_impl.h b/content/services/auction_worklet/auction_worklet_service_impl.h
index 309ab31..94c22c46 100644
--- a/content/services/auction_worklet/auction_worklet_service_impl.h
+++ b/content/services/auction_worklet/auction_worklet_service_impl.h
@@ -5,15 +5,20 @@
 #ifndef CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_WORKLET_SERVICE_IMPL_H_
 #define CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_WORKLET_SERVICE_IMPL_H_
 
-#include <vector>
-
+#include "content/services/auction_worklet/auction_v8_helper.h"
 #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/receiver_set.h"
+#include "mojo/public/cpp/bindings/unique_receiver_set.h"
 #include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
 
 namespace auction_worklet {
 
+class BidderWorklet;
+class SellerWorklet;
+
 // mojom::AuctionWorkletService implementation. This is intended to run in a
 // sandboxed utility process.
 class AuctionWorkletServiceImpl : public mojom::AuctionWorkletService {
@@ -26,15 +31,34 @@
   ~AuctionWorkletServiceImpl() override;
 
   // mojom::AuctionWorkletService implementation:
-  void RunAuction(
-      mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory,
-      blink::mojom::AuctionAdConfigPtr auction_config,
-      std::vector<mojom::BiddingInterestGroupPtr> bidders,
-      mojom::BrowserSignalsPtr browser_signals,
-      mojom::AuctionWorkletService::RunAuctionCallback callback) override;
+  void LoadBidderWorkletAndGenerateBid(
+      mojo::PendingReceiver<mojom::BidderWorklet> bidder_worklet_receiver,
+      mojo::PendingRemote<network::mojom::URLLoaderFactory>
+          pending_url_loader_factory,
+      mojom::BiddingInterestGroupPtr bidding_interest_group,
+      const absl::optional<std::string>& auction_signals_json,
+      const absl::optional<std::string>& per_buyer_signals_json,
+      const url::Origin& top_window_origin,
+      const url::Origin& seller_origin,
+      base::Time auction_start_time,
+      LoadBidderWorkletAndGenerateBidCallback
+          load_bidder_worklet_and_generate_bid_callback) override;
+  void LoadSellerWorklet(
+      mojo::PendingReceiver<mojom::SellerWorklet> seller_worklet_receiver,
+      mojo::PendingRemote<network::mojom::URLLoaderFactory>
+          pending_url_loader_factory,
+      const GURL& script_source_url,
+      LoadSellerWorkletCallback load_seller_worklet_callback) override;
 
  private:
   mojo::Receiver<mojom::AuctionWorkletService> receiver_;
+
+  // `auction_v8_helper_` needs to be before the worklets, since they refer to
+  // it, so need to be torn down before it is.
+  AuctionV8Helper auction_v8_helper_;
+
+  mojo::UniqueReceiverSet<mojom::BidderWorklet> bidder_worklets_;
+  mojo::UniqueReceiverSet<mojom::SellerWorklet> seller_worklets_;
 };
 
 }  // namespace auction_worklet
diff --git a/content/services/auction_worklet/bidder_worklet.cc b/content/services/auction_worklet/bidder_worklet.cc
index ced7edd9..67a07e1 100644
--- a/content/services/auction_worklet/bidder_worklet.cc
+++ b/content/services/auction_worklet/bidder_worklet.cc
@@ -23,6 +23,8 @@
 #include "content/services/auction_worklet/worklet_loader.h"
 #include "gin/converter.h"
 #include "gin/dictionary.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
 #include "mojo/public/cpp/bindings/struct_ptr.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
@@ -48,65 +50,44 @@
   return true;
 }
 
-// Temporary utility to run callback asynchronously, to imitate behavior once
-// this class starts implementing a Mojo API.
-//
-// TODO(mmenke): Remove once this class switches over to using Mojo.
-void InvokeReportWinCallbackAsync(BidderWorklet::ReportWinCallback callback,
-                                  const absl::optional<GURL>& report_url,
-                                  const std::vector<std::string>& errors) {
-  base::ThreadTaskRunnerHandle::Get()->PostTask(
-      FROM_HERE, base::BindOnce(std::move(callback), report_url, errors));
-}
-
 }  // namespace
 
-BidderWorklet::Bid::Bid(std::string ad,
-                        double bid,
-                        GURL render_url,
-                        base::TimeDelta bid_duration)
-    : ad(std::move(ad)),
-      bid(bid),
-      render_url(std::move(render_url)),
-      bid_duration(bid_duration) {
-  DCHECK_GT(this->bid, 0);
-  DCHECK(this->render_url.is_valid());
-}
-
-BidderWorklet::Bid::Bid(const Bid& other) = default;
-BidderWorklet::Bid::Bid(Bid&& other) = default;
-BidderWorklet::Bid::~Bid() = default;
-BidderWorklet::Bid& BidderWorklet::Bid::operator=(const Bid&) = default;
-BidderWorklet::Bid& BidderWorklet::Bid::operator=(Bid&&) = default;
-
 BidderWorklet::BidderWorklet(
     AuctionV8Helper* v8_helper,
-    network::mojom::URLLoaderFactory* url_loader_factory,
+    mojo::PendingRemote<network::mojom::URLLoaderFactory>
+        pending_url_loader_factory,
     mojom::BiddingInterestGroupPtr bidding_interest_group,
     const absl::optional<std::string>& auction_signals_json,
     const absl::optional<std::string>& per_buyer_signals_json,
     const url::Origin& browser_signal_top_window_origin,
     const url::Origin& browser_signal_seller_origin,
     base::Time auction_start_time,
-    LoadScriptAndGenerateBidCallback load_script_and_generate_bid_callback)
+    mojom::AuctionWorkletService::LoadBidderWorkletAndGenerateBidCallback
+        load_bidder_worklet_and_generate_bid_callback)
     : v8_helper_(v8_helper),
       script_source_url_(
           bidding_interest_group->group->bidding_url.value_or(GURL())),
+      load_bidder_worklet_and_generate_bid_callback_(
+          std::move(load_bidder_worklet_and_generate_bid_callback)),
       bidding_interest_group_(std::move(bidding_interest_group)),
-      load_script_and_generate_bid_callback_(
-          std::move(load_script_and_generate_bid_callback)),
       auction_signals_json_(auction_signals_json),
       per_buyer_signals_json_(per_buyer_signals_json),
       browser_signal_top_window_hostname_(
           browser_signal_top_window_origin.host()),
       browser_signal_seller_(browser_signal_seller_origin.Serialize()),
       auction_start_time_(auction_start_time) {
-  DCHECK(load_script_and_generate_bid_callback_);
+  DCHECK(load_bidder_worklet_and_generate_bid_callback_);
+
+  // Bind URLLoaderFactory. Remote is not needed after this method completes,
+  // since requests will continue after the URLLoaderFactory pipe has been
+  // closed, so no need to keep it around after requests have been issued.
+  mojo::Remote<network::mojom::URLLoaderFactory> url_loader_factory(
+      std::move(pending_url_loader_factory));
 
   // TODO(mmenke): Remove up the value_or() for script_source_url_- auction
   // worklets shouldn't be created when there's no bidding URL.
   worklet_loader_ = std::make_unique<WorkletLoader>(
-      url_loader_factory, script_source_url_, v8_helper,
+      url_loader_factory.get(), script_source_url_, v8_helper_,
       base::BindOnce(&BidderWorklet::OnScriptDownloaded,
                      base::Unretained(this)));
 
@@ -116,16 +97,20 @@
       !bidding_interest_group_->group->trusted_bidding_signals_keys->empty()) {
     trusted_bidding_signals_loading_ = true;
     trusted_bidding_signals_ = std::make_unique<TrustedBiddingSignals>(
-        url_loader_factory,
+        url_loader_factory.get(),
         *bidding_interest_group_->group->trusted_bidding_signals_keys,
         browser_signal_top_window_origin.host(),
-        *bidding_interest_group_->group->trusted_bidding_signals_url, v8_helper,
+        *bidding_interest_group_->group->trusted_bidding_signals_url,
+        v8_helper_,
         base::BindOnce(&BidderWorklet::OnTrustedBiddingSignalsDownloaded,
                        base::Unretained(this)));
   }
 }
 
-BidderWorklet::~BidderWorklet() = default;
+BidderWorklet::~BidderWorklet() {
+  if (load_bidder_worklet_and_generate_bid_callback_)
+    InvokeBidCallbackOnError();
+}
 
 void BidderWorklet::ReportWin(
     const std::string& seller_signals_json,
@@ -133,8 +118,6 @@
     const std::string& browser_signal_ad_render_fingerprint,
     double browser_signal_bid,
     ReportWinCallback callback) {
-  callback = base::BindOnce(&InvokeReportWinCallbackAsync, std::move(callback));
-
   AuctionV8Helper::FullIsolateScope isolate_scope(v8_helper_);
   v8::Isolate* isolate = v8_helper_->isolate();
 
@@ -201,7 +184,7 @@
 void BidderWorklet::OnScriptDownloaded(
     std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
     absl::optional<std::string> error_msg) {
-  DCHECK(load_script_and_generate_bid_callback_);
+  DCHECK(load_bidder_worklet_and_generate_bid_callback_);
 
   if (worklet_script == nullptr) {
     // Abort loading trusted bidding signals, if it hasn't completed already.
@@ -219,7 +202,7 @@
     bool load_result,
     absl::optional<std::string> error_msg) {
   // Worklet results should still be pending.
-  DCHECK(load_script_and_generate_bid_callback_);
+  DCHECK(load_bidder_worklet_and_generate_bid_callback_);
   DCHECK(trusted_bidding_signals_loading_);
 
   trusted_bidding_signals_error_msg_ = std::move(error_msg);
@@ -231,7 +214,7 @@
 }
 
 void BidderWorklet::GenerateBidIfReady() {
-  DCHECK(load_script_and_generate_bid_callback_);
+  DCHECK(load_bidder_worklet_and_generate_bid_callback_);
   if (trusted_bidding_signals_loading_ || !worklet_script_)
     return;
 
@@ -399,8 +382,9 @@
         errors.emplace_back(
             std::move(trusted_bidding_signals_error_msg_).value());
       }
-      std::move(load_script_and_generate_bid_callback_)
-          .Run(Bid(std::move(ad_json), bid, std::move(render_url),
+      std::move(load_bidder_worklet_and_generate_bid_callback_)
+          .Run(mojom::BidderWorkletBid::New(
+                   std::move(ad_json), bid, std::move(render_url),
                    base::TimeTicks::Now() - start /* bid_duration */),
                errors);
       return;
@@ -417,11 +401,10 @@
   std::vector<std::string> errors;
   if (error_msg)
     errors.emplace_back(std::move(error_msg).value());
-  if (trusted_bidding_signals_error_msg_) {
+  if (trusted_bidding_signals_error_msg_)
     errors.emplace_back(std::move(trusted_bidding_signals_error_msg_).value());
-  }
-  std::move(load_script_and_generate_bid_callback_)
-      .Run(absl::nullopt /* bid */, errors);
+  std::move(load_bidder_worklet_and_generate_bid_callback_)
+      .Run(mojom::BidderWorkletBidPtr(), errors);
 }
 
 }  // namespace auction_worklet
diff --git a/content/services/auction_worklet/bidder_worklet.h b/content/services/auction_worklet/bidder_worklet.h
index fd88752..b455c7d 100644
--- a/content/services/auction_worklet/bidder_worklet.h
+++ b/content/services/auction_worklet/bidder_worklet.h
@@ -12,8 +12,9 @@
 
 #include "base/callback.h"
 #include "base/time/time.h"
-#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom-forward.h"
 #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
+#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/struct_ptr.h"
 #include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
@@ -39,57 +40,8 @@
 // to both be used for two generateBid() calls for different interest groups
 // with the same owner in the same auction, and to be used to bid for the same
 // interest group in different auctions.
-class BidderWorklet {
+class BidderWorklet : public mojom::BidderWorklet {
  public:
-  // Structure containing information about a bid returned by invoking a
-  // worklet's generageBid() method. If no bid is made, no Bid is constructed.
-  struct Bid {
-    Bid(std::string ad,
-        double bid,
-        GURL render_url,
-        base::TimeDelta bid_duration);
-
-    Bid(const Bid& other);
-    Bid(Bid&& other);
-
-    ~Bid();
-
-    Bid& operator=(const Bid&);
-    Bid& operator=(Bid&&);
-
-    // JSON string to be passed to the scoring function.
-    std::string ad;
-
-    // Offered bid value.
-    double bid;
-
-    // Render URL, if any bid was made.
-    GURL render_url;
-
-    // How long it took to run the script that generated the bid.
-    base::TimeDelta bid_duration;
-  };
-
-  // If no bid is generated, `bid` is null.
-  //
-  // `errors` contains error messages for debugging. This isn't guaranteed
-  // to be produced for all failures, so should not be checked to identify
-  // bidding failures errors. It's also possible for there to be an error
-  // message on success, in the case the trusted bidding signals failed to load
-  // - auctions will still be run without it, but `errors` will be populated
-  // with information about the load failure.
-  using LoadScriptAndGenerateBidCallback =
-      base::OnceCallback<void(absl::optional<Bid> bid,
-                              const std::vector<std::string>& errors)>;
-
-  // `report_url` is the URL to request to report displaying the ad. It is
-  // nullopt on error or if report is requested. `errors` is a list of
-  // errors that occurred, if any. `errors` may be non-empty on success, or
-  // empty on failure.
-  using ReportWinCallback =
-      base::OnceCallback<void(const absl::optional<GURL>& report_url,
-                              const std::vector<std::string>& errors)>;
-
   // Starts loading the worklet script on construction, as well as the trusted
   // bidding data, if necessary. Will then call the script's generateBid()
   // function and invoke the callback with the results. Callback will always be
@@ -99,25 +51,27 @@
   // Data is cached and will be reused ReportWin().
   BidderWorklet(
       AuctionV8Helper* v8_helper,
-      network::mojom::URLLoaderFactory* url_loader_factory,
+      mojo::PendingRemote<network::mojom::URLLoaderFactory>
+          pending_url_loader_factory,
       mojom::BiddingInterestGroupPtr bidding_interest_group,
       const absl::optional<std::string>& auction_signals_json,
       const absl::optional<std::string>& per_buyer_signals_json,
       const url::Origin& browser_signal_top_window_origin,
       const url::Origin& browser_signal_seller_origin,
       base::Time auction_start_time,
-      LoadScriptAndGenerateBidCallback load_script_and_generate_bid_callback);
+      mojom::AuctionWorkletService::LoadBidderWorkletAndGenerateBidCallback
+          load_bidder_worklet_and_generate_bid_callback);
   explicit BidderWorklet(const BidderWorklet&) = delete;
   BidderWorklet& operator=(const BidderWorklet&) = delete;
-  ~BidderWorklet();
 
-  // Calls reportWin(), and asynchronously invokes `callback` with reporting
-  // information. May only be called once the worklet has successfully loaded.
+  ~BidderWorklet() override;
+
+  // mojom::BidderWorklet implementation:
   void ReportWin(const std::string& seller_signals_json,
                  const GURL& browser_signal_render_url,
                  const std::string& browser_signal_ad_render_fingerprint,
                  double browser_signal_bid,
-                 ReportWinCallback callback);
+                 ReportWinCallback callback) override;
 
  private:
   void OnScriptDownloaded(
@@ -141,11 +95,11 @@
 
   AuctionV8Helper* const v8_helper_;
 
-  const GURL script_source_url_;
+  GURL script_source_url_;
+  mojom::AuctionWorkletService::LoadBidderWorkletAndGenerateBidCallback
+      load_bidder_worklet_and_generate_bid_callback_;
+
   const mojom::BiddingInterestGroupPtr bidding_interest_group_;
-
-  LoadScriptAndGenerateBidCallback load_script_and_generate_bid_callback_;
-
   const absl::optional<std::string> auction_signals_json_;
   const absl::optional<std::string> per_buyer_signals_json_;
   const std::string browser_signal_top_window_hostname_;
diff --git a/content/services/auction_worklet/bidder_worklet_unittest.cc b/content/services/auction_worklet/bidder_worklet_unittest.cc
index e86467f..c4fe050 100644
--- a/content/services/auction_worklet/bidder_worklet_unittest.cc
+++ b/content/services/auction_worklet/bidder_worklet_unittest.cc
@@ -17,6 +17,7 @@
 #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
 #include "content/services/auction_worklet/trusted_bidding_signals.h"
 #include "content/services/auction_worklet/worklet_test_util.h"
+#include "mojo/public/cpp/bindings/self_owned_receiver.h"
 #include "mojo/public/cpp/bindings/struct_ptr.h"
 #include "net/http/http_status_code.h"
 #include "services/network/test/test_url_loader_factory.h"
@@ -110,10 +111,10 @@
   // specified return line Then runs the script, expecting the provided result.
   void RunGenerateBidWithReturnValueExpectingResult(
       const std::string& raw_return_value,
-      const absl::optional<BidderWorklet::Bid> expected_bid,
+      mojom::BidderWorkletBidPtr expected_bid,
       std::vector<std::string> expected_errors = std::vector<std::string>()) {
     RunGenerateBidWithJavascriptExpectingResult(
-        CreateGenerateBidScript(raw_return_value), expected_bid,
+        CreateGenerateBidScript(raw_return_value), std::move(expected_bid),
         expected_errors);
   }
 
@@ -121,22 +122,22 @@
   // Javascript Then runs the script, expecting the provided result.
   void RunGenerateBidWithJavascriptExpectingResult(
       const std::string& javascript,
-      const absl::optional<BidderWorklet::Bid> expected_bid,
+      mojom::BidderWorkletBidPtr expected_bid,
       std::vector<std::string> expected_errors = std::vector<std::string>()) {
     SCOPED_TRACE(javascript);
     AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
                           javascript);
-    RunGenerateBidExpectingResult(expected_bid, expected_errors);
+    RunGenerateBidExpectingResult(std::move(expected_bid), expected_errors);
   }
 
   // Loads and runs a generateBid() script, expecting the provided result.
   void RunGenerateBidExpectingResult(
-      const absl::optional<BidderWorklet::Bid> expected_bid,
+      mojom::BidderWorkletBidPtr expected_bid,
       std::vector<std::string> expected_errors = std::vector<std::string>()) {
     auto bidder_worklet = CreateWorkletAndGenerateBid();
 
-    EXPECT_EQ(expected_bid.has_value(), bid_.has_value());
-    if (expected_bid.has_value() && bid_.has_value()) {
+    EXPECT_EQ(expected_bid.is_null(), bid_.is_null());
+    if (expected_bid && bid_) {
       EXPECT_EQ(expected_bid->ad, bid_->ad);
       EXPECT_EQ(expected_bid->bid, bid_->bid);
       EXPECT_EQ(expected_bid->render_url, bid_->render_url);
@@ -193,11 +194,8 @@
     run_loop.Run();
   }
 
-  // Create a BidderWorklet, waiting for the URLLoader to complete. Returns
-  // nullptr on failure.
-  std::unique_ptr<BidderWorklet> CreateWorkletAndGenerateBid() {
-    CHECK(!load_script_run_loop_);
-
+  // Creates a BiddingInterestGroup based on test fixture configuration.
+  mojom::BiddingInterestGroupPtr CreateBiddingInterestGroup() {
     blink::mojom::InterestGroupPtr interest_group =
         blink::mojom::InterestGroup::New();
     interest_group->owner = interest_group_owner_;
@@ -224,30 +222,52 @@
     mojom::BiddingInterestGroupPtr bidding_interest_group =
         mojom::BiddingInterestGroup::New(std::move(interest_group),
                                          std::move(bidding_browser_signals));
+    return bidding_interest_group;
+  }
 
-    auto bidder_worket = std::make_unique<BidderWorklet>(
-        &v8_helper_, &url_loader_factory_, std::move(bidding_interest_group),
-        null_auction_signals_
-            ? absl::nullopt
-            : absl::make_optional<std::string>(auction_signals_),
-        null_per_buyer_signals_
-            ? absl::nullopt
-            : absl::make_optional<std::string>(per_buyer_signals_),
-        browser_signal_top_window_origin_, browser_signal_seller_origin_,
-        auction_start_time_,
-        base::BindOnce(&BidderWorkletTest::CreateWorkletCallback,
-                       base::Unretained(this)));
+  // Create a BidderWorklet, waiting for the URLLoader to complete. Returns
+  // a null Remote on failure.
+  mojo::Remote<mojom::BidderWorklet> CreateWorkletAndGenerateBid() {
+    CHECK(!load_script_run_loop_);
+
+    mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
+    url_loader_factory_.Clone(
+        url_loader_factory.InitWithNewPipeAndPassReceiver());
+
+    mojo::Remote<mojom::BidderWorklet> bidder_worklet;
+    mojo::MakeSelfOwnedReceiver(
+        std::make_unique<BidderWorklet>(
+            &v8_helper_, std::move(url_loader_factory),
+            CreateBiddingInterestGroup(),
+            null_auction_signals_
+                ? absl::nullopt
+                : absl::make_optional<std::string>(auction_signals_),
+            null_per_buyer_signals_
+                ? absl::nullopt
+                : absl::make_optional<std::string>(per_buyer_signals_),
+            browser_signal_top_window_origin_, browser_signal_seller_origin_,
+            auction_start_time_,
+            base::BindOnce(&BidderWorkletTest::CreateWorkletCallback,
+                           base::Unretained(this))),
+        bidder_worklet.BindNewPipeAndPassReceiver());
     load_script_run_loop_ = std::make_unique<base::RunLoop>();
     load_script_run_loop_->Run();
     load_script_run_loop_.reset();
     if (!bid_)
-      return nullptr;
-    return bidder_worket;
+      return mojo::Remote<mojom::BidderWorklet>();
+    return bidder_worklet;
   }
 
-  const absl::optional<BidderWorklet::Bid>& bid() const { return bid_; }
+  const mojom::BidderWorkletBidPtr& bid() const { return bid_; }
   const std::vector<std::string> bid_errors() const { return bid_errors_; }
 
+  void CreateWorkletCallback(mojom::BidderWorkletBidPtr bid,
+                             const std::vector<std::string>& errors) {
+    bid_ = std::move(bid);
+    bid_errors_ = std::move(errors);
+    load_script_run_loop_->Quit();
+  }
+
  protected:
   std::vector<mojo::StructPtr<mojom::PreviousWin>> CloneWinList(
       const std::vector<mojo::StructPtr<mojom::PreviousWin>>& prev_win_list) {
@@ -258,13 +278,6 @@
     return out;
   }
 
-  void CreateWorkletCallback(absl::optional<BidderWorklet::Bid> bid,
-                             const std::vector<std::string>& errors) {
-    bid_ = std::move(bid);
-    bid_errors_ = errors;
-    load_script_run_loop_->Quit();
-  }
-
   base::test::TaskEnvironment task_environment_;
 
   // Values used to construct the BiddingInterestGroup passed to the
@@ -308,19 +321,49 @@
   std::unique_ptr<base::RunLoop> load_script_run_loop_;
 
   // Values passed to the GenerateBidCallback().
-  absl::optional<BidderWorklet::Bid> bid_;
+  mojom::BidderWorkletBidPtr bid_;
   std::vector<std::string> bid_errors_;
 
   network::TestURLLoaderFactory url_loader_factory_;
   AuctionV8Helper v8_helper_;
 };
 
+// Test the case the BidderWorklet pipe is closed before invoking the
+// LoadBidderWorkletAndGenerateBidCallback.
+// LoadBidderWorkletAndGenerateBidCallback should be invoked, and there should
+// be no Mojo exception due to destroying the creation callback without invoking
+// it.
+TEST_F(BidderWorkletTest, PipeClosed) {
+  mojo::Remote<mojom::BidderWorklet> bidder_worklet;
+  mojo::PendingReceiver<network::mojom::URLLoaderFactory>
+      url_loader_factory_receiver;
+
+  mojo::MakeSelfOwnedReceiver(
+      std::make_unique<BidderWorklet>(
+          &v8_helper_,
+          url_loader_factory_receiver.InitWithNewPipeAndPassRemote(),
+          CreateBiddingInterestGroup(),
+          absl::nullopt /* auction_signals_json */,
+          absl::nullopt /* per_buyer_signals_json */,
+          browser_signal_top_window_origin_, browser_signal_seller_origin_,
+          auction_start_time_,
+          base::BindOnce(&BidderWorkletTest::CreateWorkletCallback,
+                         base::Unretained(this))),
+      bidder_worklet.BindNewPipeAndPassReceiver());
+  load_script_run_loop_ = std::make_unique<base::RunLoop>();
+  bidder_worklet.reset();
+
+  load_script_run_loop_->Run();
+  load_script_run_loop_.reset();
+  EXPECT_FALSE(bid_);
+}
+
 TEST_F(BidderWorkletTest, NetworkError) {
   url_loader_factory_.AddResponse(interest_group_bidding_url_.spec(),
                                   CreateBasicGenerateBidScript(),
                                   net::HTTP_NOT_FOUND);
   RunGenerateBidExpectingResult(
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"Failed to load https://url.test/ HTTP status = 404 Not Found."});
 }
 
@@ -341,8 +384,8 @@
   // CreateBasicGenerateBidScript() does indeed work.
   RunGenerateBidWithJavascriptExpectingResult(
       CreateBasicGenerateBidScript(),
-      BidderWorklet::Bid("[\"ad\"]", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New(
+          "[\"ad\"]", 1, GURL("https://response.test/"), base::TimeDelta()));
 
   // --------
   // Vary ad
@@ -351,43 +394,44 @@
   // Make sure "ad" can be of a variety of JS object types.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: "ad", bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("\"ad\"", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New("\"ad\"", 1, GURL("https://response.test/"),
+                                   base::TimeDelta()));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: {a:1,b:null}, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid(R"({"a":1,"b":null})", 1,
-                         GURL("https://response.test/"), base::TimeDelta()));
+      mojom::BidderWorkletBid::New(R"({"a":1,"b":null})", 1,
+                                   GURL("https://response.test/"),
+                                   base::TimeDelta()));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: [2.5,[]], bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("[2.5,[]]", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New(
+          "[2.5,[]]", 1, GURL("https://response.test/"), base::TimeDelta()));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: -5, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("-5", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New("-5", 1, GURL("https://response.test/"),
+                                   base::TimeDelta()));
   // Some values that can't be represented in JSON become null.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: 0/0, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("null", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New("null", 1, GURL("https://response.test/"),
+                                   base::TimeDelta()));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: [globalThis.not_defined], bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("[null]", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New("[null]", 1, GURL("https://response.test/"),
+                                   base::TimeDelta()));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: [function() {return 1;}], bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("[null]", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New("[null]", 1, GURL("https://response.test/"),
+                                   base::TimeDelta()));
 
   // Other values JSON can't represent result in failing instead of null.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: globalThis.not_defined, bid:1, render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() return value "
        "has incorrect structure."});
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: function() {return 1;}, bid:1, render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() return value "
        "has incorrect structure."});
 
@@ -400,7 +444,7 @@
           return {ad: a, bid:1, render:"https://response.test/"};
         }
       )",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() return value "
        "has incorrect structure."});
 
@@ -411,48 +455,48 @@
   // Valid positive bid values.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: "ad", bid:1.5, render:"https://response.test/"})",
-      BidderWorklet::Bid("\"ad\"", 1.5, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New(
+          "\"ad\"", 1.5, GURL("https://response.test/"), base::TimeDelta()));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: "ad", bid:2, render:"https://response.test/"})",
-      BidderWorklet::Bid("\"ad\"", 2, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New("\"ad\"", 2, GURL("https://response.test/"),
+                                   base::TimeDelta()));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: "ad", bid:0.001, render:"https://response.test/"})",
-      BidderWorklet::Bid("\"ad\"", 0.001, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New(
+          "\"ad\"", 0.001, GURL("https://response.test/"), base::TimeDelta()));
 
   // Bids <= 0.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:0, render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */);
+      mojom::BidderWorkletBidPtr() /* expected_bid */);
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:-10, render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */);
+      mojom::BidderWorkletBidPtr() /* expected_bid */);
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:-1.5, render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */);
+      mojom::BidderWorkletBidPtr() /* expected_bid */);
 
   // Infinite and NaN bid.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:1/0, render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */);
+      mojom::BidderWorkletBidPtr() /* expected_bid */);
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:-1/0, render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */);
+      mojom::BidderWorkletBidPtr() /* expected_bid */);
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:0/0, render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */);
+      mojom::BidderWorkletBidPtr() /* expected_bid */);
 
   // Non-numeric bid.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:"1", render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() return value "
        "has incorrect structure."});
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:[1], render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() return value "
        "has incorrect structure."});
 
@@ -462,48 +506,50 @@
 
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("[\"ad\"]", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New(
+          "[\"ad\"]", 1, GURL("https://response.test/"), base::TimeDelta()));
 
   // Disallowed schemes.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:1, render:"http://response.test/"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() returned "
        "render_url isn't a valid https:// URL."});
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:1, render:"chrome-extension://response.test/"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() returned "
        "render_url isn't a valid https:// URL."});
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:1, render:"about:blank"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() returned "
        "render_url isn't a valid https:// URL."});
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:1, render:"data:,foo"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() returned "
        "render_url isn't a valid https:// URL."});
 
   // Invalid URLs.
   RunGenerateBidWithReturnValueExpectingResult(
-      R"({ad: ["ad"], bid:1, render:"test"})", absl::nullopt /* expected_bid */,
+      R"({ad: ["ad"], bid:1, render:"test"})",
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() returned "
        "render_url isn't a valid https:// URL."});
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:1, render:"http://"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() returned "
        "render_url isn't a valid https:// URL."});
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:1, render:["http://response.test/"]})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() return value "
        "has incorrect structure."});
   RunGenerateBidWithReturnValueExpectingResult(
-      R"({ad: ["ad"], bid:1, render:9})", absl::nullopt /* expected_bid */,
+      R"({ad: ["ad"], bid:1, render:9})",
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() return value "
        "has incorrect structure."});
 
@@ -513,22 +559,23 @@
 
   // No return value.
   RunGenerateBidWithReturnValueExpectingResult(
-      "", absl::nullopt /* expected_bid */,
+      "", mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() return value not an object."});
 
   // Missing value.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({bid:"a", render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() return value "
        "has incorrect structure."});
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() return value "
        "has incorrect structure."});
   RunGenerateBidWithReturnValueExpectingResult(
-      R"({ad: ["ad"], bid:"a"})", absl::nullopt /* expected_bid */,
+      R"({ad: ["ad"], bid:"a"})",
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() return value "
        "has incorrect structure."});
 
@@ -539,18 +586,18 @@
           return {ad: ["ad"], bid:1, render:"https://response.test/"};
         }
       )",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ `generateBid` is not a function."});
   RunGenerateBidWithJavascriptExpectingResult(
-      "", absl::nullopt /* expected_bid */,
+      "", mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ `generateBid` is not a function."});
   RunGenerateBidWithJavascriptExpectingResult(
-      "5", absl::nullopt /* expected_bid */,
+      "5", mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ `generateBid` is not a function."});
 
   // Throw exception.
   RunGenerateBidWithJavascriptExpectingResult(
-      "shrimp", absl::nullopt /* expected_bid */,
+      "shrimp", mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/:1 Uncaught ReferenceError: "
        "shrimp is not defined."});
 }
@@ -559,7 +606,7 @@
 TEST_F(BidderWorkletTest, GenerateBidDateNotAvailable) {
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: Date().toString(), bid:1, render:"https://response.test/"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/:4 Uncaught ReferenceError: Date is not defined."});
 }
 
@@ -605,10 +652,10 @@
         base::StringPrintf(
             R"({ad: %s, bid:1, render:"https://response.test/"})",
             test_case.name),
-        test_case.is_json
-            ? absl::optional<BidderWorklet::Bid>()
-            : BidderWorklet::Bid(R"("foo")", 1, GURL("https://response.test/"),
-                                 base::TimeDelta()));
+        test_case.is_json ? mojom::BidderWorkletBidPtr()
+                          : mojom::BidderWorkletBid::New(
+                                R"("foo")", 1, GURL("https://response.test/"),
+                                base::TimeDelta()));
 
     *test_case.value_ptr = R"("foo")";
     RunGenerateBidWithReturnValueExpectingResult(
@@ -616,11 +663,12 @@
             R"({ad: %s, bid:1, render:"https://response.test/"})",
             test_case.name),
         test_case.is_json
-            ? BidderWorklet::Bid(R"("foo")", 1, GURL("https://response.test/"),
-                                 base::TimeDelta())
-            : BidderWorklet::Bid(R"("\"foo\"")", 1,
-                                 GURL("https://response.test/"),
-                                 base::TimeDelta()));
+            ? mojom::BidderWorkletBid::New(R"("foo")", 1,
+                                           GURL("https://response.test/"),
+                                           base::TimeDelta())
+            : mojom::BidderWorkletBid::New(R"("\"foo\"")", 1,
+                                           GURL("https://response.test/"),
+                                           base::TimeDelta()));
 
     *test_case.value_ptr = "[1]";
     RunGenerateBidWithReturnValueExpectingResult(
@@ -628,38 +676,44 @@
             R"({ad: %s[0], bid:1, render:"https://response.test/"})",
             test_case.name),
         test_case.is_json
-            ? BidderWorklet::Bid("1", 1, GURL("https://response.test/"),
-                                 base::TimeDelta())
-            : BidderWorklet::Bid(R"("[")", 1, GURL("https://response.test/"),
-                                 base::TimeDelta()));
+            ? mojom::BidderWorkletBid::New(
+                  "1", 1, GURL("https://response.test/"), base::TimeDelta())
+            : mojom::BidderWorkletBid::New(R"("[")", 1,
+                                           GURL("https://response.test/"),
+                                           base::TimeDelta()));
     SetDefaultParameters();
   }
 
   interest_group_owner_ = url::Origin::Create(GURL("https://foo.test/"));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: interestGroup.owner, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid(R"("https://foo.test")", 1,
-                         GURL("https://response.test/"), base::TimeDelta()));
+      mojom::BidderWorkletBid::New(R"("https://foo.test")", 1,
+                                   GURL("https://response.test/"),
+                                   base::TimeDelta()));
 
   interest_group_owner_ = url::Origin::Create(GURL("https://[::1]:40000/"));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: interestGroup.owner, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid(R"("https://[::1]:40000")", 1,
-                         GURL("https://response.test/"), base::TimeDelta()));
+      mojom::BidderWorkletBid::New(R"("https://[::1]:40000")", 1,
+                                   GURL("https://response.test/"),
+                                   base::TimeDelta()));
   SetDefaultParameters();
 
   browser_signal_seller_origin_ =
       url::Origin::Create(GURL("https://foo.test/"));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: browserSignals.seller, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid(R"("https://foo.test")", 1,
-                         GURL("https://response.test/"), base::TimeDelta()));
+      mojom::BidderWorkletBid::New(R"("https://foo.test")", 1,
+                                   GURL("https://response.test/"),
+                                   base::TimeDelta()));
+
   browser_signal_seller_origin_ =
       url::Origin::Create(GURL("https://[::1]:40000/"));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: browserSignals.seller, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid(R"("https://[::1]:40000")", 1,
-                         GURL("https://response.test/"), base::TimeDelta()));
+      mojom::BidderWorkletBid::New(R"("https://[::1]:40000")", 1,
+                                   GURL("https://response.test/"),
+                                   base::TimeDelta()));
   SetDefaultParameters();
 
   // Test the empty `userBiddingSignals` case, too. It's actually an optional
@@ -669,16 +723,18 @@
   interest_group_user_bidding_signals_ = "";
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad:typeof interestGroup.userBiddingSignals, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid(R"("undefined")", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New(R"("undefined")", 1,
+                                   GURL("https://response.test/"),
+                                   base::TimeDelta()));
   SetDefaultParameters();
 
   browser_signal_top_window_origin_ =
       url::Origin::Create(GURL("https://top.window.test/"));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: browserSignals.topWindowHostname, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid(R"("top.window.test")", 1,
-                         GURL("https://response.test/"), base::TimeDelta()));
+      mojom::BidderWorkletBid::New(R"("top.window.test")", 1,
+                                   GURL("https://response.test/"),
+                                   base::TimeDelta()));
   SetDefaultParameters();
 
   const struct IntegerTestCase {
@@ -698,16 +754,16 @@
         base::StringPrintf(
             R"({ad: %s, bid:1, render:"https://response.test/"})",
             test_case.name),
-        BidderWorklet::Bid("0", 1, GURL("https://response.test/"),
-                           base::TimeDelta()));
+        mojom::BidderWorkletBid::New("0", 1, GURL("https://response.test/"),
+                                     base::TimeDelta()));
 
     *test_case.value_ptr = 10;
     RunGenerateBidWithReturnValueExpectingResult(
         base::StringPrintf(
             R"({ad: %s, bid:1, render:"https://response.test/"})",
             test_case.name),
-        BidderWorklet::Bid("10", 1, GURL("https://response.test/"),
-                           base::TimeDelta()));
+        mojom::BidderWorkletBid::New("10", 1, GURL("https://response.test/"),
+                                     base::TimeDelta()));
     SetDefaultParameters();
   }
 
@@ -716,7 +772,7 @@
   // A bid URL that's not in the InterestGroup's ads list should fail.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: 0, bid:1, render:"https://response2.test/"})",
-      absl::nullopt /* expected_bid */,
+      mojom::BidderWorkletBidPtr() /* expected_bid */,
       {"https://url.test/ generateBid() returned render_url isn't one of "
        "the registered creative URLs."});
 
@@ -726,17 +782,18 @@
       GURL("https://response2.test/"), R"(["metadata"])" /* metadata */));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: interestGroup.ads, bid:1, render:"https://response2.test/"})",
-      BidderWorklet::Bid("[{\"renderUrl\":\"https://response.test/\"},"
-                         "{\"renderUrl\":\"https://response2.test/"
-                         "\",\"metadata\":[\"metadata\"]}]",
-                         1, GURL("https://response2.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New(
+          "[{\"renderUrl\":\"https://response.test/\"},"
+          "{\"renderUrl\":\"https://response2.test/"
+          "\",\"metadata\":[\"metadata\"]}]",
+          1, GURL("https://response2.test/"), base::TimeDelta()));
 
   // Make sure `metadata` is treated as an object, instead of a raw string.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: interestGroup.ads[1].metadata[0], bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("\"metadata\"", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New("\"metadata\"", 1,
+                                   GURL("https://response.test/"),
+                                   base::TimeDelta()));
 }
 
 // Test handling of null auctionSignals and perBuyerSignals to generateBid.
@@ -751,33 +808,33 @@
   null_auction_signals_ = false;
   null_per_buyer_signals_ = false;
   RunGenerateBidWithReturnValueExpectingResult(
-      kRetVal,
-      BidderWorklet::Bid("[false,false]", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      kRetVal, mojom::BidderWorkletBid::New("[false,false]", 1,
+                                            GURL("https://response.test/"),
+                                            base::TimeDelta()));
 
   SetDefaultParameters();
   null_auction_signals_ = false;
   null_per_buyer_signals_ = true;
   RunGenerateBidWithReturnValueExpectingResult(
-      kRetVal,
-      BidderWorklet::Bid("[false,true]", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      kRetVal, mojom::BidderWorkletBid::New("[false,true]", 1,
+                                            GURL("https://response.test/"),
+                                            base::TimeDelta()));
 
   SetDefaultParameters();
   null_auction_signals_ = true;
   null_per_buyer_signals_ = false;
   RunGenerateBidWithReturnValueExpectingResult(
-      kRetVal,
-      BidderWorklet::Bid("[true,false]", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      kRetVal, mojom::BidderWorkletBid::New("[true,false]", 1,
+                                            GURL("https://response.test/"),
+                                            base::TimeDelta()));
 
   SetDefaultParameters();
   null_auction_signals_ = true;
   null_per_buyer_signals_ = true;
   RunGenerateBidWithReturnValueExpectingResult(
       kRetVal,
-      BidderWorklet::Bid("[true,true]", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New(
+          "[true,true]", 1, GURL("https://response.test/"), base::TimeDelta()));
 }
 
 // Utility methods to create vectors of PreviousWin. Needed because StructPtr's
@@ -867,8 +924,9 @@
         base::StringPrintf(
             R"({ad: %s, bid:1, render:"https://response.test/"})",
             test_case.ad),
-        BidderWorklet::Bid(test_case.expected_ad, 1,
-                           GURL("https://response.test/"), base::TimeDelta()));
+        mojom::BidderWorkletBid::New(test_case.expected_ad, 1,
+                                     GURL("https://response.test/"),
+                                     base::TimeDelta()));
   }
 }
 
@@ -888,8 +946,8 @@
   // made.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("null", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New("null", 1, GURL("https://response.test/"),
+                                   base::TimeDelta()));
 
   // Request with TrustedBiddingSignals keys and null URL. No request should be
   // made.
@@ -897,8 +955,8 @@
       std::vector<std::string>({"key1", "key2"});
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("null", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New("null", 1, GURL("https://response.test/"),
+                                   base::TimeDelta()));
 
   // Request with TrustedBiddingSignals URL and null keys. No request should be
   // made.
@@ -906,16 +964,16 @@
   interest_group_trusted_bidding_signals_keys_.reset();
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("null", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New("null", 1, GURL("https://response.test/"),
+                                   base::TimeDelta()));
 
   // Request with TrustedBiddingSignals URL and empty keys. No request should be
   // made.
   interest_group_trusted_bidding_signals_keys_ = std::vector<std::string>();
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("null", 1, GURL("https://response.test/"),
-                         base::TimeDelta()));
+      mojom::BidderWorkletBid::New("null", 1, GURL("https://response.test/"),
+                                   base::TimeDelta()));
 
   // Request with valid TrustedBiddingSignals URL and non-empty keys. Request
   // should be made. The request fails.
@@ -925,8 +983,8 @@
                                   net::HTTP_NOT_FOUND);
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid("null", 1, GURL("https://response.test/"),
-                         base::TimeDelta()),
+      mojom::BidderWorkletBid::New("null", 1, GURL("https://response.test/"),
+                                   base::TimeDelta()),
       {"Failed to load "
        "https://signals.test/?hostname=top.window.test&keys=key1,key2 HTTP "
        "status = 404 Not Found."});
@@ -936,8 +994,9 @@
   AddJsonResponse(&url_loader_factory_, kFullSignalsUrl, kJson);
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
-      BidderWorklet::Bid(R"({"key1":1,"key2":[2]})", 1,
-                         GURL("https://response.test/"), base::TimeDelta()));
+      mojom::BidderWorkletBid::New(R"({"key1":1,"key2":[2]})", 1,
+                                   GURL("https://response.test/"),
+                                   base::TimeDelta()));
 }
 
 TEST_F(BidderWorkletTest, ReportWin) {
@@ -1065,7 +1124,8 @@
       // JSON values passed the generateBid() result in failures there, before
       // reportWin is called.
       RunGenerateBidWithJavascriptExpectingResult(
-          CreateBasicGenerateBidScript(), absl::nullopt /* expected_bid */);
+          CreateBasicGenerateBidScript(),
+          mojom::BidderWorkletBidPtr() /* expected_bid */);
     }
 
     *test_case.value_ptr = R"(["https://foo.test/"])";
diff --git a/content/services/auction_worklet/public/mojom/BUILD.gn b/content/services/auction_worklet/public/mojom/BUILD.gn
index 3b427b85..7627b86 100644
--- a/content/services/auction_worklet/public/mojom/BUILD.gn
+++ b/content/services/auction_worklet/public/mojom/BUILD.gn
@@ -6,7 +6,11 @@
 
 mojom("mojom") {
   generate_java = false
-  sources = [ "auction_worklet_service.mojom" ]
+  sources = [
+    "auction_worklet_service.mojom",
+    "bidder_worklet.mojom",
+    "seller_worklet.mojom",
+  ]
   deps = [
     "//mojo/public/mojom/base",
     "//services/network/public/mojom",
diff --git a/content/services/auction_worklet/public/mojom/auction_worklet_service.mojom b/content/services/auction_worklet/public/mojom/auction_worklet_service.mojom
index 2b0adb60..c3440ced 100644
--- a/content/services/auction_worklet/public/mojom/auction_worklet_service.mojom
+++ b/content/services/auction_worklet/public/mojom/auction_worklet_service.mojom
@@ -4,124 +4,114 @@
 
 module auction_worklet.mojom;
 
+import "content/services/auction_worklet/public/mojom/bidder_worklet.mojom";
+import "content/services/auction_worklet/public/mojom/seller_worklet.mojom";
 import "mojo/public/mojom/base/time.mojom";
 import "services/network/public/mojom/url_loader_factory.mojom";
 import "third_party/blink/public/mojom/interest_group/interest_group_types.mojom";
 import "url/mojom/origin.mojom";
 import "url/mojom/url.mojom";
 
-struct PreviousWin {
-  // Approximate time a particular group won an auction.
-  mojo_base.mojom.Time time;
-
-  // The ad object returned by that group's bidding function with the winning
-  // bid.
-  string ad_json;
-};
-
-// Information here corresponds to the interest group it's packaged with inside
-// BiddingInterestGroup.
-struct BiddingBrowserSignals {
-  // How many times this interest group has been joined in the period history
-  // is maintained.
-  int32 join_count;
-
-  // How many times this interest group has made bids in auctions.
-  int32 bid_count;
-
-  // Previous times the group won auctions.
-  array<PreviousWin> prev_wins;
-};
-
-struct BiddingInterestGroup {
-  blink.mojom.InterestGroup group;  // User JS specified, stored by browser.
-  BiddingBrowserSignals signals;  // Collected by browser.
-};
-
 struct BrowserSignals {
   url.mojom.Origin top_frame_origin;
   url.mojom.Origin seller;
 };
 
-struct WinningBidderReport {
-  // true if the winning bidder worklet's reporting function ran successfully
-  // and set a reporting URL.
-  bool report_requested;
-
-  // This is checked to be a https:// URL for `report_requested` to be true, but
-  // no restrictions on the destination are performed by AuctionRunner.
-  url.mojom.Url report_url;
-};
-
-struct SellerReport {
-  // true if the seller worklet's reporting function ran successfully. Unlike
-  // WinningBidderReport, this doesn't require the URL to be set and valid.
-  bool success;
-  string signals_for_winner_json;
-
-  // If this is non-empty, it's a valid https:// URL, but no restrictions on
-  // destination beyond the scheme are enforced by AuctionRunner.
-  url.mojom.Url report_url;
-};
-
-// Used to load and run FLEDGE worklets.
+// Used by the browser to load and run FLEDGE worklets in a sandboxed utility
+// process.
 // See https://github.com/WICG/turtledove/blob/main/FLEDGE.md
-//
-// This is used from the browser, by AuctionManager, and runs in a dedicated
-// utility process. The implementation is global (and stateless besides what's
-// needed to handle an individual request).
 interface AuctionWorkletService {
-  // Runs an entire FLEDGE auction.
+  // Loads a FLEDGE bidder worklet, its same-origin realtime bidding signals
+  // URL (if necessary), and invokes its generateBid() method, returning
+  // the generate bid and associated data.
   //
   // Arguments:
-  // `url_loader_factory` is used to load worklet scripts and trusted bidding
-  //  signals. It's recommended that the implementation be restricted to exactly
-  //  those URLs (keeping in mind query parameter usage for trusted bidding
-  //  signals and the allowed coalescing).
+  // `bidder_worklet` The pipe to communicate with the BidderWorklet.
+  //  Closing the pipe will abort any in-progress loads destroy the worklet. The
+  //  callback will be invoked on pipe destruction if it hasn't been already,
+  //  since it's on the AuctionWorkletService pipe instead of the BidderWorklet
+  //  pipe.
   //
-  // `auction_config` is the configuration provided by client JavaScript in
-  //  the renderer in order to initiate the auction.
+  // `url_loader_factory` The URLLoaderFactory used to load the worklet script
+  //  and trusted bidding signals. It's recommended that the implementation be
+  //  restricted to exactly those URLs (keeping in mind query parameter usage
+  //  for trusted bidding signals and the allowed coalescing).
   //
-  // `bidders` includes definitions of the interest groups that are selected to
-  //  participate in this auction (initially added by client JS in the renderer,
-  //  but managed by the browser's interest group store), as well as some
-  //  bidding history collected by the interest group store. The bidding
-  //  worklets of these groups will be fetched and executed.
+  // `bidding_interest_group` Definition of the interest group to fetch and
+  //  execute the bidding script of for an ad auction (initially added by
+  //  client JS in the renderer, but managed by the browser's interest group
+  //  store), as well as some bidding history collected by the interest group
+  //  store.
   //
-  // `browser_signals` signals from the browser about the auction that are the
-  //  same for all worklets.
+  // `auction_signals_json` The JSON representation of the auction signals for
+  //  the auction, specified by the publisher page and provided to bidder
+  //  worklets competing in an auction.
+  //
+  // `per_buyer_signals_json` The JSON representation of the auction signals
+  //  for the specific owner of this interest group, specified by the
+  //  publisher page and provided to all interest groups with the same owner
+  //  as the one specified `interest_group`.
+  //
+  // `browser_signal_top_window_origin` The origin of the top-level frame
+  //  where the auction is running.
+  //
+  // `browser_signal_seller_origin` The origin of the seller script running
+  //  the auction.
+  //
+  // `auction_start_time` The time the auction started, use to ensure the
+  //  last win times provided to all worklets are relative to the same time.
   //
   // Returns:
-  // `render_url` URL of auction winning ad to render.
-  //  An empty URL is used if there is no winner.
+  // `bid` If the worklet is successfully loaded and chooses to bid in the
+  //  auction, contains information about the bid. Null otherwise.
   //
-  // `winning_interest_group_owner` owner of the winning interest group.
-  //  An opaque origin if there is no winner.
+  // `errors` The various error messages to be used for debugging. These are too
+  //  sensitive for the renderer to see. There may be errors even when a bid
+  //  is offered, and there may be no errors when there's no bid.
   //
-  // `winning_interest_group_name` name of winning interest group.
-  //  Empty if there is no winner.
+  // TODO(mmenke): Make BidderWorklets with the same URL reuseable, both
+  // between auctions, and within an auction, if two interest groups with the
+  // same owner share a script URL, and do the same for bidding signals, at
+  // least within an auction.
+  LoadBidderWorkletAndGenerateBid(
+      pending_receiver<BidderWorklet> bidder_worklet,
+      pending_remote<network.mojom.URLLoaderFactory> url_loader_factory,
+      BiddingInterestGroup bidding_interest_group,
+      string? auction_signals_json,
+      string? per_buyer_signals_json,
+      url.mojom.Origin browser_signal_top_window_origin,
+      url.mojom.Origin browser_signal_seller_origin,
+      mojo_base.mojom.Time auction_start_time) => (
+          BidderWorkletBid? bid,
+          array<string> errors);
+
+  // Attempts to load Javascript at the specified URL and loads a SellerWorklet.
   //
-  // `bidder_report` indicates if the winning buyer wishes to make a report,
-  //  and the URL to use for that.
-  // `seller_report` indicates if the seller wishes to make a report,
-  //  and the URL that the seller wanted fetched for that.
   //
-  // `errors` are various error messages to be used for debugging. These are too
-  // sensitive for the renderers to see.
+  // Arguments:
+  // `seller_worklet` The pipe to communicate with the SellerWorklet. Closing
+  //  the pipe will abort any in-progress loads destroy the worklet. The
+  //  callback will be invoked on seller worklet destruction if it hasn't
+  //  already, since it's on the AuctionWorkletService pipe instead of the
+  //  SellerWorklet pipe.
   //
-  // TODO(mmenke):  May be good to make some way to share worklets between
-  // multiple auctions on the same page, but not auctions across pages.
-  // Could create separate top level AuctionWorkletService objects (using the
-  // same process), one for each tab, or something, to scope reused workets
-  // and bidding signals.
-  RunAuction(pending_remote<network.mojom.URLLoaderFactory> url_loader_factory,
-             blink.mojom.AuctionAdConfig auction_config,
-             array<BiddingInterestGroup> bidders,
-             BrowserSignals browser_signals) => (
-                url.mojom.Url render_url,
-                url.mojom.Origin winning_interest_group_owner,
-                string winning_interest_group_name,
-                WinningBidderReport bidder_report,
-                SellerReport seller_report,
-                array<string> errors);
+  // `url_loader_factory` The UrlLoaderFactory used to load the worklet script.
+  //  It's recommended that the implementation be restricted to only load the
+  //  script URL.
+  //
+  // `script_source_url` is the URL of the seller worklet script.
+  //
+  // Returns:
+  // `success` is true if the worklet was successfully loaded.
+  //
+  // `errors` The various error messages to be used for debugging. These are too
+  //  sensitive for the renderer to see. There may be errors even when the
+  //  worklet is successfully loaded, and there may be no errors when the load
+  //  fails.
+  LoadSellerWorklet(
+      pending_receiver<SellerWorklet> seller_worklet,
+      pending_remote<network.mojom.URLLoaderFactory> url_loader_factory,
+      url.mojom.Url script_source_url) => (
+          bool success,
+          array<string> errors);
 };
diff --git a/content/services/auction_worklet/public/mojom/bidder_worklet.mojom b/content/services/auction_worklet/public/mojom/bidder_worklet.mojom
new file mode 100644
index 0000000..24c4a154
--- /dev/null
+++ b/content/services/auction_worklet/public/mojom/bidder_worklet.mojom
@@ -0,0 +1,98 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module auction_worklet.mojom;
+
+import "mojo/public/mojom/base/time.mojom";
+import "services/network/public/mojom/url_loader_factory.mojom";
+import "third_party/blink/public/mojom/interest_group/interest_group_types.mojom";
+import "url/mojom/origin.mojom";
+import "url/mojom/url.mojom";
+
+struct PreviousWin {
+  // Approximate time a particular group won an auction.
+  //
+  // TODO(mmenke): Provide this as an integer time since an auction was won, in
+  // seconds, to reduce time resolution of cross-site information provided to an
+  // untrusted service.
+  //
+  // TODO(https://crbug.com/1207135): Decide what to do when wins are
+  // "in the future" due to clock changes.
+  mojo_base.mojom.Time time;
+
+  // The ad object returned by that group's bidding function with the winning
+  // bid.
+  string ad_json;
+};
+
+// Browser signals passed to the BidderWorklet's generateBid() method. Some
+// fields are cached to pass to the reportWin() method as well.
+struct BiddingBrowserSignals {
+  // How many times this interest group has been joined in the period history
+  // is maintained.
+  int32 join_count;
+
+  // How many times this interest group has made bids in auctions.
+  int32 bid_count;
+
+  // Previous times the group won auctions.
+  array<PreviousWin> prev_wins;
+};
+
+struct BiddingInterestGroup {
+  blink.mojom.InterestGroup group;  // User JS specified, stored by browser.
+  BiddingBrowserSignals signals;  // Collected by browser.
+};
+
+// The results of running a FLEDGE generateBid() script.
+struct BidderWorkletBid {
+  // JSON string to be passed to the scoring function.
+  string ad;
+
+  // Offered bid value. Always greater than 0.
+  double bid;
+
+  // Render URL of the bid.
+  url.mojom.Url render_url;
+
+  // How long it took to run the generateBid() script.
+  mojo_base.mojom.TimeDelta bid_duration;
+};
+
+// Manages the auction workflow for one loaded FLEDGE bidder worklet.
+// See https://github.com/WICG/turtledove/blob/main/FLEDGE.md
+//
+// The BidderWorklet is functionally stateless, so methods are
+// idempotent and can be called multiple times, in any order, for
+// multiple auctions using the same worklet. There is no need to wait
+// for one callback to be invoked before calling another method.
+interface BidderWorklet {
+  // Calls the worklet's reportWin() method. May only be called
+  // LoadBidderWorkletAndGenerateBid() has completed successfully.
+  //
+  // `seller_signals_json` is a JSON representation of the object returned by
+  // the seller worklet's ReportResult method.
+  //
+  // `browser_signal_render_url` is the `render_url` returned by the
+  // BidderWorklet's generateBid() method, invoked as part of BidderWorklet
+  // creation.
+  //
+  // `browser_signal_ad_render_fingerprint` is a hash of the rendering URL,
+  // eventually to be replaced with a hash of the ad bundle, per spec.
+  //
+  // `report_url` is the URL to request to report the result of the auction
+  // to the bidder. It will be null if no reports are requested, or the
+  // report script fails to run.
+  //
+  // `errors` is an array of any errors that occurred while attempting
+  // to run the worklet's reportWin() method. These are too sensitive for
+  // the renderer to see. There may be errors even when a `report_url` is
+  // provided, and there may be no errors when there's no `report_url`.
+  ReportWin(string seller_signals_json,
+            url.mojom.Url browser_signal_render_url,
+            string browser_signal_ad_render_fingerprint,
+            double browser_signal_bid) => (
+                url.mojom.Url? report_url,
+                array<string> errors);
+};
diff --git a/content/services/auction_worklet/public/mojom/seller_worklet.mojom b/content/services/auction_worklet/public/mojom/seller_worklet.mojom
new file mode 100644
index 0000000..00be49ed
--- /dev/null
+++ b/content/services/auction_worklet/public/mojom/seller_worklet.mojom
@@ -0,0 +1,113 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module auction_worklet.mojom;
+
+import "mojo/public/mojom/base/time.mojom";
+import "services/network/public/mojom/url_loader_factory.mojom";
+import "third_party/blink/public/mojom/interest_group/interest_group_types.mojom";
+import "url/mojom/origin.mojom";
+import "url/mojom/url.mojom";
+
+// Manages the auction workflow for one loaded FLEDGE seller worklet.
+// See https://github.com/WICG/turtledove/blob/main/FLEDGE.md
+//
+// The SellerWorklet is functionally stateless, so methods are
+// idempotent and can be called multiple times, in any order, for
+// multiple auctions using the same worklet. There is no need to wait
+// for one callback to be invoked before calling another method.
+interface SellerWorklet {
+  // Calls the Javascript scoreAd() function to evaluate a bid. May only be
+  // called once the worklet has successfully completed loading. No data is
+  // leaked between consecutive invocations of this method, or between
+  // invocations of this method and ReportResult().
+  //
+  // Arguments:
+  // `ad_metadata_json` JSON representation of the `ad` value returned by the
+  //  BidWorklet that offered the bid.
+  //
+  // `bid` The numeric value of the bid offered by the BidWorklet.
+  //
+  // `auction_config` The configuration provided by client JavaScript in the
+  //  renderer in order to initiate the auction.
+  //
+  // `browser_signal_top_window_origin` The top-level origin of the window
+  //  running the auction.
+  //
+  // `browser_signal_interest_group_owner` The owner of the interest group
+  //  that offered the bid.
+  //
+  // `browser_signal_ad_render_fingerprint` The hash of the rendering URL,
+  //  eventually to be replaced with a hash of the ad bundle, per spec.
+  //
+  // `browser_signal_bidding_duration_msecs` is the duration the BiddingWorklet
+  //  took to generate the bid. Taken as milliseconds to reduce granularity of
+  //  timing information passed to an untrusted process.
+  //
+  // Returns:
+  // `score` Non-negative score the SellerWorklet assigns to the bid. A value
+  //  of 0 indicates either an error running the script, or that the script
+  //  indicated the bid should not be used.
+  //
+  // `errors` are various error messages to be used for debugging. These are too
+  //  sensitive for the renderers to see. `errors` should not be assumed to be
+  //  empty if `score` is positive, nor should it be assumed to be non-empty if
+  //  `score` is 0.
+  ScoreAd(string ad_metadata_json,
+          double bid,
+          blink.mojom.AuctionAdConfig auction_config,
+          url.mojom.Origin browser_signal_top_window_origin,
+          url.mojom.Origin browser_signal_interest_group_owner,
+          string browser_signal_ad_render_fingerprint,
+          uint32 browser_signal_bidding_duration_msecs) =>
+              (double score, array<string> errors);
+
+  // Calls the Javascript reportResult() function to get the information needed
+  // to report the result of the auction to the seller. May only be called once
+  // the worklet has successfully completed loading. No data is leaked between
+  // consecutive invocations of this method, or between invocations of this
+  // method and ScoreAd().
+  //
+  // Arguments:
+  // `auction_config` The configuration provided by client JavaScript in the
+  //  renderer in order to initiate the auction.
+  //
+  // `browser_signal_top_window_origin` The top-level origin of the window
+  //  running the auction.
+  //
+  // `browser_signal_interest_group_owner` The owner of the interest group
+  //  that offered the winning bid.
+  //
+  // `browser_signal_render_url` The render URL provided by the winning bid.
+  //
+  // `browser_signal_ad_render_fingerprint` The hash of the rendering URL,
+  //  eventually to be replaced with a hash of the ad bundle, per spec.
+  //
+  // `browser_signal_bid` The numeric value of the winning bid.
+  //
+  // `browser_signal_desirability` The score returned by ScoreAd for the
+  //  the winning bid.
+  //
+  // Returns:
+  // `signals_for_winner` The value to pass to the winning bidder's
+  //  ReportWin function, as a JSON string. Null if no value is provided.
+  //
+  // `report_url` The URL to request to report the result of the auction to the
+  //  seller, if any.
+  //
+  // `errors` are various error messages to be used for debugging. These are too
+  //  sensitive for the renderers to see. `errors` should not be assumed to be
+  //  empty if the other values are populated, nor should it be assumed to be
+  //  non-empty if the other values are null.
+  ReportResult(blink.mojom.AuctionAdConfig auction_config,
+               url.mojom.Origin browser_signal_top_window_origin,
+               url.mojom.Origin browser_signal_interest_group_owner,
+               url.mojom.Url browser_signal_render_url,
+               string browser_signal_ad_render_fingerprint,
+               double browser_signal_bid,
+               double browser_signal_desirability) =>
+                   (string? signals_for_winner,
+                    url.mojom.Url? report_url,
+                    array<string> error_msgs);
+};
diff --git a/content/services/auction_worklet/seller_worklet.cc b/content/services/auction_worklet/seller_worklet.cc
index b4b1668..cb20351 100644
--- a/content/services/auction_worklet/seller_worklet.cc
+++ b/content/services/auction_worklet/seller_worklet.cc
@@ -17,10 +17,13 @@
 #include "base/time/time.h"
 #include "content/services/auction_worklet/auction_v8_helper.h"
 #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
+#include "content/services/auction_worklet/public/mojom/seller_worklet.mojom.h"
 #include "content/services/auction_worklet/report_bindings.h"
 #include "content/services/auction_worklet/worklet_loader.h"
 #include "gin/converter.h"
 #include "gin/dictionary.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
 #include "url/gurl.h"
 #include "url/origin.h"
 #include "v8/include/v8.h"
@@ -132,25 +135,39 @@
 
 SellerWorklet::SellerWorklet(
     AuctionV8Helper* v8_helper,
-    network::mojom::URLLoaderFactory* url_loader_factory,
+    mojo::PendingRemote<network::mojom::URLLoaderFactory>
+        pending_url_loader_factory,
     const GURL& script_source_url,
-    LoadWorkletCallback load_worklet_callback)
+    mojom::AuctionWorkletService::LoadSellerWorkletCallback
+        load_worklet_callback)
     : v8_helper_(v8_helper),
       script_source_url_(script_source_url),
       load_worklet_callback_(std::move(load_worklet_callback)) {
   DCHECK(load_worklet_callback_);
+
+  // Bind URLLoaderFactory. Remote is not needed after this method completes,
+  // since requests will continue after the URLLoaderFactory pipe has been
+  // closed, so no need to keep it around after requests have been issued.
+  mojo::Remote<network::mojom::URLLoaderFactory> url_loader_factory(
+      std::move(pending_url_loader_factory));
+
   worklet_loader_ = std::make_unique<WorkletLoader>(
-      url_loader_factory, script_source_url, v8_helper,
+      url_loader_factory.get(), script_source_url, v8_helper,
       base::BindOnce(&SellerWorklet::OnDownloadComplete,
                      base::Unretained(this)));
 }
 
-SellerWorklet::~SellerWorklet() = default;
+SellerWorklet::~SellerWorklet() {
+  if (load_worklet_callback_) {
+    std::move(load_worklet_callback_)
+        .Run(false /* success */, std::vector<std::string>() /* errors */);
+  }
+}
 
 void SellerWorklet::ScoreAd(
     const std::string& ad_metadata_json,
     double bid,
-    const blink::mojom::AuctionAdConfig& auction_config,
+    blink::mojom::AuctionAdConfigPtr auction_config,
     const url::Origin& browser_signal_top_window_origin,
     const url::Origin& browser_signal_interest_group_owner,
     const std::string& browser_signal_ad_render_fingerprint,
@@ -174,7 +191,7 @@
 
   args.push_back(gin::ConvertToV8(isolate, bid));
 
-  if (!AppendAuctionConfig(v8_helper_, context, auction_config, &args)) {
+  if (!AppendAuctionConfig(v8_helper_, context, *auction_config, &args)) {
     std::move(callback).Run(0 /* score */,
                             std::vector<std::string>() /* errors */);
     return;
@@ -233,7 +250,7 @@
 }
 
 void SellerWorklet::ReportResult(
-    const blink::mojom::AuctionAdConfig& auction_config,
+    blink::mojom::AuctionAdConfigPtr auction_config,
     const url::Origin& browser_signal_top_window_origin,
     const url::Origin& browser_signal_interest_group_owner,
     const GURL& browser_signal_render_url,
@@ -257,7 +274,7 @@
   v8::Context::Scope context_scope(context);
 
   std::vector<v8::Local<v8::Value>> args;
-  if (!AppendAuctionConfig(v8_helper_, context, auction_config, &args)) {
+  if (!AppendAuctionConfig(v8_helper_, context, *auction_config, &args)) {
     std::move(callback).Run(absl::nullopt /* signals_for_winner */,
                             absl::nullopt /* report_url */,
                             std::vector<std::string>() /* errors */);
diff --git a/content/services/auction_worklet/seller_worklet.h b/content/services/auction_worklet/seller_worklet.h
index db1c4bb..7abd3c7 100644
--- a/content/services/auction_worklet/seller_worklet.h
+++ b/content/services/auction_worklet/seller_worklet.h
@@ -12,6 +12,9 @@
 
 #include "base/callback.h"
 #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom-forward.h"
+#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
+#include "content/services/auction_worklet/public/mojom/seller_worklet.mojom.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
 #include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom-forward.h"
@@ -27,71 +30,40 @@
 // Represents a seller worklet for FLEDGE
 // (https://github.com/WICG/turtledove/blob/main/FLEDGE.md). Loads and runs the
 // seller worklet's Javascript.
-class SellerWorklet {
+class SellerWorklet : public mojom::SellerWorklet {
  public:
-  using LoadWorkletCallback =
-      base::OnceCallback<void(bool success,
-                              const std::vector<std::string>& errors)>;
-
-  // Callback for ScoreAd(). On success, `score` is the positive score returned
-  // by the script. On failure, it's 0. `errors` is a vector of any errors that
-  // occurred while running the script.
-  using ScoreAdCallback =
-      base::OnceCallback<void(double score,
-                              const std::vector<std::string>& errors)>;
-
-  // Callback for ReportResult().
-  //
-  // `signals_for_winner` is JSON data as a string that should be sent to the
-  // winning bidder worklet's reportWin() function. It's empty if the script
-  // failed to run or threw an exception, and "null" if the return value wasn't
-  // a valid JSON object.
-  //
-  // `report_url` is the report URL provided by the script, if one is provided.
-  // nullopt on failure, or if no report URL is provided.
-  //
-  // `errors` is a vector of any errors that occurred while running the
-  // script.
-  using ReportResultCallback = base::OnceCallback<void(
-      const absl::optional<std::string>& signals_for_winner,
-      const absl::optional<GURL>& report_url,
-      const std::vector<std::string>& errors)>;
-
   // Starts loading the worklet script on construction. Callback will be invoked
   // asynchronously once the data has been fetched or an error has occurred.
   // Must be destroyed before `v8_helper`.
   SellerWorklet(AuctionV8Helper* v8_helper,
-                network::mojom::URLLoaderFactory* url_loader_factory,
+                mojo::PendingRemote<network::mojom::URLLoaderFactory>
+                    pending_url_loader_factory,
                 const GURL& script_source_url,
-                LoadWorkletCallback load_worklet_callback);
+                mojom::AuctionWorkletService::LoadSellerWorkletCallback
+                    load_worklet_callback);
+
   explicit SellerWorklet(const SellerWorklet&) = delete;
   SellerWorklet& operator=(const SellerWorklet&) = delete;
-  ~SellerWorklet();
 
-  // Calls scoreAd(), and invokes passed in callback asynchronously with the
-  // resulting score. May only be called once the worklet has successfully
-  // completed loaded. No data is leaked between consecutive invocations of this
-  // method, or between invocations of this method and ReportResult().
+  ~SellerWorklet() override;
+
+  // mojom::SellerWorklet implementation:
   void ScoreAd(const std::string& ad_metadata_json,
                double bid,
-               const blink::mojom::AuctionAdConfig& auction_config,
+               blink::mojom::AuctionAdConfigPtr auction_config,
                const url::Origin& browser_signal_top_window_origin,
                const url::Origin& browser_signal_interest_group_owner,
                const std::string& browser_signal_ad_render_fingerprint,
                uint32_t browser_signal_bidding_duration_msecs,
-               ScoreAdCallback callback);
-
-  // Calls reportResult(), and invokes passed in callback asynchronously with
-  // the reporting information. May only be called once the worklet has
-  // successfully loaded.
-  void ReportResult(const blink::mojom::AuctionAdConfig& auction_config,
+               ScoreAdCallback callback) override;
+  void ReportResult(blink::mojom::AuctionAdConfigPtr auction_config,
                     const url::Origin& browser_signal_top_window_origin,
                     const url::Origin& browser_signal_interest_group_owner,
                     const GURL& browser_signal_render_url,
                     const std::string& browser_signal_ad_render_fingerprint,
                     double browser_signal_bid,
                     double browser_signal_desirability,
-                    ReportResultCallback callback);
+                    ReportResultCallback callback) override;
 
  private:
   void OnDownloadComplete(
@@ -103,7 +75,8 @@
   const GURL script_source_url_;
   std::unique_ptr<WorkletLoader> worklet_loader_;
 
-  LoadWorkletCallback load_worklet_callback_;
+  mojom::AuctionWorkletService::LoadSellerWorkletCallback
+      load_worklet_callback_;
   // Compiled script, not bound to any context. Can be repeatedly bound to
   // different context and executed, without persisting any state.
   std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script_;
diff --git a/content/services/auction_worklet/seller_worklet_unittest.cc b/content/services/auction_worklet/seller_worklet_unittest.cc
index f3b618c..97886b3 100644
--- a/content/services/auction_worklet/seller_worklet_unittest.cc
+++ b/content/services/auction_worklet/seller_worklet_unittest.cc
@@ -16,6 +16,7 @@
 #include "content/services/auction_worklet/auction_v8_helper.h"
 #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
 #include "content/services/auction_worklet/worklet_test_util.h"
+#include "mojo/public/cpp/bindings/self_owned_receiver.h"
 #include "net/http/http_status_code.h"
 #include "services/network/test/test_url_loader_factory.h"
 #include "testing/gmock/include/gmock/gmock-matchers.h"
@@ -121,12 +122,12 @@
 
     base::RunLoop run_loop;
     seller_worket->ScoreAd(
-        ad_metadata_, bid_, *auction_config_, browser_signal_top_window_origin_,
-        browser_signal_interest_group_owner_,
+        ad_metadata_, bid_, auction_config_.Clone(),
+        browser_signal_top_window_origin_, browser_signal_interest_group_owner_,
         browser_signal_ad_render_fingerprint_,
         browser_signal_bidding_duration_msecs_,
         base::BindLambdaForTesting(
-            [&run_loop, &expected_score, expected_errors](
+            [&run_loop, &expected_score, &expected_errors](
                 double score, const std::vector<std::string>& errors) {
               EXPECT_EQ(expected_score, score);
               EXPECT_EQ(expected_errors, errors);
@@ -176,7 +177,7 @@
 
     base::RunLoop run_loop;
     seller_worket->ReportResult(
-        *auction_config_, browser_signal_top_window_origin_,
+        auction_config_.Clone(), browser_signal_top_window_origin_,
         browser_signal_interest_group_owner_, browser_signal_render_url_,
         browser_signal_ad_render_fingerprint_, bid_,
         browser_signal_desireability_,
@@ -195,24 +196,30 @@
   }
 
   // Create a SellerWorklet, waiting for the URLLoader to complete. Returns
-  // nullptr on failure.
-  std::unique_ptr<SellerWorklet> CreateWorklet() {
+  // a null Remote on failure.
+  mojo::Remote<mojom::SellerWorklet> CreateWorklet() {
     CHECK(!load_script_run_loop_);
 
+    mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
+    url_loader_factory_.Clone(
+        url_loader_factory.InitWithNewPipeAndPassReceiver());
+
     create_worklet_succeeded_ = false;
-    auto seller_worket = std::make_unique<SellerWorklet>(
-        &v8_helper_, &url_loader_factory_, url_,
-        base::BindOnce(&SellerWorkletTest::CreateWorkletCallback,
-                       base::Unretained(this)));
+    mojo::Remote<mojom::SellerWorklet> seller_worklet;
+    mojo::MakeSelfOwnedReceiver(
+        std::make_unique<SellerWorklet>(
+            &v8_helper_, std::move(url_loader_factory), url_,
+            base::BindOnce(&SellerWorkletTest::CreateWorkletCallback,
+                           base::Unretained(this))),
+        seller_worklet.BindNewPipeAndPassReceiver());
     load_script_run_loop_ = std::make_unique<base::RunLoop>();
     load_script_run_loop_->Run();
     load_script_run_loop_.reset();
     if (!create_worklet_succeeded_)
-      return nullptr;
-    return seller_worket;
+      return mojo::Remote<mojom::SellerWorklet>();
+    return seller_worklet;
   }
 
- protected:
   void CreateWorkletCallback(bool success,
                              const std::vector<std::string>& errors) {
     create_worklet_succeeded_ = success;
@@ -222,6 +229,7 @@
     load_script_run_loop_->Quit();
   }
 
+ protected:
   base::test::TaskEnvironment task_environment_;
 
   const GURL url_ = GURL("https://url.test/");
@@ -251,6 +259,30 @@
   AuctionV8Helper v8_helper_;
 };
 
+// Test the case the SellerWorklet pipe is closed before invoking the
+// LoadSellerWorkletCallback. The LoadSellerWorkletCallback should be invoked,
+// and there should be no Mojo exception due to destroying the creation callback
+// without invoking it.
+TEST_F(SellerWorkletTest, PipeClosed) {
+  mojo::Remote<mojom::SellerWorklet> seller_worklet;
+  mojo::PendingReceiver<network::mojom::URLLoaderFactory>
+      url_loader_factory_receiver;
+
+  mojo::MakeSelfOwnedReceiver(
+      std::make_unique<SellerWorklet>(
+          &v8_helper_,
+          url_loader_factory_receiver.InitWithNewPipeAndPassRemote(), url_,
+          base::BindOnce(&SellerWorkletTest::CreateWorkletCallback,
+                         base::Unretained(this))),
+      seller_worklet.BindNewPipeAndPassReceiver());
+  load_script_run_loop_ = std::make_unique<base::RunLoop>();
+  seller_worklet.reset();
+
+  load_script_run_loop_->Run();
+  load_script_run_loop_.reset();
+  EXPECT_FALSE(create_worklet_succeeded_);
+}
+
 TEST_F(SellerWorkletTest, NetworkError) {
   url_loader_factory_.AddResponse(url_.spec(), CreateBasicSellAdScript(),
                                   net::HTTP_NOT_FOUND);
@@ -654,7 +686,7 @@
     for (int j = 0; j < 2; ++j) {
       base::RunLoop run_loop;
       seller_worket->ScoreAd(
-          ad_metadata_, bid_, *auction_config_,
+          ad_metadata_, bid_, auction_config_.Clone(),
           browser_signal_top_window_origin_,
           browser_signal_interest_group_owner_,
           browser_signal_ad_render_fingerprint_,
@@ -672,7 +704,7 @@
     for (int j = 0; j < 2; ++j) {
       base::RunLoop run_loop;
       seller_worket->ReportResult(
-          *auction_config_, browser_signal_top_window_origin_,
+          auction_config_.Clone(), browser_signal_top_window_origin_,
           browser_signal_interest_group_owner_, browser_signal_render_url_,
           browser_signal_ad_render_fingerprint_, bid_,
           browser_signal_desireability_,
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 6244f57..a68b7f49 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -1040,6 +1040,7 @@
     "../browser/browsing_data/same_site_data_remover_impl_browsertest.cc",
     "../browser/child_process_launcher_browsertest.cc",
     "../browser/child_process_security_policy_browsertest.cc",
+    "../browser/compute_pressure/compute_pressure_origin_trial_browsertest.cc",
     "../browser/content_index/content_index_browsertest.cc",
     "../browser/content_security_policy_browsertest.cc",
     "../browser/conversions/conversion_internals_browsertest.cc",
@@ -1954,6 +1955,7 @@
     "../browser/indexed_db/mock_mojo_indexed_db_callbacks.h",
     "../browser/indexed_db/mock_mojo_indexed_db_database_callbacks.cc",
     "../browser/indexed_db/mock_mojo_indexed_db_database_callbacks.h",
+    "../browser/interest_group/auction_runner_unittest.cc",
     "../browser/interest_group/auction_url_loader_factory_proxy_unittest.cc",
     "../browser/interest_group/interest_group_storage_unittest.cc",
     "../browser/loader/cors_origin_pattern_setter_unittest.cc",
@@ -2346,6 +2348,7 @@
     "//content/public/common:trust_tokens_mojo_bindings",
     "//content/public/renderer",
     "//content/renderer:for_content_tests",
+    "//content/services/auction_worklet",
     "//content/services/auction_worklet:tests",
     "//content/services/auction_worklet/public/mojom",
     "//crypto",
diff --git a/content/test/data/compute_pressure/no_token.html b/content/test/data/compute_pressure/no_token.html
new file mode 100644
index 0000000..47ff433
--- /dev/null
+++ b/content/test/data/compute_pressure/no_token.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Compute Pressure API No Origin Trial Test</title>
diff --git a/content/test/data/compute_pressure/valid_token.html b/content/test/data/compute_pressure/valid_token.html
new file mode 100644
index 0000000..4512f81
--- /dev/null
+++ b/content/test/data/compute_pressure/valid_token.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<meta charset="utf-8">
+<!-- The OT token below expires in 2033.
+   Regenerate this token with the command:
+   generate_token.py --expire-timestamp=2000000000 https://example.test ComputePressure -->
+<meta http-equiv="origin-trial" content="A/jtk5npXPKyDrNo9oTFvtpkxme5321eVz7rRB35qNSG+U00gtajlCmSfhxTxUDuAb0umALAsiXoKIfMHl3mnQMAAABaeyJvcmlnaW4iOiAiaHR0cHM6Ly9leGFtcGxlLnRlc3Q6NDQzIiwgImZlYXR1cmUiOiAiQ29tcHV0ZVByZXNzdXJlIiwgImV4cGlyeSI6IDIwMDAwMDAwMDB9">
+<title>Compute Pressure API Origin Trial Test</title>
diff --git a/content/test/gpu/gpu_tests/gpu_process_integration_test.py b/content/test/gpu/gpu_tests/gpu_process_integration_test.py
index a5c9fff..121b8aa0 100644
--- a/content/test/gpu/gpu_tests/gpu_process_integration_test.py
+++ b/content/test/gpu/gpu_tests/gpu_process_integration_test.py
@@ -464,7 +464,6 @@
           'OES_vertex_array_object',
           'WEBGL_compressed_texture_etc1',
           'WEBGL_debug_renderer_info',
-          'WEBGL_debug_shaders',
           'WEBGL_depth_texture',
           'WEBKIT_WEBGL_depth_texture',
           'WEBGL_draw_buffers',
diff --git a/content/test/gpu/gpu_tests/webcodecs_integration_test.py b/content/test/gpu/gpu_tests/webcodecs_integration_test.py
index 27baa52..0be2686 100644
--- a/content/test/gpu/gpu_tests/webcodecs_integration_test.py
+++ b/content/test/gpu/gpu_tests/webcodecs_integration_test.py
@@ -50,7 +50,7 @@
     arg_obj = args[0]
     tab.Navigate(url)
     tab.action_runner.WaitForJavaScriptCondition(
-        'document.readyState == "complete"', timeout=5)
+        'document.readyState == "complete"')
     tab.EvaluateJavaScript('TEST.run(' + str(arg_obj) + ')')
     tab.action_runner.WaitForJavaScriptCondition('TEST.finished', timeout=60)
     if not tab.EvaluateJavaScript('TEST.success'):
diff --git a/device/fido/cable/fido_cable_discovery.cc b/device/fido/cable/fido_cable_discovery.cc
index 50f2eac..3147400 100644
--- a/device/fido/cable/fido_cable_discovery.cc
+++ b/device/fido/cable/fido_cable_discovery.cc
@@ -562,6 +562,8 @@
 
 absl::optional<FidoCableDiscovery::V1DiscoveryDataAndEID>
 FidoCableDiscovery::GetCableDiscoveryData(const BluetoothDevice* device) {
+  const std::vector<uint8_t>* const service_data =
+      device->GetServiceDataForUUID(CableAdvertisementUUID());
   absl::optional<CableEidArray> maybe_eid_from_service_data =
       MaybeGetEidFromServiceData(device);
   std::vector<CableEidArray> uuids = GetUUIDs(device);
@@ -592,6 +594,8 @@
         GetCableDiscoveryDataFromAuthenticatorEid(*maybe_eid_from_service_data);
     FIDO_LOG(DEBUG) << "  Service data: "
                     << ResultDebugString(*maybe_eid_from_service_data, result);
+  } else if (service_data) {
+    FIDO_LOG(DEBUG) << "  Service data: " << base::HexEncode(*service_data);
   } else {
     FIDO_LOG(DEBUG) << "  Service data: <none>";
   }
@@ -622,11 +626,12 @@
     }
   }
 
-  // Try all combinations of 16- and 4-byte UUIDs to form 20-byte advert
-  // payloads. (We don't know if something in the BLE stack might add other
-  // short UUIDs to a BLE advert message).
   if (advert_callback_) {
     std::array<uint8_t, 16 + 4> v2_advert;
+
+    // Try all combinations of 16- and 4-byte UUIDs to form 20-byte advert
+    // payloads. (We don't know if something in the BLE stack might add other
+    // short UUIDs to a BLE advert message).
     for (const auto& uuid128 : uuid128s) {
       static_assert(EXTENT(uuid128) == 16, "");
       memcpy(v2_advert.data(), uuid128.data(), 16);
@@ -637,6 +642,11 @@
         advert_callback_.Run(v2_advert);
       }
     }
+
+    if (service_data && service_data->size() == v2_advert.size()) {
+      memcpy(v2_advert.data(), service_data->data(), v2_advert.size());
+      advert_callback_.Run(v2_advert);
+    }
   }
 
   auto observed_data = std::make_unique<ObservedDeviceData>();
@@ -650,7 +660,7 @@
 // static
 absl::optional<CableEidArray> FidoCableDiscovery::MaybeGetEidFromServiceData(
     const BluetoothDevice* device) {
-  const auto* service_data =
+  const std::vector<uint8_t>* service_data =
       device->GetServiceDataForUUID(CableAdvertisementUUID());
   if (!service_data) {
     return absl::nullopt;
diff --git a/docs/accessibility/select_to_speak.md b/docs/accessibility/select_to_speak.md
index 44641f6f..92a2589 100644
--- a/docs/accessibility/select_to_speak.md
+++ b/docs/accessibility/select_to_speak.md
@@ -47,7 +47,7 @@
 
 - An event handler, ash/events/select_to_speak_event_handler.h
 
-- The status tray button, ash/system/accessibility/select_to_speak_tray.h
+- The status tray button, ash/system/accessibility/select_to_speak/select_to_speak_tray.h
 
 - Floating panel, system/accessibility/select_to_speak_menu_bubble_controller.h
 
@@ -202,7 +202,7 @@
 #### Floating control panel
 
 The panel is implemented as a native ASH component
-[select_to_speak_menu_bubble_controller.h](https://source.chromium.org/chromium/chromium/src/+/master:ash/system/accessibility/select_to_speak_menu_bubble_controller.h).
+[select_to_speak_menu_bubble_controller.h](https://source.chromium.org/chromium/chromium/src/+/main:ash/system/accessibility/select_to_speak/select_to_speak_menu_bubble_controller.h).
 Similar to focus rings, the STS component extension communicates with the panel
 via the `chrome.accessibilityPrivate` API. The
 `chrome.accessibilityPrivate.updateSelectToSpeakPanel` API controls the
@@ -262,7 +262,7 @@
 
 Users can navigate to adjacent paragraphs from the current block parent when
 Select-to-speak is active. A 'paragraph' is any block element as defined by
-[ParagraphUtils.isBlock](https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/resources/chromeos/accessibility/select_to_speak/paragraph_utils.js)
+[ParagraphUtils.isBlock](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/resources/chromeos/accessibility/select_to_speak/paragraph_utils.js)
 and the navigation occurs in DOM-order.
 
 #### Sentence navigation
@@ -270,7 +270,7 @@
 Paragraphs are split into sentences based on the `sentenceStarts` property of
 an AutomationNode. Users can skip to previous and next sentences using similar
 technique as pause/resume (`stop` then `speak` with trimmed text). See
-[sentence_utils.js](https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/resources/chromeos/accessibility/select_to_speak/sentence_utils.js)
+[sentence_utils.js](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/resources/chromeos/accessibility/select_to_speak/sentence_utils.js)
 for logic on breaking node groups into sentences.
 
 #### Reading speed
@@ -316,4 +316,4 @@
 [go/chrome-sts-sentences-and-words](go/chrome-sts-sentences-and-words) and
 [go/chromeos-sts-highlight](go/chromeos-sts-highlight)
 
-- Navigation features, [go/enhanced-sts-dd](go/enhanced-sts-dd)
\ No newline at end of file
+- Navigation features, [go/enhanced-sts-dd](go/enhanced-sts-dd)
diff --git a/extensions/browser/api/app_window/app_window_apitest.cc b/extensions/browser/api/app_window/app_window_apitest.cc
index d4e412fa..20e26ce 100644
--- a/extensions/browser/api/app_window/app_window_apitest.cc
+++ b/extensions/browser/api/app_window/app_window_apitest.cc
@@ -66,67 +66,65 @@
 #endif  // defined(OS_LINUX) || defined(OS_CHROMEOS)
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, MAYBE_OnMinimizedEvent) {
-  EXPECT_TRUE(RunExtensionTest({.name = "platform_apps/windows_api_properties",
-                                .custom_arg = "minimized"}))
+  EXPECT_TRUE(RunExtensionTest("platform_apps/windows_api_properties",
+                               {.custom_arg = "minimized"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, MAYBE_OnMaximizedEvent) {
-  EXPECT_TRUE(RunExtensionTest({.name = "platform_apps/windows_api_properties",
-                                .custom_arg = "maximized"}))
+  EXPECT_TRUE(RunExtensionTest("platform_apps/windows_api_properties",
+                               {.custom_arg = "maximized"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, MAYBE_OnRestoredEvent) {
-  EXPECT_TRUE(RunExtensionTest({.name = "platform_apps/windows_api_properties",
-                                .custom_arg = "restored"}))
+  EXPECT_TRUE(RunExtensionTest("platform_apps/windows_api_properties",
+                               {.custom_arg = "restored"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, OnBoundsChangedEvent) {
-  EXPECT_TRUE(RunExtensionTest({.name = "platform_apps/windows_api_properties",
-                                .custom_arg = "boundsChanged"}))
+  EXPECT_TRUE(RunExtensionTest("platform_apps/windows_api_properties",
+                               {.custom_arg = "boundsChanged"}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, AlwaysOnTopWithPermissions) {
   EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_always_on_top/has_permissions",
-       .launch_as_platform_app = true}))
+      "platform_apps/windows_api_always_on_top/has_permissions",
+      {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, AlwaysOnTopWithOldPermissions) {
   EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_always_on_top/has_old_permissions",
-       .launch_as_platform_app = true}))
+      "platform_apps/windows_api_always_on_top/has_old_permissions",
+      {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, AlwaysOnTopNoPermissions) {
-  EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_always_on_top/no_permissions",
-       .launch_as_platform_app = true}))
+  EXPECT_TRUE(
+      RunExtensionTest("platform_apps/windows_api_always_on_top/no_permissions",
+                       {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, Get) {
-  EXPECT_TRUE(RunExtensionTest({.name = "platform_apps/windows_api_get",
-                                .launch_as_platform_app = true}))
+  EXPECT_TRUE(RunExtensionTest("platform_apps/windows_api_get",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, SetShapeHasPerm) {
-  EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_shape/has_permission",
-       .launch_as_platform_app = true}))
+  EXPECT_TRUE(RunExtensionTest("platform_apps/windows_api_shape/has_permission",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, SetShapeNoPerm) {
-  EXPECT_TRUE(
-      RunExtensionTest({.name = "platform_apps/windows_api_shape/no_permission",
-                        .launch_as_platform_app = true}))
+  EXPECT_TRUE(RunExtensionTest("platform_apps/windows_api_shape/no_permission",
+                               {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -156,67 +154,65 @@
 #endif  // OS_WIN
 #endif  // USE_AURA && !(OS_LINUX || IS_CHROMEOS_LACROS)
 
-  EXPECT_TRUE(
-      RunExtensionTest({.name = test_dir, .launch_as_platform_app = true}))
+  EXPECT_TRUE(RunExtensionTest(test_dir, {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, AlphaEnabledNoPermissions) {
-  EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_alpha_enabled/no_permissions",
-       .launch_as_platform_app = true}))
+  EXPECT_TRUE(
+      RunExtensionTest("platform_apps/windows_api_alpha_enabled/no_permissions",
+                       {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, AlphaEnabledInStable) {
   extensions::ScopedCurrentChannel channel(version_info::Channel::STABLE);
-  EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_alpha_enabled/in_stable",
-       .launch_as_platform_app = true},
-      // Ignore manifest warnings because the extension will not load at all
-      // in stable.
-      {.ignore_manifest_warnings = true}))
+  EXPECT_TRUE(
+      RunExtensionTest("platform_apps/windows_api_alpha_enabled/in_stable",
+                       {.launch_as_platform_app = true},
+                       // Ignore manifest warnings because the extension will
+                       // not load at all in stable.
+                       {.ignore_manifest_warnings = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, AlphaEnabledWrongFrameType) {
   EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_alpha_enabled/wrong_frame_type",
-       .launch_as_platform_app = true}))
+      "platform_apps/windows_api_alpha_enabled/wrong_frame_type",
+      {.launch_as_platform_app = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, VisibleOnAllWorkspacesInStable) {
   extensions::ScopedCurrentChannel channel(version_info::Channel::STABLE);
   EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_visible_on_all_workspaces/in_stable",
-       .launch_as_platform_app = true}))
+      "platform_apps/windows_api_visible_on_all_workspaces/in_stable",
+      {.launch_as_platform_app = true}))
       << message_;
 }
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, ImeWindowHasPermissions) {
   EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_ime/has_permissions_whitelisted"},
+      "platform_apps/windows_api_ime/has_permissions_whitelisted", {},
       {.load_as_component = true}))
       << message_;
 
   EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_ime/has_permissions_platform_app",
-       .launch_as_platform_app = true},
-      {.ignore_manifest_warnings = true}))
+      "platform_apps/windows_api_ime/has_permissions_platform_app",
+      {.launch_as_platform_app = true}, {.ignore_manifest_warnings = true}))
       << message_;
 }
 
 IN_PROC_BROWSER_TEST_F(AppWindowApiTest, ImeWindowNoPermissions) {
   EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_ime/no_permissions_whitelisted"},
+      "platform_apps/windows_api_ime/no_permissions_whitelisted", {},
       {.load_as_component = true}))
       << message_;
 
   EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_ime/no_permissions_platform_app",
-       .launch_as_platform_app = true}))
+      "platform_apps/windows_api_ime/no_permissions_platform_app",
+      {.launch_as_platform_app = true}))
       << message_;
 }
 
@@ -227,7 +223,7 @@
                                   "jkghodnilhceideoidjikpgommlajknk");
 
   EXPECT_TRUE(RunExtensionTest(
-      {.name = "platform_apps/windows_api_ime/forced_app_mode_not_fullscreen"},
+      "platform_apps/windows_api_ime/forced_app_mode_not_fullscreen", {},
       {.load_as_component = true}))
       << message_;
 }
diff --git a/extensions/browser/api/audio/audio_api.cc b/extensions/browser/api/audio/audio_api.cc
index cd96c7b..ef792c20 100644
--- a/extensions/browser/api/audio/audio_api.cc
+++ b/extensions/browser/api/audio/audio_api.cc
@@ -91,7 +91,7 @@
 
   auto event = std::make_unique<Event>(events::AUDIO_ON_DEVICE_CHANGED,
                                        audio::OnDeviceChanged::kEventName,
-                                       std::make_unique<base::ListValue>());
+                                       std::vector<base::Value>());
   event->will_dispatch_callback =
       base::BindRepeating(&CanReceiveDeprecatedAudioEvent);
   event_router->BroadcastEvent(std::move(event));
diff --git a/extensions/browser/api/clipboard/clipboard_api.cc b/extensions/browser/api/clipboard/clipboard_api.cc
index 633d2495..18e4e50 100644
--- a/extensions/browser/api/clipboard/clipboard_api.cc
+++ b/extensions/browser/api/clipboard/clipboard_api.cc
@@ -44,7 +44,7 @@
     std::unique_ptr<Event> event(
         new Event(events::CLIPBOARD_ON_CLIPBOARD_DATA_CHANGED,
                   clipboard::OnClipboardDataChanged::kEventName,
-                  std::make_unique<base::ListValue>()));
+                  std::vector<base::Value>()));
     router->BroadcastEvent(std::move(event));
   }
 }
diff --git a/extensions/browser/api/declarative_net_request/action_tracker.cc b/extensions/browser/api/declarative_net_request/action_tracker.cc
index 969fee0..db5f31bc 100644
--- a/extensions/browser/api/declarative_net_request/action_tracker.cc
+++ b/extensions/browser/api/declarative_net_request/action_tracker.cc
@@ -431,8 +431,9 @@
   matched_rule_info_debug.rule = std::move(matched_rule);
   matched_rule_info_debug.request = std::move(request_details);
 
-  auto args = std::make_unique<base::ListValue>();
-  args->Append(matched_rule_info_debug.ToValue());
+  std::vector<base::Value> args;
+  args.push_back(
+      base::Value::FromUniquePtrValue(matched_rule_info_debug.ToValue()));
 
   auto event = std::make_unique<Event>(
       events::DECLARATIVE_NET_REQUEST_ON_RULE_MATCHED_DEBUG,
diff --git a/extensions/browser/api/management/management_api.cc b/extensions/browser/api/management/management_api.cc
index e9abff54..84d1b025 100644
--- a/extensions/browser/api/management/management_api.cc
+++ b/extensions/browser/api/management/management_api.cc
@@ -1094,12 +1094,12 @@
     const char* event_name) {
   if (!extension->ShouldExposeViaManagementAPI())
     return;
-  std::unique_ptr<base::ListValue> args(new base::ListValue());
+  std::vector<base::Value> args;
   if (event_name == management::OnUninstalled::kEventName) {
-    args->AppendString(extension->id());
+    args.push_back(base::Value(extension->id()));
   } else {
-    args->Append(
-        CreateExtensionInfo(nullptr, *extension, browser_context_).ToValue());
+    args.push_back(base::Value::FromUniquePtrValue(
+        CreateExtensionInfo(nullptr, *extension, browser_context_).ToValue()));
   }
 
   EventRouter::Get(browser_context_)
diff --git a/extensions/browser/api/printer_provider/printer_provider_api.cc b/extensions/browser/api/printer_provider/printer_provider_api.cc
index 9bacc4d..e828002 100644
--- a/extensions/browser/api/printer_provider/printer_provider_api.cc
+++ b/extensions/browser/api/printer_provider/printer_provider_api.cc
@@ -528,10 +528,10 @@
   // be needed later on.
   int request_id = pending_get_printers_requests_.Add(callback);
 
-  std::unique_ptr<base::ListValue> internal_args(new base::ListValue);
+  std::vector<base::Value> internal_args;
   // Request id is not part of the public API, but it will be massaged out in
   // custom bindings.
-  internal_args->AppendInteger(request_id);
+  internal_args.push_back(base::Value(request_id));
 
   std::unique_ptr<Event> event(
       new Event(events::PRINTER_PROVIDER_ON_GET_PRINTERS_REQUESTED,
@@ -567,11 +567,11 @@
   int request_id =
       pending_capability_requests_[extension_id].Add(std::move(callback));
 
-  std::unique_ptr<base::ListValue> internal_args(new base::ListValue);
+  std::vector<base::Value> internal_args;
   // Request id is not part of the public API, but it will be massaged out in
   // custom bindings.
-  internal_args->AppendInteger(request_id);
-  internal_args->AppendString(internal_printer_id);
+  internal_args.push_back(base::Value(request_id));
+  internal_args.push_back(base::Value(internal_printer_id));
 
   std::unique_ptr<Event> event(
       new Event(events::PRINTER_PROVIDER_ON_GET_CAPABILITY_REQUESTED,
@@ -612,11 +612,11 @@
   int request_id = pending_print_requests_[extension_id].Add(
       std::move(job), std::move(callback));
 
-  std::unique_ptr<base::ListValue> internal_args(new base::ListValue);
+  std::vector<base::Value> internal_args;
   // Request id is not part of the public API and it will be massaged out in
   // custom bindings.
-  internal_args->AppendInteger(request_id);
-  internal_args->Append(print_job.ToValue());
+  internal_args.push_back(base::Value(request_id));
+  internal_args.push_back(base::Value::FromUniquePtrValue(print_job.ToValue()));
   std::unique_ptr<Event> event(
       new Event(events::PRINTER_PROVIDER_ON_PRINT_REQUESTED,
                 api::printer_provider::OnPrintRequested::kEventName,
@@ -650,11 +650,12 @@
   api::usb::Device api_device;
   UsbDeviceManager::Get(browser_context_)->GetApiDevice(device, &api_device);
 
-  std::unique_ptr<base::ListValue> internal_args(new base::ListValue());
+  std::vector<base::Value> internal_args;
   // Request id is not part of the public API and it will be massaged out in
   // custom bindings.
-  internal_args->AppendInteger(request_id);
-  internal_args->Append(api_device.ToValue());
+  internal_args.push_back(base::Value(request_id));
+  internal_args.push_back(
+      base::Value::FromUniquePtrValue(api_device.ToValue()));
   std::unique_ptr<Event> event(
       new Event(events::PRINTER_PROVIDER_ON_GET_USB_PRINTER_INFO_REQUESTED,
                 api::printer_provider::OnGetUsbPrinterInfoRequested::kEventName,
diff --git a/extensions/browser/api/runtime/runtime_api.cc b/extensions/browser/api/runtime/runtime_api.cc
index ba976db..cba8db7 100644
--- a/extensions/browser/api/runtime/runtime_api.cc
+++ b/extensions/browser/api/runtime/runtime_api.cc
@@ -137,10 +137,9 @@
     }
   }
 
-  std::unique_ptr<base::ListValue> event_args(new base::ListValue());
   std::unique_ptr<Event> event(new Event(events::RUNTIME_ON_STARTUP,
                                          runtime::OnStartup::kEventName,
-                                         std::move(event_args)));
+                                         std::vector<base::Value>()));
   EventRouter::Get(browser_context)
       ->DispatchEventToExtension(extension_id, std::move(event));
 }
@@ -450,17 +449,17 @@
     return;
   }
 
-  std::unique_ptr<base::ListValue> event_args(new base::ListValue());
-  std::unique_ptr<base::DictionaryValue> info(new base::DictionaryValue());
+  std::vector<base::Value> event_args;
+  base::Value info(base::Value::Type::DICTIONARY);
   if (old_version.IsValid()) {
-    info->SetString(kInstallReason, kInstallReasonUpdate);
-    info->SetString(kInstallPreviousVersion, old_version.GetString());
+    info.SetStringKey(kInstallReason, kInstallReasonUpdate);
+    info.SetStringKey(kInstallPreviousVersion, old_version.GetString());
   } else if (chrome_updated) {
-    info->SetString(kInstallReason, kInstallReasonChromeUpdate);
+    info.SetStringKey(kInstallReason, kInstallReasonChromeUpdate);
   } else {
-    info->SetString(kInstallReason, kInstallReasonInstall);
+    info.SetStringKey(kInstallReason, kInstallReasonInstall);
   }
-  event_args->Append(std::move(info));
+  event_args.push_back(std::move(info));
   EventRouter* event_router = EventRouter::Get(context);
   DCHECK(event_router);
   std::unique_ptr<Event> event(new Event(events::RUNTIME_ON_INSTALLED,
@@ -478,13 +477,12 @@
       for (ExtensionSet::const_iterator i = dependents->begin();
            i != dependents->end();
            i++) {
-        std::unique_ptr<base::ListValue> sm_event_args(new base::ListValue());
-        std::unique_ptr<base::DictionaryValue> sm_info(
-            new base::DictionaryValue());
-        sm_info->SetString(kInstallReason, kInstallReasonSharedModuleUpdate);
-        sm_info->SetString(kInstallPreviousVersion, old_version.GetString());
-        sm_info->SetString(kInstallId, extension_id);
-        sm_event_args->Append(std::move(sm_info));
+        std::vector<base::Value> sm_event_args;
+        base::Value sm_info(base::Value::Type::DICTIONARY);
+        sm_info.SetStringKey(kInstallReason, kInstallReasonSharedModuleUpdate);
+        sm_info.SetStringKey(kInstallPreviousVersion, old_version.GetString());
+        sm_info.SetStringKey(kInstallId, extension_id);
+        sm_event_args.push_back(std::move(sm_info));
         std::unique_ptr<Event> sm_event(new Event(
             events::RUNTIME_ON_INSTALLED, runtime::OnInstalled::kEventName,
             std::move(sm_event_args)));
@@ -504,8 +502,8 @@
   if (!system)
     return;
 
-  std::unique_ptr<base::ListValue> args(new base::ListValue);
-  args->Append(manifest->CreateDeepCopy());
+  std::vector<base::Value> args;
+  args.push_back(manifest->Clone());
   EventRouter* event_router = EventRouter::Get(context);
   DCHECK(event_router);
   std::unique_ptr<Event> event(new Event(events::RUNTIME_ON_UPDATE_AVAILABLE,
@@ -521,12 +519,12 @@
   if (!system)
     return;
 
-  std::unique_ptr<base::ListValue> args(new base::ListValue);
   EventRouter* event_router = EventRouter::Get(context);
   DCHECK(event_router);
-  std::unique_ptr<Event> event(new Event(
-      events::RUNTIME_ON_BROWSER_UPDATE_AVAILABLE,
-      runtime::OnBrowserUpdateAvailable::kEventName, std::move(args)));
+  std::unique_ptr<Event> event(
+      new Event(events::RUNTIME_ON_BROWSER_UPDATE_AVAILABLE,
+                runtime::OnBrowserUpdateAvailable::kEventName,
+                std::vector<base::Value>()));
   event_router->BroadcastEvent(std::move(event));
 }
 
diff --git a/extensions/browser/api/storage/storage_frontend.cc b/extensions/browser/api/storage/storage_frontend.cc
index 866fb1c..5f28f3d6 100644
--- a/extensions/browser/api/storage/storage_frontend.cc
+++ b/extensions/browser/api/storage/storage_frontend.cc
@@ -89,9 +89,9 @@
     // Event for each storage(sync, local, managed).
     if (event_router->ExtensionHasEventListener(
             extension_id, api::storage::OnChanged::kEventName)) {
-      std::unique_ptr<base::ListValue> args(new base::ListValue());
-      args->Append(changes.Clone());
-      args->AppendString(namespace_string);
+      std::vector<base::Value> args;
+      args.push_back(changes.Clone());
+      args.push_back(base::Value(namespace_string));
       std::unique_ptr<Event> event(
           new Event(events::STORAGE_ON_CHANGED,
                     api::storage::OnChanged::kEventName, std::move(args)));
@@ -103,8 +103,8 @@
         base::StringPrintf("storage.%s.onChanged", namespace_string.c_str());
     if (event_router->ExtensionHasEventListener(extension_id,
                                                 area_event_name)) {
-      auto args = std::make_unique<base::ListValue>();
-      args->Append(changes.Clone());
+      std::vector<base::Value> args;
+      args.push_back(changes.Clone());
       auto event =
           std::make_unique<Event>(StorageAreaToEventHistogram(storage_area),
                                   area_event_name, std::move(args));
diff --git a/extensions/browser/event_router.cc b/extensions/browser/event_router.cc
index d1bf4e3..f9b8f149 100644
--- a/extensions/browser/event_router.cc
+++ b/extensions/browser/event_router.cc
@@ -1038,11 +1038,6 @@
 
 Event::Event(events::HistogramValue histogram_value,
              const std::string& event_name,
-             std::unique_ptr<base::ListValue> event_args)
-    : Event(histogram_value, event_name, std::move(event_args), nullptr) {}
-
-Event::Event(events::HistogramValue histogram_value,
-             const std::string& event_name,
              std::vector<base::Value> event_args,
              content::BrowserContext* restrict_to_browser_context)
     : Event(histogram_value,
diff --git a/extensions/browser/event_router.h b/extensions/browser/event_router.h
index 5507c9f..4a89090 100644
--- a/extensions/browser/event_router.h
+++ b/extensions/browser/event_router.h
@@ -511,10 +511,6 @@
   Event(events::HistogramValue histogram_value,
         const std::string& event_name,
         std::vector<base::Value> event_args);
-  // TODO(crbug.com/1139221): Remove this deprecated ctor and use the one above.
-  Event(events::HistogramValue histogram_value,
-        const std::string& event_name,
-        std::unique_ptr<base::ListValue> event_args);
 
   Event(events::HistogramValue histogram_value,
         const std::string& event_name,
diff --git a/extensions/browser/extension_protocols.cc b/extensions/browser/extension_protocols.cc
index cbf71af..b25f70f 100644
--- a/extensions/browser/extension_protocols.cc
+++ b/extensions/browser/extension_protocols.cc
@@ -302,8 +302,7 @@
   // Dedicated Worker (with PlzDedicatedWorker) and Shared Worker main scripts
   // can be loaded with extension URLs in browser process.
   // Service Worker and the imported scripts can be loaded with extension URLs
-  // in browser process during update check when
-  // ServiceWorkerImportedScriptUpdateCheck is enabled.
+  // in browser process when PlzServiceWorker is enabled or during update check.
   if (child_id == content::ChildProcessHost::kInvalidUniqueID &&
       (blink::IsRequestDestinationFrame(destination) ||
        (base::FeatureList::IsEnabled(blink::features::kPlzDedicatedWorker) &&
diff --git a/extensions/browser/extension_protocols.h b/extensions/browser/extension_protocols.h
index eaaac71..a6410e6d 100644
--- a/extensions/browser/extension_protocols.h
+++ b/extensions/browser/extension_protocols.h
@@ -61,8 +61,8 @@
 
 // Creates a new network::mojom::URLLoaderFactory implementation suitable for
 // handling service worker main/imported script requests initiated by the
-// browser process to extension URLs during service worker update check when
-// ServiceWorkerImportedScriptUpdateCheck is enabled.
+// browser process to extension URLs when PlzServiceWorker is enabled or during
+// service worker update check.
 mojo::PendingRemote<network::mojom::URLLoaderFactory>
 CreateExtensionServiceWorkerScriptURLLoaderFactory(
     content::BrowserContext* browser_context);
diff --git a/extensions/common/api/_api_features.json b/extensions/common/api/_api_features.json
index 8422479b..688f60e 100644
--- a/extensions/common/api/_api_features.json
+++ b/extensions/common/api/_api_features.json
@@ -183,7 +183,8 @@
       "contexts": ["webui"],
       "matches": [
         "chrome://feedback/*",
-        "chrome://cast-feedback/*"
+        "chrome://cast-feedback/*",
+        "chrome://os-feedback/*"
       ]
     }
   ],
diff --git a/extensions/common/extension_features.cc b/extensions/common/extension_features.cc
index 6fe1f6b2..a9e1b277 100644
--- a/extensions/common/extension_features.cc
+++ b/extensions/common/extension_features.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "extensions/common/extension_features.h"
+#include "base/feature_list.h"
 
 namespace extensions_features {
 
@@ -16,6 +17,12 @@
 const base::Feature kDisableMalwareExtensionsRemotely{
     "DisableMalwareExtensionsRemotely", base::FEATURE_ENABLED_BY_DEFAULT};
 
+// Controls whether we disable extensions that are marked as policy violation
+// by the Omaha attribute.
+const base::Feature kDisablePolicyViolationExtensionsRemotely{
+    "DisablePolicyViolationExtensionsRemotely",
+    base::FEATURE_DISABLED_BY_DEFAULT};
+
 // Controls whether we show an install friction dialog when an Enhanced Safe
 // Browsing user tries to install an extension that is not included in the
 // Safe Browsing CRX allowlist. This feature also controls if we show a warning
diff --git a/extensions/common/extension_features.h b/extensions/common/extension_features.h
index b2f8640..baa0c07 100644
--- a/extensions/common/extension_features.h
+++ b/extensions/common/extension_features.h
@@ -10,6 +10,7 @@
 namespace extensions_features {
 
 extern const base::Feature kDisableMalwareExtensionsRemotely;
+extern const base::Feature kDisablePolicyViolationExtensionsRemotely;
 extern const base::Feature kSafeBrowsingCrxAllowlistShowWarnings;
 extern const base::Feature kSafeBrowsingCrxAllowlistAutoDisable;
 
diff --git a/extensions/shell/browser/shell_extensions_browser_client.cc b/extensions/shell/browser/shell_extensions_browser_client.cc
index 1dc55b11..f31fe653 100644
--- a/extensions/shell/browser/shell_extensions_browser_client.cc
+++ b/extensions/shell/browser/shell_extensions_browser_client.cc
@@ -245,7 +245,7 @@
   }
 
   std::unique_ptr<Event> event(
-      new Event(histogram_value, event_name, std::move(args)));
+      new Event(histogram_value, event_name, args->TakeList()));
   EventRouter::Get(browser_context_)->BroadcastEvent(std::move(event));
 }
 
diff --git a/fuchsia/engine/BUILD.gn b/fuchsia/engine/BUILD.gn
index aaf47ef..40b546d 100644
--- a/fuchsia/engine/BUILD.gn
+++ b/fuchsia/engine/BUILD.gn
@@ -374,8 +374,8 @@
 source_set("browsertest_core") {
   testonly = true
   sources = [
-    "test/frame_test_helper.cc",
-    "test/frame_test_helper.h",
+    "test/frame_for_test.cc",
+    "test/frame_for_test.h",
     "test/test_data.cc",
     "test/test_data.h",
     "test/web_engine_browser_test.cc",
@@ -446,6 +446,7 @@
     "//testing/gtest",
     "//third_party/fuchsia-sdk/sdk/fidl/fuchsia.accessibility.semantics",
     "//third_party/fuchsia-sdk/sdk/fidl/fuchsia.input.virtualkeyboard",
+    "//third_party/fuchsia-sdk/sdk/fidl/fuchsia.mediacodec",
     "//third_party/fuchsia-sdk/sdk/pkg/scenic_cpp",
     "//ui/gfx",
     "//ui/ozone",
diff --git a/fuchsia/engine/browser/autoplay_browsertest.cc b/fuchsia/engine/browser/autoplay_browsertest.cc
index fad72f7..0cacab9c 100644
--- a/fuchsia/engine/browser/autoplay_browsertest.cc
+++ b/fuchsia/engine/browser/autoplay_browsertest.cc
@@ -14,6 +14,12 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom.h"
 
+namespace {
+
+constexpr char kAutoplayVp8Url[] = "/play_vp8.html?autoplay=1&codecs=vp8";
+
+}  // namespace
+
 class AutoplayTest : public cr_fuchsia::WebEngineBrowserTest {
  public:
   AutoplayTest() {
@@ -53,7 +59,7 @@
 IN_PROC_BROWSER_TEST_F(
     AutoplayTest,
     UserActivationPolicy_UserActivatedViaSimulatedInteraction) {
-  const GURL kUrl(embedded_test_server()->GetURL("/play_vp8.html?autoplay=1"));
+  const GURL kUrl(embedded_test_server()->GetURL(kAutoplayVp8Url));
   constexpr const char kPageLoadedTitle[] = "initial title";
 
   fuchsia::web::FramePtr frame =
@@ -76,7 +82,7 @@
 
 IN_PROC_BROWSER_TEST_F(AutoplayTest,
                        UserActivationPolicy_UserActivatedNavigation) {
-  const GURL kUrl(embedded_test_server()->GetURL("/play_vp8.html?autoplay=1"));
+  const GURL kUrl(embedded_test_server()->GetURL(kAutoplayVp8Url));
 
   fuchsia::web::FramePtr frame =
       CreateFrame(fuchsia::web::AutoplayPolicy::REQUIRE_USER_ACTIVATION);
@@ -90,7 +96,7 @@
 }
 
 IN_PROC_BROWSER_TEST_F(AutoplayTest, UserActivationPolicy_NoUserActivation) {
-  const GURL kUrl(embedded_test_server()->GetURL("/play_vp8.html?autoplay=1"));
+  const GURL kUrl(embedded_test_server()->GetURL(kAutoplayVp8Url));
 
   fuchsia::web::FramePtr frame =
       CreateFrame(fuchsia::web::AutoplayPolicy::REQUIRE_USER_ACTIVATION);
@@ -105,7 +111,7 @@
 
 IN_PROC_BROWSER_TEST_F(AutoplayTest,
                        AllowAllPolicy_DefaultNotUserActivatedNavigation) {
-  const GURL kUrl(embedded_test_server()->GetURL("/play_vp8.html?autoplay=1"));
+  const GURL kUrl(embedded_test_server()->GetURL(kAutoplayVp8Url));
 
   fuchsia::web::FramePtr frame =
       CreateFrame(fuchsia::web::AutoplayPolicy::ALLOW);
diff --git a/fuchsia/engine/browser/frame_impl_browsertest.cc b/fuchsia/engine/browser/frame_impl_browsertest.cc
index 93cc405b..4c7bb15 100644
--- a/fuchsia/engine/browser/frame_impl_browsertest.cc
+++ b/fuchsia/engine/browser/frame_impl_browsertest.cc
@@ -80,7 +80,7 @@
 const int64_t kOnLoadScriptId = 0;
 const char kChildQueryParamName[] = "child_url";
 const char kPopupChildFile[] = "popup_child.html";
-const char kAutoplayFileAndQuery[] = "play_vp8.html?autoplay=1";
+const char kAutoplayFileAndQuery[] = "play_vp8.html?autoplay=1&codecs=vp8";
 const char kAutoPlayBlockedTitle[] = "blocked";
 const char kAutoPlaySuccessTitle[] = "playing";
 
diff --git a/fuchsia/engine/browser/media_browsertest.cc b/fuchsia/engine/browser/media_browsertest.cc
index bfd3c21..84c12e4 100644
--- a/fuchsia/engine/browser/media_browsertest.cc
+++ b/fuchsia/engine/browser/media_browsertest.cc
@@ -4,16 +4,30 @@
 
 #include "fuchsia/engine/test/web_engine_browser_test.h"
 
+#include <fuchsia/mediacodec/cpp/fidl_test_base.h>
+
 #include "base/files/file_path.h"
+#include "base/fuchsia/scoped_service_binding.h"
+#include "base/fuchsia/test_component_context_for_process.h"
 #include "content/public/test/browser_test.h"
 #include "fuchsia/base/test/frame_test_util.h"
 #include "fuchsia/base/test/test_navigation_listener.h"
 #include "fuchsia/engine/switches.h"
 #include "fuchsia/engine/test/test_data.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace {
 
+// Currently, VP8 can only be decoded in software, and VP9 can be decoded in
+// hardware and software. The tests rely on this.
+// TODO(crbug.com/1207695): Rename play_vp8.html to play_video.html.
+constexpr char kLoadSoftwareOnlyCodecUrl[] = "/play_vp8.html?codecs=vp8";
+constexpr char kLoadHardwareAndSoftwareCodecUrl[] = "/play_vp8.html?codecs=vp9";
+constexpr char kCanPlaySoftwareOnlyCodecUrl[] = "/can_play_vp8.html";
+
+}  // namespace
+
 class MediaTest : public cr_fuchsia::WebEngineBrowserTest {
  public:
   MediaTest() {
@@ -21,42 +35,86 @@
   }
   ~MediaTest() override = default;
 
+  MediaTest(const MediaTest&) = delete;
+  MediaTest& operator=(const MediaTest&) = delete;
+
+ protected:
   void SetUpOnMainThread() override {
     CHECK(embedded_test_server()->Start());
     cr_fuchsia::WebEngineBrowserTest::SetUpOnMainThread();
   }
 
- protected:
   // Creates a Frame with |navigation_listener_| attached.
   fuchsia::web::FramePtr CreateFrame() {
     return WebEngineBrowserTest::CreateFrame(&navigation_listener_);
   }
 
   cr_fuchsia::TestNavigationListener navigation_listener_;
-
-  DISALLOW_COPY_AND_ASSIGN(MediaTest);
 };
 
-// VP8 can presently only be decoded in software.
-// Verify that the --disable-software-video-decoders flag results in VP8
-// media being reported as unplayable.
+using SoftwareDecoderEnabledTest = MediaTest;
+
+// MediaTest with switches::kDisableSoftwareVideoDecoders.
 class SoftwareDecoderDisabledTest : public MediaTest {
  public:
   SoftwareDecoderDisabledTest() = default;
   ~SoftwareDecoderDisabledTest() override = default;
 
-  void SetUp() override {
-    base::CommandLine::ForCurrentProcess()->AppendSwitch(
-        switches::kDisableSoftwareVideoDecoders);
-    cr_fuchsia::WebEngineBrowserTest::SetUp();
+ protected:
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    command_line->AppendSwitch(switches::kDisableSoftwareVideoDecoders);
+    MediaTest::SetUpCommandLine(command_line);
   }
-
- private:
-  DISALLOW_COPY_AND_ASSIGN(SoftwareDecoderDisabledTest);
 };
 
-IN_PROC_BROWSER_TEST_F(SoftwareDecoderDisabledTest, VP8IsTypeSupported) {
-  const GURL kUrl(embedded_test_server()->GetURL("/can_play_vp8.html"));
+// SoftwareDecoderDisabledTest with fuchsia.mediacodec.CodecFactory
+// disconnected.
+class SoftwareDecoderDisabledAndHardwareDecoderFailureTest
+    : public SoftwareDecoderDisabledTest {
+ public:
+  SoftwareDecoderDisabledAndHardwareDecoderFailureTest() = default;
+  ~SoftwareDecoderDisabledAndHardwareDecoderFailureTest() override = default;
+
+ protected:
+  // Removes the decoder service to cause calls to it to fail.
+  void SetUpOnMainThread() override {
+    component_context_.emplace(
+        base::TestComponentContextForProcess::InitialState::kCloneAll);
+    component_context_->additional_services()
+        ->RemovePublicService<fuchsia::mediacodec::CodecFactory>();
+
+    SoftwareDecoderDisabledTest::SetUpOnMainThread();
+  }
+
+  // Used to disconnect fuchsia.mediacodec.CodecFactory.
+  absl::optional<base::TestComponentContextForProcess> component_context_;
+};
+
+// Verify that a codec only supported by a software decoder is reported as
+// playable if kDisableSoftwareVideoDecoders is not present.
+IN_PROC_BROWSER_TEST_F(SoftwareDecoderEnabledTest,
+                       CanPlayTypeSoftwareOnlyCodecIsTrue) {
+  const GURL kUrl(embedded_test_server()->GetURL(kCanPlaySoftwareOnlyCodecUrl));
+
+  // TODO(crbug.com/1200314): Refactor these tests to use FrameForTest and
+  // possibly to simplify the calls below since some of the details are not
+  // interesting to the individual tests. In particular, has_user_activation is
+  // more relevant than speclific LoadUrlParams.
+  fuchsia::web::FramePtr frame = CreateFrame();
+
+  fuchsia::web::NavigationControllerPtr controller;
+  frame->GetNavigationController(controller.NewRequest());
+
+  EXPECT_TRUE(cr_fuchsia::LoadUrlAndExpectResponse(
+      controller.get(), fuchsia::web::LoadUrlParams(), kUrl.spec()));
+  navigation_listener_.RunUntilUrlAndTitleEquals(kUrl, "can play vp8: true");
+}
+
+// Verify that a codec only supported by a software decoder is reported as not
+// playable if kDisableSoftwareVideoDecoders is present.
+IN_PROC_BROWSER_TEST_F(SoftwareDecoderDisabledTest,
+                       CanPlayTypeSoftwareOnlyCodecIsFalse) {
+  const GURL kUrl(embedded_test_server()->GetURL(kCanPlaySoftwareOnlyCodecUrl));
 
   fuchsia::web::FramePtr frame = CreateFrame();
 
@@ -68,27 +126,11 @@
   navigation_listener_.RunUntilUrlAndTitleEquals(kUrl, "can play vp8: false");
 }
 
-using SoftwareDecoderEnabledTest = MediaTest;
-
-// Verify that VP8 is reported as playable if --disable-software-video-decoders
-// is unset.
-IN_PROC_BROWSER_TEST_F(SoftwareDecoderEnabledTest, VP8IsTypeSupported) {
-  const GURL kUrl(embedded_test_server()->GetURL("/can_play_vp8.html"));
-
-  fuchsia::web::FramePtr frame = CreateFrame();
-
-  fuchsia::web::NavigationControllerPtr controller;
-  frame->GetNavigationController(controller.NewRequest());
-
-  EXPECT_TRUE(cr_fuchsia::LoadUrlAndExpectResponse(
-      controller.get(), fuchsia::web::LoadUrlParams(), kUrl.spec()));
-  navigation_listener_.RunUntilUrlAndTitleEquals(kUrl, "can play vp8: true");
-}
-
-// Verify that a VP8 video is loaded if --disable-software-video-decoders is
-// unset.
-IN_PROC_BROWSER_TEST_F(SoftwareDecoderEnabledTest, PlayVP8) {
-  const GURL kUrl(embedded_test_server()->GetURL("/play_vp8.html"));
+// Verify that a codec only supported by a software decoder is loaded if
+// kDisableSoftwareVideoDecoders is not present.
+IN_PROC_BROWSER_TEST_F(SoftwareDecoderEnabledTest,
+                       PlaySoftwareOnlyCodecSucceeds) {
+  const GURL kUrl(embedded_test_server()->GetURL(kLoadSoftwareOnlyCodecUrl));
 
   fuchsia::web::FramePtr frame = CreateFrame();
 
@@ -100,10 +142,11 @@
   navigation_listener_.RunUntilUrlAndTitleEquals(kUrl, "loaded");
 }
 
-// Verifies that VP8 videos won't play if --disable-software-video-decoders is
-// set.
-IN_PROC_BROWSER_TEST_F(SoftwareDecoderDisabledTest, PlayVP8Disabled) {
-  const GURL kUrl(embedded_test_server()->GetURL("/play_vp8.html"));
+// Verify that a codec only supported by a software decoder is not loaded if
+// kDisableSoftwareVideoDecoders is present.
+IN_PROC_BROWSER_TEST_F(SoftwareDecoderDisabledTest,
+                       LoadSoftwareOnlyCodecFails) {
+  const GURL kUrl(embedded_test_server()->GetURL(kLoadSoftwareOnlyCodecUrl));
 
   fuchsia::web::FramePtr frame = CreateFrame();
 
@@ -112,7 +155,55 @@
 
   EXPECT_TRUE(cr_fuchsia::LoadUrlAndExpectResponse(
       controller.get(), fuchsia::web::LoadUrlParams(), kUrl.spec()));
-  navigation_listener_.RunUntilUrlAndTitleEquals(kUrl, "error");
+  navigation_listener_.RunUntilUrlAndTitleEquals(kUrl, "media element error");
 }
 
-}  // namespace
+// Verify that a codec supported by hardware and software decoders plays if
+// kDisableSoftwareVideoDecoders is present.
+// Unlike the software-only codec, this codec loads and plays (when a hardware)
+// decoder is actually available, such as on real hardware.
+// TODO(crbug.com/1207695): Correct the final expected result to "playing".
+// Currently, this fails in emulators. Fixing the bug will change this.
+IN_PROC_BROWSER_TEST_F(SoftwareDecoderDisabledTest,
+                       PlayHardwareAndSoftwareCodecSucceeds) {
+  const GURL kUrl(
+      embedded_test_server()->GetURL(kLoadHardwareAndSoftwareCodecUrl));
+
+  fuchsia::web::FramePtr frame = CreateFrame();
+
+  fuchsia::web::NavigationControllerPtr controller;
+  frame->GetNavigationController(controller.NewRequest());
+
+  EXPECT_TRUE(cr_fuchsia::LoadUrlAndExpectResponse(
+      controller.get(), cr_fuchsia::CreateLoadUrlParamsWithUserActivation(),
+      kUrl.spec()));
+
+  navigation_listener_.RunUntilUrlAndTitleEquals(kUrl, "loaded");
+  cr_fuchsia::ExecuteJavaScript(frame.get(), "bear.play()");
+
+  navigation_listener_.RunUntilUrlAndTitleEquals(kUrl, "media element error");
+}
+
+// Verify that a codec supported by hardware and software does not play if
+// kDisableSoftwareVideoDecoders is present and the hardware decoder fails.
+// Unlike the software-only codec, this codec loads because it is supposed to be
+// supported but fails when the hardware decoder is unavailable.
+IN_PROC_BROWSER_TEST_F(SoftwareDecoderDisabledAndHardwareDecoderFailureTest,
+                       PlayHardwareAndSoftwareCodecFails) {
+  const GURL kUrl(
+      embedded_test_server()->GetURL(kLoadHardwareAndSoftwareCodecUrl));
+
+  fuchsia::web::FramePtr frame = CreateFrame();
+
+  fuchsia::web::NavigationControllerPtr controller;
+  frame->GetNavigationController(controller.NewRequest());
+
+  EXPECT_TRUE(cr_fuchsia::LoadUrlAndExpectResponse(
+      controller.get(), cr_fuchsia::CreateLoadUrlParamsWithUserActivation(),
+      kUrl.spec()));
+
+  navigation_listener_.RunUntilUrlAndTitleEquals(kUrl, "loaded");
+  cr_fuchsia::ExecuteJavaScript(frame.get(), "bear.play()");
+
+  navigation_listener_.RunUntilUrlAndTitleEquals(kUrl, "media element error");
+}
diff --git a/fuchsia/engine/browser/theme_manager_browsertest.cc b/fuchsia/engine/browser/theme_manager_browsertest.cc
index ba69168..2a29413 100644
--- a/fuchsia/engine/browser/theme_manager_browsertest.cc
+++ b/fuchsia/engine/browser/theme_manager_browsertest.cc
@@ -119,7 +119,9 @@
     if (on_watch_closure_)
       std::move(on_watch_closure_).Run();
   }
-  void NotImplemented_(const std::string&) final {}
+  void NotImplemented_(const std::string& name) final {
+    ADD_FAILURE() << "Unexpected call: " << name;
+  }
 
   absl::optional<base::TestComponentContextForProcess> component_context_;
   absl::optional<base::ScopedServiceBinding<fuchsia::settings::Display>>
diff --git a/fuchsia/engine/test/data/bear-vp9.webm b/fuchsia/engine/test/data/bear-vp9.webm
new file mode 100644
index 0000000..4f497ae
--- /dev/null
+++ b/fuchsia/engine/test/data/bear-vp9.webm
Binary files differ
diff --git a/fuchsia/engine/test/data/play_audio.html b/fuchsia/engine/test/data/play_audio.html
index 8bfdddb..d8da238 100644
--- a/fuchsia/engine/test/data/play_audio.html
+++ b/fuchsia/engine/test/data/play_audio.html
@@ -7,7 +7,7 @@
       audio.src = URL.createObjectURL(mediaSource);
 
       audio.onended = function() { document.title = 'ended'; }
-      audio.onerror = function() { document.title = 'error'; }
+      audio.onerror = function() { document.title = 'media element error'; }
 
       // Play two files with different sample rate to force mid-stream
       // re-initialization.
diff --git a/fuchsia/engine/test/data/play_video.html b/fuchsia/engine/test/data/play_video.html
deleted file mode 100644
index 35a347b..0000000
--- a/fuchsia/engine/test/data/play_video.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<html>
-  <head><title>initial title</title></head>
-  <body>
-    <script>
-      var bear = document.createElement('video');
-      var isMetadataLoaded = false;
-
-      var autoplay = (window.location.href.indexOf("autoplay") > 0);
-      if (autoplay) {
-        bear.autoplay = true;
-      }
-
-      bear.onerror = function() { document.title = 'error'; }
-      bear.onloadeddata = function() { document.title = 'loaded'; }
-      bear.onloadedmetadata = function() { isMetadataLoaded = true; }
-      bear.onpause = function () { isPlaying = false; }
-      bear.onplay = function() { document.title = 'playing'; }
-      bear.onstalled = function() { document.title = 'stalled'; }
-      bear.onended = function() { document.title = 'ended'; }
-      bear.src = 'bear-vp9-opus.webm';
-
-      document.body.appendChild(bear);
-    </script>
-  </body>
-</html>
diff --git a/fuchsia/engine/test/data/play_vp8.html b/fuchsia/engine/test/data/play_vp8.html
index 6f0d088..c01ea8c 100644
--- a/fuchsia/engine/test/data/play_vp8.html
+++ b/fuchsia/engine/test/data/play_vp8.html
@@ -2,20 +2,49 @@
   <head><title>document loading</title></head>
   <body>
     <script>
+      // Returns the video file name corresponding to |codecs|.
+      // |codecs| may be null, in which case null will be returned.
+      // Otherwise, |codecs| is a comma-delimited string without spaces.
+      function getVideoFileNameForCodecs(codecs) {
+        if (!codecs) {
+          document.title = 'The "codecs" parameter is required.';
+          return null;
+        }
+
+        if (codecs == "vp8") {
+          return 'bear-vp8a.webm';
+        } else if (codecs == "vp9,opus") {
+          return 'bear-vp9-opus.webm';
+        } else if (codecs == "vp9") {
+          return 'bear-vp9.webm';
+        } else {
+          document.title = 'Unrecognized value in "codecs" parameter';
+          return null;
+        }
+      }
       var bear = document.createElement('video');
       var isMetadataLoaded = false;
       var isPlaying = false;
 
-      var autoplay = (window.location.href.indexOf("autoplay") > 0);
-      if (autoplay) {
+      var params = new URLSearchParams(window.location.search);
+      var videoFile = getVideoFileNameForCodecs(params.get("codecs"));
+    
+      // Per https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video,
+      // "the video will autoplay if the attribute is there in the <video> tag
+      // at all." Therefore, only set the attribute if the parameter is true.
+      if (params.get("autoplay") == "1") {
         bear.autoplay = true;
       }
 
-      bear.onerror = function() { document.title = 'error'; }
+      // The title will only be changed from 'playing' to 'ended' if requested.
+      // This avoids potentially flaky tests if the video ends during the test.
+      var reportEnded = params.get("reportended") == "1";
+
+      bear.onerror = function() { document.title = 'media element error'; }
       bear.onloadeddata = function() {
         document.title = 'loaded';
 
-        if (autoplay) {
+        if (bear.autoplay) {
           // No events are generated if autoplay's been denied, so use a
           // 0.2s timeout to detect and report the denial.
           window.setTimeout(function() {
@@ -31,11 +60,20 @@
         document.title = 'playing';
       }
       bear.onstalled = function() { document.title = 'stalled'; }
-      bear.src = 'bear-vp8a.webm';
+      bear.onended = function() {
+        if (reportEnded) {
+          document.title = 'ended';
+        }
+      }
 
-      // Programatically set the title, to provide an externally-visible
-      // indication that the page's scripts have executed.
-      document.title = 'initial title';
+      if (videoFile) {
+        // Programatically set the title, to provide an externally-visible
+        // indication that the page's scripts have executed.
+        document.title = 'initial title';
+
+        bear.src = videoFile;
+      }
+      // Else, the title should already be set to the error.
     </script>
   </body>
 </html>
diff --git a/fuchsia/engine/test/frame_for_test.cc b/fuchsia/engine/test/frame_for_test.cc
new file mode 100644
index 0000000..191bc93
--- /dev/null
+++ b/fuchsia/engine/test/frame_for_test.cc
@@ -0,0 +1,59 @@
+// 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 "fuchsia/engine/test/frame_for_test.h"
+
+#include "fuchsia/base/test/test_navigation_listener.h"
+
+namespace cr_fuchsia {
+
+// static
+FrameForTest FrameForTest::Create(fuchsia::web::Context* context,
+                                  fuchsia::web::CreateFrameParams params) {
+  FrameForTest result;
+  context->CreateFrameWithParams(std::move(params), result.frame_.NewRequest());
+  result.CreateAndAttachNavigationListener();
+  return result;
+}
+
+// static
+FrameForTest FrameForTest::Create(fuchsia::web::FrameHost* frame_host,
+                                  fuchsia::web::CreateFrameParams params) {
+  FrameForTest result;
+  frame_host->CreateFrameWithParams(std::move(params),
+                                    result.frame_.NewRequest());
+  result.CreateAndAttachNavigationListener();
+  return result;
+}
+
+// static
+FrameForTest FrameForTest::Create(const fuchsia::web::ContextPtr& context,
+                                  fuchsia::web::CreateFrameParams params) {
+  return Create(context.get(), std::move(params));
+}
+
+FrameForTest::FrameForTest() = default;
+
+FrameForTest::FrameForTest(FrameForTest&&) = default;
+
+FrameForTest& FrameForTest::operator=(FrameForTest&&) = default;
+
+FrameForTest::~FrameForTest() = default;
+
+fuchsia::web::NavigationControllerPtr FrameForTest::GetNavigationController() {
+  fuchsia::web::NavigationControllerPtr controller;
+  frame_->GetNavigationController(controller.NewRequest());
+  return controller;
+}
+
+void FrameForTest::CreateAndAttachNavigationListener() {
+  navigation_listener_ = std::make_unique<TestNavigationListener>();
+  navigation_listener_binding_ =
+      std::make_unique<fidl::Binding<fuchsia::web::NavigationEventListener>>(
+          navigation_listener_.get());
+  frame_->SetNavigationEventListener(
+      navigation_listener_binding_->NewBinding());
+}
+
+}  // namespace cr_fuchsia
diff --git a/fuchsia/engine/test/frame_for_test.h b/fuchsia/engine/test/frame_for_test.h
new file mode 100644
index 0000000..90ade83
--- /dev/null
+++ b/fuchsia/engine/test/frame_for_test.h
@@ -0,0 +1,60 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FUCHSIA_ENGINE_TEST_FRAME_FOR_TEST_H_
+#define FUCHSIA_ENGINE_TEST_FRAME_FOR_TEST_H_
+
+#include <fuchsia/web/cpp/fidl.h>
+#include <lib/fidl/cpp/binding.h>
+#include <memory>
+
+namespace cr_fuchsia {
+
+class TestNavigationListener;
+
+// Helper for tests which need to create fuchsia.web.Frames.
+// Each instance owns a fuchsia.web.Frame, and attaches a TestNavigationListener
+// to it.
+class FrameForTest {
+ public:
+  // Returns a FrameForTest that encapsulates a new Frame, created using the
+  // specified container and |params|.
+  static FrameForTest Create(fuchsia::web::Context* context,
+                             fuchsia::web::CreateFrameParams params);
+  static FrameForTest Create(fuchsia::web::FrameHost* frame_host,
+                             fuchsia::web::CreateFrameParams params);
+  static FrameForTest Create(const fuchsia::web::ContextPtr& context,
+                             fuchsia::web::CreateFrameParams params);
+
+  FrameForTest();
+  FrameForTest(FrameForTest&&);
+  FrameForTest& operator=(FrameForTest&&);
+  ~FrameForTest();
+
+  // Returns a new NavigationController for each call, which ensures that any
+  // calls made to |frame()| will have been processed before navigation
+  // controller requests.
+  fuchsia::web::NavigationControllerPtr GetNavigationController();
+
+  // Returns the fuchsia.web.FramePtr owned by this instance.
+  fuchsia::web::FramePtr& ptr() { return frame_; }
+
+  // May be called only on non-default-initialized instances, i.e. those
+  // returned directly, or via move-assignment, from Create().
+  TestNavigationListener& navigation_listener() {
+    return *navigation_listener_;
+  }
+
+ private:
+  void CreateAndAttachNavigationListener();
+
+  fuchsia::web::FramePtr frame_;
+  std::unique_ptr<TestNavigationListener> navigation_listener_;
+  std::unique_ptr<fidl::Binding<fuchsia::web::NavigationEventListener>>
+      navigation_listener_binding_;
+};
+
+}  // namespace cr_fuchsia
+
+#endif  // FUCHSIA_ENGINE_TEST_FRAME_FOR_TEST_H_
diff --git a/fuchsia/engine/test/frame_test_helper.cc b/fuchsia/engine/test/frame_test_helper.cc
deleted file mode 100644
index 294f5a8..0000000
--- a/fuchsia/engine/test/frame_test_helper.cc
+++ /dev/null
@@ -1,34 +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 "fuchsia/engine/test/frame_test_helper.h"
-
-namespace cr_fuchsia {
-
-FrameTestHelper::FrameTestHelper(fuchsia::web::Context* context,
-                                 fuchsia::web::CreateFrameParams params)
-    : navigation_listener_binding_(&navigation_listener_) {
-  context->CreateFrameWithParams(std::move(params), frame_.NewRequest());
-  frame_->SetNavigationEventListener(navigation_listener_binding_.NewBinding());
-}
-
-FrameTestHelper::FrameTestHelper(fuchsia::web::FrameHost* frame_host,
-                                 fuchsia::web::CreateFrameParams params)
-    : navigation_listener_binding_(&navigation_listener_) {
-  frame_host->CreateFrameWithParams(std::move(params), frame_.NewRequest());
-  frame_->SetNavigationEventListener(navigation_listener_binding_.NewBinding());
-}
-
-FrameTestHelper::FrameTestHelper(const fuchsia::web::ContextPtr& context,
-                                 fuchsia::web::CreateFrameParams params)
-    : FrameTestHelper(context.get(), std::move(params)) {}
-
-fuchsia::web::NavigationControllerPtr
-FrameTestHelper::GetNavigationController() {
-  fuchsia::web::NavigationControllerPtr controller;
-  frame_->GetNavigationController(controller.NewRequest());
-  return controller;
-}
-
-}  // namespace cr_fuchsia
diff --git a/fuchsia/engine/test/frame_test_helper.h b/fuchsia/engine/test/frame_test_helper.h
deleted file mode 100644
index 5fba622..0000000
--- a/fuchsia/engine/test/frame_test_helper.h
+++ /dev/null
@@ -1,46 +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 FUCHSIA_ENGINE_TEST_FRAME_TEST_HELPER_H_
-#define FUCHSIA_ENGINE_TEST_FRAME_TEST_HELPER_H_
-
-#include <fuchsia/web/cpp/fidl.h>
-#include <lib/fidl/cpp/binding.h>
-
-#include "fuchsia/base/test/test_navigation_listener.h"
-
-namespace cr_fuchsia {
-
-// Helper for tests which need to create fuchsia.web.Frames.
-class FrameTestHelper {
- public:
-  FrameTestHelper(fuchsia::web::Context* context,
-                  fuchsia::web::CreateFrameParams params);
-  FrameTestHelper(fuchsia::web::FrameHost* frame_host,
-                  fuchsia::web::CreateFrameParams params);
-  FrameTestHelper(const fuchsia::web::ContextPtr& context,
-                  fuchsia::web::CreateFrameParams params);
-  ~FrameTestHelper();
-
-  FrameTestHelper(const FrameTestHelper&) = delete;
-  FrameTestHelper& operator=(const FrameTestHelper&) = delete;
-
-  // Returns a new NavigationController for each call, which ensures that any
-  // calls made to |frame()| will have been processed before navigation
-  // controller requests.
-  fuchsia::web::NavigationControllerPtr GetNavigationController();
-
-  fuchsia::web::FramePtr& frame() { return frame_; }
-  TestNavigationListener& navigation_listener() { return navigation_listener_; }
-
- private:
-  fuchsia::web::FramePtr frame_;
-  TestNavigationListener navigation_listener_;
-  fidl::Binding<fuchsia::web::NavigationEventListener>
-      navigation_listener_binding_;
-};
-
-}  // namespace cr_fuchsia
-
-#endif  // FUCHSIA_ENGINE_TEST_FRAME_TEST_HELPER_H_
diff --git a/fuchsia/engine/web_engine_integration_test.cc b/fuchsia/engine/web_engine_integration_test.cc
index 768d4118..942e445 100644
--- a/fuchsia/engine/web_engine_integration_test.cc
+++ b/fuchsia/engine/web_engine_integration_test.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <fuchsia/mediacodec/cpp/fidl.h>
 #include <fuchsia/mem/cpp/fidl.h>
 #include <zircon/rights.h>
 #include <zircon/types.h>
@@ -29,6 +30,15 @@
 constexpr char kInvalidUserAgentProduct[] = "Test/Product";
 constexpr char kInvalidUserAgentVersion[] = "dev/12345";
 
+// TODO(crbug.com/1207695): Rename play_vp8.html to play_video.html.
+constexpr char kAutoplayVp9OpusUrl[] =
+    "fuchsia-dir://testdata/play_vp8.html?codecs=vp9,opus&autoplay=1";
+constexpr char kAutoplayVp9OpusToEndUrl[] =
+    "fuchsia-dir://testdata/"
+    "play_vp8.html?codecs=vp9,opus&autoplay=1&reportended=1";
+constexpr char kLoadVp9OpusUrl[] =
+    "fuchsia-dir://testdata/play_vp8.html?codecs=vp9,opus";
+
 }  // namespace
 
 // Starts a WebEngine instance before running the test.
@@ -317,7 +327,7 @@
       "fuchsia-dir://testdata/play_audio.html",
       cr_fuchsia::CreateLoadUrlParamsWithUserActivation()));
 
-  navigation_listener()->RunUntilTitleEquals("error");
+  navigation_listener()->RunUntilTitleEquals("media element error");
   EXPECT_FALSE(is_requested);
 }
 
@@ -325,7 +335,7 @@
   CreateContextAndFrame(ContextParamsWithAudioAndTestData());
 
   ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
-      "fuchsia-dir://testdata/play_video.html?autoplay",
+      kAutoplayVp9OpusToEndUrl,
       cr_fuchsia::CreateLoadUrlParamsWithUserActivation()));
 
   navigation_listener()->RunUntilTitleEquals("ended");
@@ -380,7 +390,7 @@
   frame_->SetBlockMediaLoading(true);
 
   ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
-      "fuchsia-dir://testdata/play_video.html?autoplay",
+      kAutoplayVp9OpusUrl,
       cr_fuchsia::CreateLoadUrlParamsWithUserActivation()));
 
   // Check different indicators that media has not loaded and is not playing.
@@ -399,7 +409,7 @@
   frame_->SetBlockMediaLoading(true);
 
   ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
-      "fuchsia-dir://testdata/play_video.html?autoplay",
+      kAutoplayVp9OpusUrl,
       cr_fuchsia::CreateLoadUrlParamsWithUserActivation()));
 
   // Check that media loading has been blocked.
@@ -419,8 +429,7 @@
   CreateContextAndFrame(ContextParamsWithAudioAndTestData());
 
   ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
-      "fuchsia-dir://testdata/play_video.html",
-      cr_fuchsia::CreateLoadUrlParamsWithUserActivation()));
+      kLoadVp9OpusUrl, cr_fuchsia::CreateLoadUrlParamsWithUserActivation()));
 
   navigation_listener()->RunUntilTitleEquals("loaded");
   frame_->SetBlockMediaLoading(true);
@@ -532,7 +541,7 @@
   CreateContextAndFrame(std::move(create_params));
 
   ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
-      "fuchsia-dir://testdata/play_video.html?autoplay",
+      kAutoplayVp9OpusToEndUrl,
       cr_fuchsia::CreateLoadUrlParamsWithUserActivation()));
   navigation_listener()->RunUntilTitleEquals("ended");
 
@@ -555,7 +564,7 @@
   CreateContextAndFrame(std::move(create_params));
 
   ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
-      "fuchsia-dir://testdata/play_video.html?autoplay",
+      kAutoplayVp9OpusToEndUrl,
       cr_fuchsia::CreateLoadUrlParamsWithUserActivation()));
 
   navigation_listener()->RunUntilTitleEquals("ended");
diff --git a/fuchsia/engine/web_engine_integration_test_base.h b/fuchsia/engine/web_engine_integration_test_base.h
index af5c0ed..65ba780 100644
--- a/fuchsia/engine/web_engine_integration_test_base.h
+++ b/fuchsia/engine/web_engine_integration_test_base.h
@@ -5,7 +5,6 @@
 #ifndef FUCHSIA_ENGINE_WEB_ENGINE_INTEGRATION_TEST_BASE_H_
 #define FUCHSIA_ENGINE_WEB_ENGINE_INTEGRATION_TEST_BASE_H_
 
-#include <fuchsia/mediacodec/cpp/fidl.h>
 #include <fuchsia/sys/cpp/fidl.h>
 #include <fuchsia/web/cpp/fidl.h>
 #include <lib/fidl/cpp/binding.h>
diff --git a/fuchsia/runners/BUILD.gn b/fuchsia/runners/BUILD.gn
index f403160..1a78ec4 100644
--- a/fuchsia/runners/BUILD.gn
+++ b/fuchsia/runners/BUILD.gn
@@ -78,6 +78,7 @@
   ]
   deps = [
     "//base",
+    "//components/cast/common:constants",
     "//components/cast/message_port",
     "//components/cast/named_message_port_connector:named_message_port_connector",
     "//fuchsia/base",
diff --git a/fuchsia/runners/cast/DEPS b/fuchsia/runners/cast/DEPS
index 8129faaf..1a96cd7 100644
--- a/fuchsia/runners/cast/DEPS
+++ b/fuchsia/runners/cast/DEPS
@@ -1,5 +1,4 @@
 include_rules = [
-  "+components/cast/message_port",
-  "+components/cast/named_message_port_connector",
+  "+components/cast",
   "+content/public/test",
 ]
diff --git a/fuchsia/runners/cast/cast_runner.cc b/fuchsia/runners/cast/cast_runner.cc
index 522761e..420aa40b 100644
--- a/fuchsia/runners/cast/cast_runner.cc
+++ b/fuchsia/runners/cast/cast_runner.cc
@@ -23,6 +23,7 @@
 #include "base/strings/strcat.h"
 #include "base/time/time.h"
 #include "base/values.h"
+#include "components/cast/common/constants.h"
 #include "fuchsia/base/agent_manager.h"
 #include "fuchsia/base/config_reader.h"
 #include "fuchsia/runners/cast/cast_streaming.h"
@@ -562,9 +563,8 @@
   EnsureSoftwareVideoDecodersAreDisabled(params.mutable_features());
   params.set_remote_debugging_port(CastRunner::kRemoteDebuggingPort);
 
-  // TODO(crbug.com/1166790): Fetch UserAgent version strings from Agent.
   params.set_user_agent_product("CrKey");
-  params.set_user_agent_version("1.52.999999");
+  params.set_user_agent_version(chromecast::kFrozenCrKeyValue);
 
   zx_status_t status = main_services_->ConnectClient(
       params.mutable_service_directory()->NewRequest());
diff --git a/gpu/config/gpu_util.cc b/gpu/config/gpu_util.cc
index b986d93e..0f56d6a 100644
--- a/gpu/config/gpu_util.cc
+++ b/gpu/config/gpu_util.cc
@@ -44,6 +44,7 @@
 #include "gpu/vulkan/buildflags.h"
 #include "ui/gfx/extension_set.h"
 #include "ui/gl/buildflags.h"
+#include "ui/gl/gl_implementation.h"
 #include "ui/gl/gl_switches.h"
 
 #if defined(OS_ANDROID)
@@ -738,8 +739,10 @@
           kGpuFeatureStatusEnabled ||
       gpu_feature_info.status_values[GPU_FEATURE_TYPE_ACCELERATED_GL] !=
           kGpuFeatureStatusEnabled) {
-    command_line->AppendSwitchASCII(
-        switches::kUseGL, gl::kGLImplementationSwiftShaderForWebGLName);
+    // This setting makes WebGL run on legacy SwiftShader GL when true and
+    // SwANGLE when false.
+    bool legacy_software_gl = true;
+    gl::SetSoftwareWebGLCommandLineSwitches(command_line, legacy_software_gl);
     return true;
   }
   return false;
diff --git a/headless/app/headless_shell.cc b/headless/app/headless_shell.cc
index 3c59c382..f15a5bd7 100644
--- a/headless/app/headless_shell.cc
+++ b/headless/app/headless_shell.cc
@@ -785,6 +785,11 @@
         command_line.GetSwitchValueASCII(switches::kUseGL));
   }
 
+  if (command_line.HasSwitch(switches::kUseANGLE)) {
+    builder.SetANGLEImplementation(
+        command_line.GetSwitchValueASCII(switches::kUseANGLE));
+  }
+
   if (command_line.HasSwitch(switches::kUserDataDir)) {
     builder.SetUserDataDir(
         command_line.GetSwitchValuePath(switches::kUserDataDir));
diff --git a/headless/app/headless_shell_switches.cc b/headless/app/headless_shell_switches.cc
index 5229318d..3ace56d 100644
--- a/headless/app/headless_shell_switches.cc
+++ b/headless/app/headless_shell_switches.cc
@@ -105,6 +105,10 @@
 // rendering.
 const char kUseGL[] = "use-gl";
 
+// Sets the ANGLE implementation to use. Only relevant if "use-gl" is set to
+// "angle"
+const char kUseANGLE[] = "use-angle";
+
 // A string used to override the default user agent with a custom one.
 const char kUserAgent[] = "user-agent";
 
diff --git a/headless/app/headless_shell_switches.h b/headless/app/headless_shell_switches.h
index d10b842..00b3391 100644
--- a/headless/app/headless_shell_switches.h
+++ b/headless/app/headless_shell_switches.h
@@ -33,6 +33,7 @@
 HEADLESS_EXPORT extern const char kScreenshot[];
 HEADLESS_EXPORT extern const char kSSLKeyLogFile[];
 HEADLESS_EXPORT extern const char kTimeout[];
+HEADLESS_EXPORT extern const char kUseANGLE[];
 HEADLESS_EXPORT extern const char kUseGL[];
 HEADLESS_EXPORT extern const char kUserAgent[];
 HEADLESS_EXPORT extern const char kUserDataDir[];
diff --git a/headless/lib/headless_content_main_delegate.cc b/headless/lib/headless_content_main_delegate.cc
index 1a3ea19..c3ea2d3 100644
--- a/headless/lib/headless_content_main_delegate.cc
+++ b/headless/lib/headless_content_main_delegate.cc
@@ -215,6 +215,10 @@
     if (!options()->gl_implementation.empty()) {
       command_line->AppendSwitchASCII(::switches::kUseGL,
                                       options()->gl_implementation);
+      if (!options()->angle_implementation.empty()) {
+        command_line->AppendSwitchASCII(::switches::kUseANGLE,
+                                        options()->angle_implementation);
+      }
     } else {
       command_line->AppendSwitch(::switches::kDisableGpu);
     }
diff --git a/headless/public/headless_browser.cc b/headless/public/headless_browser.cc
index e90e0c5..94da51c 100644
--- a/headless/public/headless_browser.cc
+++ b/headless/public/headless_browser.cc
@@ -6,6 +6,7 @@
 
 #include <utility>
 
+#include "build/build_config.h"
 #include "content/public/common/user_agent.h"
 #include "headless/public/version.h"
 #include "ui/gl/gl_switches.h"
@@ -36,6 +37,7 @@
     : argc(argc),
       argv(argv),
       gl_implementation(gl::kGLImplementationSwiftShaderForWebGLName),
+      angle_implementation(gl::kANGLEImplementationNoneName),
       product_name_and_version(GetProductNameAndVersion()),
       user_agent(content::BuildUserAgentFromProduct(product_name_and_version)),
       window_size(kDefaultWindowSize),
@@ -119,6 +121,12 @@
   return *this;
 }
 
+Builder& Builder::SetANGLEImplementation(
+    const std::string& angle_implementation) {
+  options_.angle_implementation = angle_implementation;
+  return *this;
+}
+
 Builder& Builder::SetAppendCommandLineFlagsCallback(
     const Options::AppendCommandLineFlagsCallback& callback) {
   options_.append_command_line_flags_callback = callback;
diff --git a/headless/public/headless_browser.h b/headless/public/headless_browser.h
index 9bcc978..80441a32 100644
--- a/headless/public/headless_browser.h
+++ b/headless/public/headless_browser.h
@@ -150,6 +150,10 @@
   // string can be used to disable GL rendering (e.g., WebGL support).
   std::string gl_implementation;
 
+  // Choose the ANGLE implementation to use for rendering.
+  // Only relevant if the gl_implementation above is set to "angle".
+  std::string angle_implementation;
+
   // Default per-context options, can be specialized on per-context basis.
 
   std::string product_name_and_version;
@@ -236,6 +240,7 @@
   Builder& SetDisableSandbox(bool disable_sandbox);
   Builder& SetEnableResourceScheduler(bool enable_resource_scheduler);
   Builder& SetGLImplementation(const std::string& gl_implementation);
+  Builder& SetANGLEImplementation(const std::string& angle_implementation);
   Builder& SetAppendCommandLineFlagsCallback(
       const Options::AppendCommandLineFlagsCallback& callback);
 #if defined(OS_WIN)
diff --git a/infra/config/generated/commit-queue.cfg b/infra/config/generated/commit-queue.cfg
index 2853a37e..f9dc579 100644
--- a/infra/config/generated/commit-queue.cfg
+++ b/infra/config/generated/commit-queue.cfg
@@ -84,6 +84,12 @@
         owner_whitelist_group: "project-chromium-robot-committers"
       }
       builders {
+        name: "chrome/try/lacros-arm-generic-chrome"
+        includable_only: true
+        owner_whitelist_group: "googlers"
+        owner_whitelist_group: "project-chromium-robot-committers"
+      }
+      builders {
         name: "chrome/try/linux-chrome"
         includable_only: true
         owner_whitelist_group: "googlers"
diff --git a/infra/config/generated/luci-milo.cfg b/infra/config/generated/luci-milo.cfg
index 8b38102..95afc19 100644
--- a/infra/config/generated/luci-milo.cfg
+++ b/infra/config/generated/luci-milo.cfg
@@ -449,6 +449,11 @@
     short_name: "lcr"
   }
   builders {
+    name: "buildbucket/luci.chrome.ci/lacros-arm-generic-chrome"
+    category: "chrome"
+    short_name: "lcr"
+  }
+  builders {
     name: "buildbucket/luci.chrome.ci/linux-chromeos-chrome"
     category: "chrome"
     short_name: "cro"
diff --git a/infra/config/subprojects/chromium/ci.star b/infra/config/subprojects/chromium/ci.star
index 584704b..abfc9b50 100644
--- a/infra/config/subprojects/chromium/ci.star
+++ b/infra/config/subprojects/chromium/ci.star
@@ -435,6 +435,7 @@
     short_name = short_name,
 ) for name, short_name in (
     ("lacros-amd64-generic-chrome", "lcr"),
+    ("lacros-arm-generic-chrome", "lcr"),
     ("linux-chromeos-chrome", "cro"),
     ("linux-chrome", "lnx"),
     ("mac-chrome", "mac"),
diff --git a/infra/config/subprojects/chromium/try.star b/infra/config/subprojects/chromium/try.star
index 7f369f4..d0838d64 100644
--- a/infra/config/subprojects/chromium/try.star
+++ b/infra/config/subprojects/chromium/try.star
@@ -2155,6 +2155,10 @@
 )
 
 chrome_internal_verifier(
+    builder = "lacros-arm-generic-chrome",
+)
+
+chrome_internal_verifier(
     builder = "linux-chrome",
     branch_selector = branches.STANDARD_MILESTONE,
 )
diff --git a/ios/build/tools/setup-gn.py b/ios/build/tools/setup-gn.py
index 8c1697e0..4721c9e 100755
--- a/ios/build/tools/setup-gn.py
+++ b/ios/build/tools/setup-gn.py
@@ -1,11 +1,13 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # 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.
 
 import argparse
+import configparser
 import convert_gn_xcodeproj
 import errno
+import io
 import os
 import re
 import shutil
@@ -13,16 +15,6 @@
 import sys
 import tempfile
 
-try:
-  import configparser
-except ImportError:
-  import ConfigParser as configparser
-
-try:
-  import StringIO as io
-except ImportError:
-  import io
-
 
 SUPPORTED_TARGETS = ('iphoneos', 'iphonesimulator', 'maccatalyst')
 SUPPORTED_CONFIGS = ('Debug', 'Release', 'Profile', 'Official', 'Coverage')
@@ -41,7 +33,7 @@
     re.compile('^settings append target.source-map .* /google/src/.*$'),
 )
 
-class ConfigParserWithStringInterpolation(configparser.SafeConfigParser):
+class ConfigParserWithStringInterpolation(configparser.ConfigParser):
 
   '''A .ini file parser that supports strings and environment variables.'''
 
@@ -55,7 +47,7 @@
   def getstring(self, section, option, fallback=''):
     try:
       raw_value = self.get(section, option)
-    except configparser.NoOptionError, _:
+    except configparser.NoOptionError:
       return fallback
     return self._UnquoteString(self._ExpandEnvVar(raw_value))
 
diff --git a/ios/chrome/browser/context_menu/BUILD.gn b/ios/chrome/browser/context_menu/BUILD.gn
index df554ea..c5a2f2e67 100644
--- a/ios/chrome/browser/context_menu/BUILD.gn
+++ b/ios/chrome/browser/context_menu/BUILD.gn
@@ -15,6 +15,7 @@
     "//base/test:test_support",
     "//components/strings",
     "//ios/chrome/app/strings",
+    "//ios/chrome/browser/ui/fullscreen:feature_flags",
     "//ios/chrome/browser/ui/fullscreen/test:eg_test_support+eg2",
     "//ios/chrome/test/earl_grey:eg_test_support+eg2",
     "//ios/testing/earl_grey:eg_test_support+eg2",
diff --git a/ios/chrome/browser/context_menu/context_menu_egtest.mm b/ios/chrome/browser/context_menu/context_menu_egtest.mm
index f6b6620..3c919b7f 100644
--- a/ios/chrome/browser/context_menu/context_menu_egtest.mm
+++ b/ios/chrome/browser/context_menu/context_menu_egtest.mm
@@ -10,6 +10,7 @@
 #include "base/strings/sys_string_conversions.h"
 #import "base/test/ios/wait_util.h"
 #include "components/strings/grit/components_strings.h"
+#import "ios/chrome/browser/ui/fullscreen/fullscreen_features.h"
 #import "ios/chrome/browser/ui/fullscreen/test/fullscreen_app_interface.h"
 #include "ios/chrome/grit/ios_strings.h"
 #import "ios/chrome/test/earl_grey/chrome_actions.h"
@@ -180,6 +181,14 @@
 
 @implementation ContextMenuTestCase
 
+- (AppLaunchConfiguration)appConfigurationForTestCase {
+  AppLaunchConfiguration config;
+
+  config.features_disabled.push_back(
+      fullscreen::features::kSmoothScrollingDefault);
+  return config;
+}
+
 + (void)setUpForTestCase {
   [super setUpForTestCase];
   [ChromeEarlGrey setContentSettings:CONTENT_SETTING_ALLOW];
@@ -265,8 +274,7 @@
 
   // Calculate a point inside the displayed image.
   CGFloat topInset = 0.0;
-  if ([ChromeEarlGrey webStateWebViewUsesContentInset] ||
-      [FullscreenAppInterface isFullscreenInitialized]) {
+  if ([ChromeEarlGrey webStateWebViewUsesContentInset]) {
     topInset = [FullscreenAppInterface currentViewportInsets].top;
   }
   CGPoint pointOnImage = CGPointZero;
diff --git a/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_browser_agent.h b/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_browser_agent.h
index 480f4c58..f1b51e37 100644
--- a/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_browser_agent.h
+++ b/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_browser_agent.h
@@ -7,7 +7,7 @@
 
 #include <string>
 
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #include "ios/chrome/browser/main/browser_observer.h"
 #include "ios/chrome/browser/main/browser_user_data.h"
 #include "ios/chrome/browser/overlays/public/overlay_presenter.h"
@@ -136,7 +136,8 @@
   std::unique_ptr<BatchOperation> batch_operation_;
 
   // Observes overlays presentation.
-  ScopedObserver<OverlayPresenter, OverlayPresenterObserver> overlay_observer_;
+  base::ScopedObservation<OverlayPresenter, OverlayPresenterObserver>
+      overlay_observation_{this};
 };
 
 #endif  // IOS_CHROME_BROWSER_CRASH_REPORT_BREADCRUMBS_BREADCRUMB_MANAGER_BROWSER_AGENT_H_
diff --git a/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_browser_agent.mm b/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_browser_agent.mm
index 1e1b3b6..771a8d8 100644
--- a/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_browser_agent.mm
+++ b/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_browser_agent.mm
@@ -37,14 +37,14 @@
 BROWSER_USER_DATA_KEY_IMPL(BreadcrumbManagerBrowserAgent)
 
 BreadcrumbManagerBrowserAgent::BreadcrumbManagerBrowserAgent(Browser* browser)
-    : browser_(browser), overlay_observer_(this) {
+    : browser_(browser) {
   static int next_unique_id = 1;
   unique_id_ = next_unique_id++;
 
   browser_->AddObserver(this);
   browser_->GetWebStateList()->AddObserver(this);
 
-  overlay_observer_.Add(
+  overlay_observation_.Observe(
       OverlayPresenter::FromBrowser(browser, OverlayModality::kWebContentArea));
 }
 
@@ -208,5 +208,6 @@
 
 void BreadcrumbManagerBrowserAgent::OverlayPresenterDestroyed(
     OverlayPresenter* presenter) {
-  overlay_observer_.Remove(presenter);
+  DCHECK(overlay_observation_.IsObservingSource(presenter));
+  overlay_observation_.Reset();
 }
diff --git a/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_tab_helper.h b/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_tab_helper.h
index 506931d..4ae8587 100644
--- a/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_tab_helper.h
+++ b/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_tab_helper.h
@@ -7,7 +7,7 @@
 
 #include <string>
 
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #include "components/infobars/core/infobar_manager.h"
 #include "ios/web/public/web_state_observer.h"
 #import "ios/web/public/web_state_user_data.h"
@@ -154,8 +154,9 @@
   int sequentially_scrolled_ = 0;
 
   // Manages this object as an observer of infobars.
-  ScopedObserver<infobars::InfoBarManager, infobars::InfoBarManager::Observer>
-      infobar_observer_;
+  base::ScopedObservation<infobars::InfoBarManager,
+                          infobars::InfoBarManager::Observer>
+      infobar_observation_{this};
 
   // Allows observing Objective-C object for Scroll and Zoom events.
   __strong id<CRWWebViewScrollViewProxyObserver> scroll_observer_;
diff --git a/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_tab_helper.mm b/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_tab_helper.mm
index 22efc7c1..7d8bd36 100644
--- a/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_tab_helper.mm
+++ b/ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_tab_helper.mm
@@ -106,14 +106,13 @@
 
 BreadcrumbManagerTabHelper::BreadcrumbManagerTabHelper(web::WebState* web_state)
     : web_state_(web_state),
-      infobar_manager_(InfoBarManagerImpl::FromWebState(web_state)),
-      infobar_observer_(this) {
+      infobar_manager_(InfoBarManagerImpl::FromWebState(web_state)) {
   web_state_->AddObserver(this);
 
   static int next_unique_id = 1;
   unique_id_ = next_unique_id++;
 
-  infobar_observer_.Add(infobar_manager_);
+  infobar_observation_.Observe(infobar_manager_);
 
   scroll_observer_ = [[BreadcrumbScrollingObserver alloc]
       initWithLoggingBlock:^(const std::string& event) {
@@ -307,7 +306,8 @@
 void BreadcrumbManagerTabHelper::OnManagerShuttingDown(
     infobars::InfoBarManager* manager) {
   DCHECK_EQ(infobar_manager_, manager);
-  infobar_observer_.Remove(manager);
+  DCHECK(infobar_observation_.IsObservingSource(manager));
+  infobar_observation_.Reset();
   infobar_manager_ = nullptr;
   sequentially_replaced_infobars_ = 0;
 }
diff --git a/ios/chrome/browser/infobars/infobar_badge_tab_helper.h b/ios/chrome/browser/infobars/infobar_badge_tab_helper.h
index f91aeba..e287273d 100644
--- a/ios/chrome/browser/infobars/infobar_badge_tab_helper.h
+++ b/ios/chrome/browser/infobars/infobar_badge_tab_helper.h
@@ -7,7 +7,8 @@
 
 #include <map>
 
-#include "base/scoped_observer.h"
+#include "base/scoped_multi_source_observation.h"
+#include "base/scoped_observation.h"
 #include "components/infobars/core/infobar_manager.h"
 #include "ios/chrome/browser/infobars/infobar_ios.h"
 #import "ios/chrome/browser/infobars/infobar_type.h"
@@ -64,9 +65,10 @@
     explicit InfobarAcceptanceObserver(InfobarBadgeTabHelper* tab_helper);
     ~InfobarAcceptanceObserver() override;
 
-    // Returns a reference to the scoped observer.
-    ScopedObserver<InfoBarIOS, InfoBarIOS::Observer>& scoped_observer() {
-      return scoped_observer_;
+    // Returns a reference to the scoped observations.
+    base::ScopedMultiSourceObservation<InfoBarIOS, InfoBarIOS::Observer>&
+    scoped_observations() {
+      return scoped_observations_;
     }
 
    private:
@@ -77,7 +79,8 @@
     // The owning tab helper.
     InfobarBadgeTabHelper* tab_helper_ = nullptr;
     // Scoped observer that facilitates observing InfoBarIOS objects.
-    ScopedObserver<InfoBarIOS, InfoBarIOS::Observer> scoped_observer_;
+    base::ScopedMultiSourceObservation<InfoBarIOS, InfoBarIOS::Observer>
+        scoped_observations_{this};
   };
 
   // Helper object that updates state and adds an InfobarAcceptanceObserver
@@ -103,8 +106,9 @@
     // in the observed manager.
     InfobarAcceptanceObserver* infobar_accept_observer_ = nullptr;
     // Scoped observer that facilitates observing an InfoBarManager.
-    ScopedObserver<infobars::InfoBarManager, infobars::InfoBarManager::Observer>
-        scoped_observer_;
+    base::ScopedObservation<infobars::InfoBarManager,
+                            infobars::InfoBarManager::Observer>
+        scoped_observation_{this};
   };
 
   // Delegate which displays the Infobar badge.
diff --git a/ios/chrome/browser/infobars/infobar_badge_tab_helper.mm b/ios/chrome/browser/infobars/infobar_badge_tab_helper.mm
index 97df4f00..02023b5 100644
--- a/ios/chrome/browser/infobars/infobar_badge_tab_helper.mm
+++ b/ios/chrome/browser/infobars/infobar_badge_tab_helper.mm
@@ -112,7 +112,7 @@
 
 InfobarBadgeTabHelper::InfobarAcceptanceObserver::InfobarAcceptanceObserver(
     InfobarBadgeTabHelper* tab_helper)
-    : tab_helper_(tab_helper), scoped_observer_(this) {
+    : tab_helper_(tab_helper) {
   DCHECK(tab_helper_);
 }
 
@@ -127,7 +127,7 @@
 
 void InfobarBadgeTabHelper::InfobarAcceptanceObserver::InfobarDestroyed(
     InfoBarIOS* infobar) {
-  scoped_observer_.Remove(infobar);
+  scoped_observations_.RemoveObservation(infobar);
 }
 
 #pragma mark - InfobarBadgeTabHelper::InfobarManagerObserver
@@ -137,11 +137,10 @@
     web::WebState* web_state,
     InfobarAcceptanceObserver* infobar_accept_observer)
     : tab_helper_(tab_helper),
-      infobar_accept_observer_(infobar_accept_observer),
-      scoped_observer_(this) {
+      infobar_accept_observer_(infobar_accept_observer) {
   DCHECK(tab_helper_);
   DCHECK(infobar_accept_observer_);
-  scoped_observer_.Add(InfoBarManagerImpl::FromWebState(web_state));
+  scoped_observation_.Observe(InfoBarManagerImpl::FromWebState(web_state));
 }
 
 InfobarBadgeTabHelper::InfobarManagerObserver::~InfobarManagerObserver() =
@@ -151,7 +150,7 @@
     infobars::InfoBar* infobar) {
   if (SupportsBadges(infobar)) {
     tab_helper_->ResetStateForAddedInfobar(GetInfobarType(infobar));
-    infobar_accept_observer_->scoped_observer().Add(
+    infobar_accept_observer_->scoped_observations().AddObservation(
         static_cast<InfoBarIOS*>(infobar));
   }
 }
@@ -161,7 +160,7 @@
     bool animate) {
   if (SupportsBadges(infobar)) {
     tab_helper_->ResetStateForRemovedInfobar(GetInfobarType(infobar));
-    infobar_accept_observer_->scoped_observer().Remove(
+    infobar_accept_observer_->scoped_observations().RemoveObservation(
         static_cast<InfoBarIOS*>(infobar));
   }
 }
@@ -175,5 +174,6 @@
 
 void InfobarBadgeTabHelper::InfobarManagerObserver::OnManagerShuttingDown(
     infobars::InfoBarManager* manager) {
-  scoped_observer_.Remove(manager);
+  DCHECK(scoped_observation_.IsObservingSource(manager));
+  scoped_observation_.Reset();
 }
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/infobar_overlay_browser_agent.h b/ios/chrome/browser/infobars/overlays/browser_agent/infobar_overlay_browser_agent.h
index 031bead..aea240f 100644
--- a/ios/chrome/browser/infobars/overlays/browser_agent/infobar_overlay_browser_agent.h
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/infobar_overlay_browser_agent.h
@@ -8,7 +8,7 @@
 #include <map>
 #include <memory>
 
-#include "base/scoped_observer.h"
+#include "base/scoped_multi_source_observation.h"
 #import "ios/chrome/browser/infobars/infobar_type.h"
 #import "ios/chrome/browser/main/browser_user_data.h"
 #import "ios/chrome/browser/overlays/public/overlay_browser_agent_base.h"
@@ -65,7 +65,9 @@
     void OverlayPresenterDestroyed(OverlayPresenter* presenter) override;
 
     InfobarOverlayBrowserAgent* browser_agent_ = nullptr;
-    ScopedObserver<OverlayPresenter, OverlayPresenterObserver> scoped_observer_;
+    base::ScopedMultiSourceObservation<OverlayPresenter,
+                                       OverlayPresenterObserver>
+        scoped_observations_{this};
   };
 
   // The interaction handlers for each InfobarType.
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/infobar_overlay_browser_agent.mm b/ios/chrome/browser/infobars/overlays/browser_agent/infobar_overlay_browser_agent.mm
index 1aa6038d..f3922f4 100644
--- a/ios/chrome/browser/infobars/overlays/browser_agent/infobar_overlay_browser_agent.mm
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/infobar_overlay_browser_agent.mm
@@ -58,11 +58,11 @@
 InfobarOverlayBrowserAgent::OverlayVisibilityObserver::
     OverlayVisibilityObserver(Browser* browser,
                               InfobarOverlayBrowserAgent* browser_agent)
-    : browser_agent_(browser_agent), scoped_observer_(this) {
+    : browser_agent_(browser_agent) {
   DCHECK(browser_agent_);
-  scoped_observer_.Add(
+  scoped_observations_.AddObservation(
       OverlayPresenter::FromBrowser(browser, OverlayModality::kInfobarBanner));
-  scoped_observer_.Add(
+  scoped_observations_.AddObservation(
       OverlayPresenter::FromBrowser(browser, OverlayModality::kInfobarModal));
 }
 
@@ -98,5 +98,5 @@
 
 void InfobarOverlayBrowserAgent::OverlayVisibilityObserver::
     OverlayPresenterDestroyed(OverlayPresenter* presenter) {
-  scoped_observer_.Remove(presenter);
+  scoped_observations_.RemoveObservation(presenter);
 }
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/BUILD.gn b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/BUILD.gn
index 8528d5d..97721d3 100644
--- a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/BUILD.gn
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/BUILD.gn
@@ -27,6 +27,7 @@
     "//ios/chrome/browser/overlays",
     "//ios/chrome/browser/overlays/public/infobar_banner",
     "//ios/chrome/browser/overlays/public/infobar_modal",
+    "//ios/chrome/browser/ui/autofill:autofill_ui_type",
     "//ios/chrome/browser/web_state_list",
   ]
 }
@@ -37,6 +38,7 @@
   sources = [
     "save_address_profile_infobar_banner_interaction_handler_unittest.mm",
     "save_address_profile_infobar_modal_interaction_handler_unittest.mm",
+    "save_address_profile_infobar_modal_overlay_request_callback_installer_unittest.mm",
   ]
   deps = [
     ":autofill_address_profile",
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler.h b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler.h
index 3eab1bf..ef7b88c5 100644
--- a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler.h
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler.h
@@ -25,10 +25,9 @@
   void PerformMainAction(InfoBarIOS* infobar) override;
   void InfobarVisibilityChanged(InfoBarIOS* infobar, bool visible) override;
 
-  // Instructs the handler that the user has requested the address profile
-  // settings page through |infobar|'s modal UI.  The settings will be presented
-  // after the dismissal of |infobar|'s modal UI.
-  void PresentAddressProfileSettings(InfoBarIOS* infobar);
+  // Instructs the handler that the user has edited and then saved the profile.
+  virtual void SaveEditedProfile(InfoBarIOS* infobar,
+                                 NSDictionary* profileData);
 
  private:
   // InfobarModalInteractionHandler:
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler.mm b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler.mm
index f0e6b4ff..13d7294 100644
--- a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler.mm
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler.mm
@@ -4,9 +4,12 @@
 
 #import "ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler.h"
 
+#include "base/strings/sys_string_conversions.h"
 #include "components/autofill/core/browser/autofill_save_update_address_profile_delegate_ios.h"
+#include "components/autofill/core/browser/field_types.h"
 #include "ios/chrome/browser/infobars/infobar_ios.h"
 #import "ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer.h"
+#import "ios/chrome/browser/ui/autofill/autofill_ui_type_util.h"
 
 #if !defined(__has_feature) || !__has_feature(objc_arc)
 #error "This file requires ARC support."
@@ -23,6 +26,9 @@
 void SaveAddressProfileInfobarModalInteractionHandler::PerformMainAction(
     InfoBarIOS* infobar) {
   infobar->set_accepted(GetInfoBarDelegate(infobar)->Accept());
+  // Post save, the infobar should not be brought back from the omnibox
+  // icon.
+  infobar->RemoveSelf();
 }
 
 void SaveAddressProfileInfobarModalInteractionHandler::InfobarVisibilityChanged(
@@ -35,9 +41,19 @@
   }
 }
 
-void SaveAddressProfileInfobarModalInteractionHandler::
-    PresentAddressProfileSettings(InfoBarIOS* infobar) {
-  // TODO(crbug.com/1167062): Open Address Profile settings.
+void SaveAddressProfileInfobarModalInteractionHandler::SaveEditedProfile(
+    InfoBarIOS* infobar,
+    NSDictionary* profileData) {
+  for (NSNumber* key in profileData) {
+    autofill::ServerFieldType type =
+        AutofillTypeFromAutofillUIType((AutofillUIType)[key intValue]);
+    std::u16string data = base::SysNSStringToUTF16(profileData[key]);
+    GetInfoBarDelegate(infobar)->SetProfileRawInfo(type, data);
+  }
+  infobar->set_accepted(GetInfoBarDelegate(infobar)->EditAccepted());
+  // On post-edit save, the infobar should not be brought back from the omnibox
+  // icon.
+  infobar->RemoveSelf();
 }
 
 #pragma mark - Private
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler_unittest.mm b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler_unittest.mm
index d2c83db..b8e11aa 100644
--- a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler_unittest.mm
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler_unittest.mm
@@ -50,3 +50,9 @@
   EXPECT_CALL(mock_delegate(), Accept()).WillOnce(testing::Return(true));
   handler_.PerformMainAction(infobar_.get());
 }
+
+TEST_F(SaveAddressProfileInfobarModalInteractionHandlerTest,
+       SaveEditedProfile) {
+  EXPECT_CALL(mock_delegate(), EditAccepted()).WillOnce(testing::Return(true));
+  handler_.SaveEditedProfile(infobar_.get(), @{}.mutableCopy);
+}
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer.h b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer.h
index 05493cde..34abf92 100644
--- a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer.h
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer.h
@@ -25,9 +25,9 @@
   // Used as a callback for OverlayResponses dispatched through |request|'s
   // callback manager.  The OverlayDispatchCallback is created with an
   // OverlayResponseSupport that guarantees that |response| is created with a
-  // save_address_profile_infobar_modal_responses::PresentAddressProfileSettings.
-  void PresentAddressProfileSettingsCallback(OverlayRequest* request,
-                                             OverlayResponse* response);
+  // save_address_profile_infobar_modal_responses::EditedProfileSaveAction.
+  void SaveEditedProfileDetailsCallback(OverlayRequest* request,
+                                        OverlayResponse* response);
 
   // OverlayRequestCallbackInstaller:
   void InstallCallbacksInternal(OverlayRequest* request) override;
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer.mm b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer.mm
index 7a48344..b381969 100644
--- a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer.mm
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer.mm
@@ -21,8 +21,7 @@
 
 using autofill_address_profile_infobar_overlays::
     SaveAddressProfileModalRequestConfig;
-using save_address_profile_infobar_modal_responses::
-    PresentAddressProfileSettings;
+using save_address_profile_infobar_modal_responses::EditedProfileSaveAction;
 
 SaveAddressProfileInfobarModalOverlayRequestCallbackInstaller::
     SaveAddressProfileInfobarModalOverlayRequestCallbackInstaller(
@@ -37,15 +36,19 @@
 SaveAddressProfileInfobarModalOverlayRequestCallbackInstaller::
     ~SaveAddressProfileInfobarModalOverlayRequestCallbackInstaller() = default;
 
+#pragma mark - Private
+
 void SaveAddressProfileInfobarModalOverlayRequestCallbackInstaller::
-    PresentAddressProfileSettingsCallback(OverlayRequest* request,
-                                          OverlayResponse* response) {
+    SaveEditedProfileDetailsCallback(OverlayRequest* request,
+                                     OverlayResponse* response) {
   InfoBarIOS* infobar = GetOverlayRequestInfobar(request);
   if (!infobar) {
     return;
   }
 
-  interaction_handler_->PresentAddressProfileSettings(infobar);
+  EditedProfileSaveAction* info = response->GetInfo<EditedProfileSaveAction>();
+  interaction_handler_->SaveEditedProfile(GetOverlayRequestInfobar(request),
+                                          info->profile_data());
 }
 
 #pragma mark - OverlayRequestCallbackInstaller
@@ -59,7 +62,7 @@
   manager->AddDispatchCallback(OverlayDispatchCallback(
       base::BindRepeating(
           &SaveAddressProfileInfobarModalOverlayRequestCallbackInstaller::
-              PresentAddressProfileSettingsCallback,
+              SaveEditedProfileDetailsCallback,
           weak_factory_.GetWeakPtr(), request),
-      PresentAddressProfileSettings::ResponseSupport()));
+      EditedProfileSaveAction::ResponseSupport()));
 }
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer_unittest.mm b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer_unittest.mm
new file mode 100644
index 0000000..9cc2364
--- /dev/null
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer_unittest.mm
@@ -0,0 +1,94 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_overlay_request_callback_installer.h"
+
+#include "base/guid.h"
+#include "base/strings/sys_string_conversions.h"
+#include "components/autofill/core/browser/autofill_test_utils.h"
+#include "components/autofill/core/browser/data_model/autofill_profile.h"
+#include "ios/chrome/browser/infobars/infobar_ios.h"
+#include "ios/chrome/browser/infobars/infobar_manager_impl.h"
+#include "ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_autofill_save_update_address_profile_delegate_ios.h"
+#import "ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_save_address_profile_modal_infobar_interaction_handler.h"
+#import "ios/chrome/browser/overlays/public/infobar_modal/infobar_modal_overlay_responses.h"
+#import "ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_request_config.h"
+#import "ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_responses.h"
+#include "ios/chrome/browser/overlays/public/overlay_callback_manager.h"
+#include "ios/chrome/browser/overlays/public/overlay_request.h"
+#include "ios/chrome/browser/overlays/public/overlay_request_queue.h"
+#include "ios/chrome/browser/overlays/public/overlay_response.h"
+#import "ios/web/public/test/fakes/fake_navigation_manager.h"
+#import "ios/web/public/test/fakes/fake_web_state.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/platform_test.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+using autofill_address_profile_infobar_overlays::
+    SaveAddressProfileModalRequestConfig;
+using save_address_profile_infobar_modal_responses::EditedProfileSaveAction;
+
+// Test fixture for
+// SaveAddressProfileInfobarModalOverlayRequestCallbackInstaller.
+class SaveAddressProfileInfobarModalOverlayRequestCallbackInstallerTest
+    : public PlatformTest {
+ public:
+  SaveAddressProfileInfobarModalOverlayRequestCallbackInstallerTest()
+      : profile_(base::GenerateGUID(), "https://www.example.com/"),
+        installer_(&mock_handler_),
+        delegate_factory_() {
+    // Create the infobar and add it to the WebState's manager.
+    web_state_.SetNavigationManager(
+        std::make_unique<web::FakeNavigationManager>());
+    InfoBarManagerImpl::CreateForWebState(&web_state_);
+    std::unique_ptr<MockAutofillSaveUpdateAddressProfileDelegateIOS> delegate =
+        delegate_factory_
+            .CreateMockAutofillSaveUpdateAddressProfileDelegateIOSFactory(
+                profile_);
+    delegate_ = delegate.get();
+    std::unique_ptr<InfoBarIOS> infobar = std::make_unique<InfoBarIOS>(
+        InfobarType::kInfobarTypeSaveAutofillAddressProfile,
+        std::move(delegate));
+
+    infobar_ = infobar.get();
+    manager()->AddInfoBar(std::move(infobar));
+    // Create the request and add it to the WebState's queue.
+    std::unique_ptr<OverlayRequest> added_request =
+        OverlayRequest::CreateWithConfig<SaveAddressProfileModalRequestConfig>(
+            infobar_);
+    request_ = added_request.get();
+    queue()->AddRequest(std::move(added_request));
+    // Install the callbacks on the added request.
+    installer_.InstallCallbacks(request_);
+  }
+
+  InfoBarManagerImpl* manager() {
+    return InfoBarManagerImpl::FromWebState(&web_state_);
+  }
+  OverlayRequestQueue* queue() {
+    return OverlayRequestQueue::FromWebState(&web_state_,
+                                             OverlayModality::kInfobarModal);
+  }
+
+ protected:
+  autofill::AutofillProfile profile_;
+  web::FakeWebState web_state_;
+  InfoBarIOS* infobar_ = nullptr;
+  OverlayRequest* request_ = nullptr;
+  MockSaveAddressProfileInfobarModalInteractionHandler mock_handler_;
+  SaveAddressProfileInfobarModalOverlayRequestCallbackInstaller installer_;
+  MockAutofillSaveUpdateAddressProfileDelegateIOSFactory delegate_factory_;
+  MockAutofillSaveUpdateAddressProfileDelegateIOS* delegate_;
+};
+
+TEST_F(SaveAddressProfileInfobarModalOverlayRequestCallbackInstallerTest,
+       SaveEditedProfile) {
+  NSDictionary* empty = @{}.mutableCopy;
+  EXPECT_CALL(mock_handler_, SaveEditedProfile(infobar_, empty));
+  request_->GetCallbackManager()->DispatchResponse(
+      OverlayResponse::CreateWithInfo<EditedProfileSaveAction>(empty));
+}
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/BUILD.gn b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/BUILD.gn
index c7ba1abb0..4b3d3dd 100644
--- a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/BUILD.gn
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/BUILD.gn
@@ -11,6 +11,8 @@
     "mock_autofill_save_update_address_profile_delegate_ios.mm",
     "mock_infobar_interaction_handler.h",
     "mock_infobar_interaction_handler.mm",
+    "mock_save_address_profile_modal_infobar_interaction_handler.h",
+    "mock_save_address_profile_modal_infobar_interaction_handler.mm",
     "mock_save_card_banner_infobar_interaction_handler.h",
     "mock_save_card_banner_infobar_interaction_handler.mm",
     "mock_save_card_modal_infobar_interaction_handler.h",
@@ -29,8 +31,10 @@
     "//ios/chrome/browser/infobars:public",
     "//ios/chrome/browser/infobars/overlays",
     "//ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers",
+    "//ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile",
     "//ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/save_card",
     "//ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/translate",
+    "//ios/chrome/browser/main:test_support",
     "//ios/chrome/browser/overlays",
     "//ios/chrome/browser/overlays/public/common/infobars",
     "//ios/chrome/browser/overlays/public/infobar_banner",
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_autofill_save_update_address_profile_delegate_ios.h b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_autofill_save_update_address_profile_delegate_ios.h
index aa8663a..b7a2fd6 100644
--- a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_autofill_save_update_address_profile_delegate_ios.h
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_autofill_save_update_address_profile_delegate_ios.h
@@ -26,6 +26,7 @@
 
   MOCK_METHOD0(Accept, bool());
   MOCK_METHOD0(InfoBarDismissed, void());
+  MOCK_METHOD0(EditAccepted, bool());
 };
 
 class MockAutofillSaveUpdateAddressProfileDelegateIOSFactory {
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_save_address_profile_modal_infobar_interaction_handler.h b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_save_address_profile_modal_infobar_interaction_handler.h
new file mode 100644
index 0000000..696c1a9
--- /dev/null
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_save_address_profile_modal_infobar_interaction_handler.h
@@ -0,0 +1,26 @@
+// 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 IOS_CHROME_BROWSER_INFOBARS_OVERLAYS_BROWSER_AGENT_INTERACTION_HANDLERS_TEST_MOCK_SAVE_ADDRESS_PROFILE_MODAL_INFOBAR_INTERACTION_HANDLER_H_
+#define IOS_CHROME_BROWSER_INFOBARS_OVERLAYS_BROWSER_AGENT_INTERACTION_HANDLERS_TEST_MOCK_SAVE_ADDRESS_PROFILE_MODAL_INFOBAR_INTERACTION_HANDLER_H_
+
+#import "ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/autofill_address_profile/save_address_profile_infobar_modal_interaction_handler.h"
+
+#include "testing/gmock/include/gmock/gmock.h"
+
+class InfoBarIOS;
+
+// Mock version of SaveAddressProfileInfobarModalInteractionHandler for use in
+// tests.
+class MockSaveAddressProfileInfobarModalInteractionHandler
+    : public SaveAddressProfileInfobarModalInteractionHandler {
+ public:
+  MockSaveAddressProfileInfobarModalInteractionHandler();
+  ~MockSaveAddressProfileInfobarModalInteractionHandler();
+
+  MOCK_METHOD2(SaveEditedProfile,
+               void(InfoBarIOS* infobar, NSDictionary* profileData));
+};
+
+#endif  // IOS_CHROME_BROWSER_INFOBARS_OVERLAYS_BROWSER_AGENT_INTERACTION_HANDLERS_TEST_MOCK_SAVE_ADDRESS_PROFILE_MODAL_INFOBAR_INTERACTION_HANDLER_H_
diff --git a/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_save_address_profile_modal_infobar_interaction_handler.mm b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_save_address_profile_modal_infobar_interaction_handler.mm
new file mode 100644
index 0000000..610673d
--- /dev/null
+++ b/ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_save_address_profile_modal_infobar_interaction_handler.mm
@@ -0,0 +1,15 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/infobars/overlays/browser_agent/interaction_handlers/test/mock_save_address_profile_modal_infobar_interaction_handler.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+MockSaveAddressProfileInfobarModalInteractionHandler::
+    MockSaveAddressProfileInfobarModalInteractionHandler() = default;
+
+MockSaveAddressProfileInfobarModalInteractionHandler::
+    ~MockSaveAddressProfileInfobarModalInteractionHandler() = default;
diff --git a/ios/chrome/browser/infobars/overlays/infobar_banner_overlay_request_cancel_handler.h b/ios/chrome/browser/infobars/overlays/infobar_banner_overlay_request_cancel_handler.h
index 49a5520..000c37c 100644
--- a/ios/chrome/browser/infobars/overlays/infobar_banner_overlay_request_cancel_handler.h
+++ b/ios/chrome/browser/infobars/overlays/infobar_banner_overlay_request_cancel_handler.h
@@ -7,7 +7,7 @@
 
 #import "ios/chrome/browser/infobars/overlays/infobar_overlay_request_cancel_handler.h"
 
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #import "ios/chrome/browser/infobars/overlays/infobar_modal_completion_notifier.h"
 #import "ios/chrome/browser/infobars/overlays/infobar_overlay_request_inserter.h"
 
@@ -44,9 +44,9 @@
     InfobarBannerOverlayRequestCancelHandler* cancel_handler_ = nullptr;
     // The infobar for which to look for modal insertions.
     InfoBarIOS* infobar_ = nullptr;
-    ScopedObserver<InfobarOverlayRequestInserter,
-                   InfobarOverlayRequestInserter::Observer>
-        scoped_observer_;
+    base::ScopedObservation<InfobarOverlayRequestInserter,
+                            InfobarOverlayRequestInserter::Observer>
+        scoped_observation_{this};
   };
 
   // Helper object that triggers request cancellation for the completion of
@@ -71,9 +71,9 @@
     InfobarBannerOverlayRequestCancelHandler* cancel_handler_ = nullptr;
     // The infobar whose modal dismissals should trigger cancellation.
     InfoBarIOS* infobar_ = nullptr;
-    ScopedObserver<InfobarModalCompletionNotifier,
-                   InfobarModalCompletionNotifier::Observer>
-        scoped_observer_;
+    base::ScopedObservation<InfobarModalCompletionNotifier,
+                            InfobarModalCompletionNotifier::Observer>
+        scoped_observation_{this};
   };
 
   // Indicates to the cancel handler that its banner presented a modal.
diff --git a/ios/chrome/browser/infobars/overlays/infobar_banner_overlay_request_cancel_handler.mm b/ios/chrome/browser/infobars/overlays/infobar_banner_overlay_request_cancel_handler.mm
index 8769e47..e27e6c3 100644
--- a/ios/chrome/browser/infobars/overlays/infobar_banner_overlay_request_cancel_handler.mm
+++ b/ios/chrome/browser/infobars/overlays/infobar_banner_overlay_request_cancel_handler.mm
@@ -67,13 +67,11 @@
     InfobarOverlayRequestInserter* inserter,
     InfoBarIOS* infobar,
     InfobarBannerOverlayRequestCancelHandler* cancel_handler)
-    : cancel_handler_(cancel_handler),
-      infobar_(infobar),
-      scoped_observer_(this) {
+    : cancel_handler_(cancel_handler), infobar_(infobar) {
   DCHECK(inserter);
   DCHECK(infobar);
   DCHECK(cancel_handler);
-  scoped_observer_.Add(inserter);
+  scoped_observation_.Observe(inserter);
 }
 
 InfobarBannerOverlayRequestCancelHandler::InsertionObserver::
@@ -89,7 +87,8 @@
 
 void InfobarBannerOverlayRequestCancelHandler::InsertionObserver::
     InserterDestroyed(InfobarOverlayRequestInserter* inserter) {
-  scoped_observer_.Remove(inserter);
+  DCHECK(scoped_observation_.IsObservingSource(inserter));
+  scoped_observation_.Reset();
 }
 
 #pragma mark - InfobarBannerOverlayRequestCancelHandler::ModalCompletionObserver
@@ -99,13 +98,11 @@
         InfobarBannerOverlayRequestCancelHandler* cancel_handler,
         InfobarModalCompletionNotifier* completion_notifier,
         InfoBarIOS* infobar)
-    : cancel_handler_(cancel_handler),
-      infobar_(infobar),
-      scoped_observer_(this) {
+    : cancel_handler_(cancel_handler), infobar_(infobar) {
   DCHECK(cancel_handler_);
   DCHECK(infobar_);
   DCHECK(completion_notifier);
-  scoped_observer_.Add(completion_notifier);
+  scoped_observation_.Observe(completion_notifier);
 }
 
 InfobarBannerOverlayRequestCancelHandler::ModalCompletionObserver::
@@ -124,7 +121,8 @@
 void InfobarBannerOverlayRequestCancelHandler::ModalCompletionObserver::
     InfobarModalCompletionNotifierDestroyed(
         InfobarModalCompletionNotifier* notifier) {
-  scoped_observer_.Remove(notifier);
+  DCHECK(scoped_observation_.IsObservingSource(notifier));
+  scoped_observation_.Reset();
   cancel_handler_->ModalCompleted();
   // The cancel handler is destroyed after CancelForModalCompletion(), so no
   // code can be added after this call.
diff --git a/ios/chrome/browser/infobars/overlays/infobar_modal_completion_notifier_unittest.mm b/ios/chrome/browser/infobars/overlays/infobar_modal_completion_notifier_unittest.mm
index 51048eb4..5ed47ea1 100644
--- a/ios/chrome/browser/infobars/overlays/infobar_modal_completion_notifier_unittest.mm
+++ b/ios/chrome/browser/infobars/overlays/infobar_modal_completion_notifier_unittest.mm
@@ -4,7 +4,7 @@
 
 #import "ios/chrome/browser/infobars/overlays/infobar_modal_completion_notifier.h"
 
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #import "ios/chrome/browser/infobars/test/fake_infobar_ios.h"
 #import "ios/chrome/browser/overlays/public/common/infobars/infobar_overlay_request_config.h"
 #include "ios/chrome/browser/overlays/public/overlay_request_queue.h"
@@ -34,9 +34,8 @@
 // Test fixture for InfobarModalCompletionNotifier.
 class InfobarModalCompletionNotifierTest : public PlatformTest {
  public:
-  InfobarModalCompletionNotifierTest()
-      : notifier_(&web_state_), scoped_observer_(&observer_) {
-    scoped_observer_.Add(&notifier_);
+  InfobarModalCompletionNotifierTest() : notifier_(&web_state_) {
+    scoped_observation_.Observe(&notifier_);
   }
 
   OverlayRequestQueue* queue() {
@@ -49,9 +48,9 @@
   FakeInfobarIOS infobar_;
   InfobarModalCompletionNotifier notifier_;
   MockInfobarModalCompletionNotifierObserver observer_;
-  ScopedObserver<InfobarModalCompletionNotifier,
-                 InfobarModalCompletionNotifier::Observer>
-      scoped_observer_;
+  base::ScopedObservation<InfobarModalCompletionNotifier,
+                          InfobarModalCompletionNotifier::Observer>
+      scoped_observation_{&observer_};
 };
 
 // Tests that the observer is notified when all modal requests for |infobar_|
diff --git a/ios/chrome/browser/infobars/overlays/infobar_modal_overlay_request_cancel_handler.h b/ios/chrome/browser/infobars/overlays/infobar_modal_overlay_request_cancel_handler.h
index 3609aa2..49a514f2 100644
--- a/ios/chrome/browser/infobars/overlays/infobar_modal_overlay_request_cancel_handler.h
+++ b/ios/chrome/browser/infobars/overlays/infobar_modal_overlay_request_cancel_handler.h
@@ -7,7 +7,7 @@
 
 #import "ios/chrome/browser/infobars/overlays/infobar_overlay_request_cancel_handler.h"
 
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #import "ios/chrome/browser/infobars/overlays/infobar_modal_completion_notifier.h"
 
 // A cancel handler for Infobar modal UI OverlayRequests.
@@ -46,9 +46,9 @@
     InfobarModalOverlayRequestCancelHandler* cancel_handler_ = nullptr;
     // The infobar whose modal dismissals should trigger cancellation.
     InfoBarIOS* infobar_ = nullptr;
-    ScopedObserver<InfobarModalCompletionNotifier,
-                   InfobarModalCompletionNotifier::Observer>
-        scoped_observer_;
+    base::ScopedObservation<InfobarModalCompletionNotifier,
+                            InfobarModalCompletionNotifier::Observer>
+        scoped_observation_{this};
   };
 
   // Cancels the request for modal completion.
diff --git a/ios/chrome/browser/infobars/overlays/infobar_modal_overlay_request_cancel_handler.mm b/ios/chrome/browser/infobars/overlays/infobar_modal_overlay_request_cancel_handler.mm
index 45bec16..3606f99c 100644
--- a/ios/chrome/browser/infobars/overlays/infobar_modal_overlay_request_cancel_handler.mm
+++ b/ios/chrome/browser/infobars/overlays/infobar_modal_overlay_request_cancel_handler.mm
@@ -35,13 +35,11 @@
         InfobarModalOverlayRequestCancelHandler* cancel_handler,
         InfobarModalCompletionNotifier* completion_notifier,
         InfoBarIOS* infobar)
-    : cancel_handler_(cancel_handler),
-      infobar_(infobar),
-      scoped_observer_(this) {
+    : cancel_handler_(cancel_handler), infobar_(infobar) {
   DCHECK(cancel_handler_);
   DCHECK(infobar_);
   DCHECK(completion_notifier);
-  scoped_observer_.Add(completion_notifier);
+  scoped_observation_.Observe(completion_notifier);
 }
 
 InfobarModalOverlayRequestCancelHandler::ModalCompletionObserver::
@@ -60,7 +58,8 @@
 void InfobarModalOverlayRequestCancelHandler::ModalCompletionObserver::
     InfobarModalCompletionNotifierDestroyed(
         InfobarModalCompletionNotifier* notifier) {
-  scoped_observer_.Remove(notifier);
+  DCHECK(scoped_observation_.IsObservingSource(notifier));
+  scoped_observation_.Reset();
   cancel_handler_->CancelForModalCompletion();
   // The cancel handler is destroyed after CancelForModalCompletion(), so no
   // code can be added after this call.
diff --git a/ios/chrome/browser/infobars/overlays/infobar_overlay_request_cancel_handler.h b/ios/chrome/browser/infobars/overlays/infobar_overlay_request_cancel_handler.h
index 0ad415f..2628532 100644
--- a/ios/chrome/browser/infobars/overlays/infobar_overlay_request_cancel_handler.h
+++ b/ios/chrome/browser/infobars/overlays/infobar_overlay_request_cancel_handler.h
@@ -5,7 +5,7 @@
 #ifndef IOS_CHROME_BROWSER_INFOBARS_OVERLAYS_INFOBAR_OVERLAY_REQUEST_CANCEL_HANDLER_H_
 #define IOS_CHROME_BROWSER_INFOBARS_OVERLAYS_INFOBAR_OVERLAY_REQUEST_CANCEL_HANDLER_H_
 
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #include "components/infobars/core/infobar_manager.h"
 #import "ios/chrome/browser/overlays/public/overlay_request_cancel_handler.h"
 
@@ -48,8 +48,9 @@
 
    private:
     InfobarOverlayRequestCancelHandler* cancel_handler_ = nullptr;
-    ScopedObserver<infobars::InfoBarManager, infobars::InfoBarManager::Observer>
-        scoped_observer_;
+    base::ScopedObservation<infobars::InfoBarManager,
+                            infobars::InfoBarManager::Observer>
+        scoped_observation_{this};
   };
 
   InfoBarIOS* infobar_ = nullptr;
diff --git a/ios/chrome/browser/infobars/overlays/infobar_overlay_request_cancel_handler.mm b/ios/chrome/browser/infobars/overlays/infobar_overlay_request_cancel_handler.mm
index 03e047d..28b0a41b 100644
--- a/ios/chrome/browser/infobars/overlays/infobar_overlay_request_cancel_handler.mm
+++ b/ios/chrome/browser/infobars/overlays/infobar_overlay_request_cancel_handler.mm
@@ -48,11 +48,11 @@
 
 InfobarOverlayRequestCancelHandler::RemovalObserver::RemovalObserver(
     InfobarOverlayRequestCancelHandler* cancel_handler)
-    : cancel_handler_(cancel_handler), scoped_observer_(this) {
+    : cancel_handler_(cancel_handler) {
   DCHECK(cancel_handler_);
   InfoBarManager* manager = cancel_handler_->infobar()->owner();
   DCHECK(manager);
-  scoped_observer_.Add(manager);
+  scoped_observation_.Observe(manager);
 }
 
 InfobarOverlayRequestCancelHandler::RemovalObserver::~RemovalObserver() =
@@ -81,7 +81,8 @@
 
 void InfobarOverlayRequestCancelHandler::RemovalObserver::OnManagerShuttingDown(
     infobars::InfoBarManager* manager) {
-  scoped_observer_.Remove(manager);
+  DCHECK(scoped_observation_.IsObservingSource(manager));
+  scoped_observation_.Reset();
   cancel_handler_->CancelForInfobarRemoval();
   // The cancel handler is destroyed after Cancel(), so no code can be added
   // after this call.
diff --git a/ios/chrome/browser/infobars/overlays/infobar_overlay_tab_helper.h b/ios/chrome/browser/infobars/overlays/infobar_overlay_tab_helper.h
index 122d6ef4..a2dfa3e 100644
--- a/ios/chrome/browser/infobars/overlays/infobar_overlay_tab_helper.h
+++ b/ios/chrome/browser/infobars/overlays/infobar_overlay_tab_helper.h
@@ -5,8 +5,7 @@
 #ifndef IOS_CHROME_BROWSER_INFOBARS_OVERLAYS_INFOBAR_OVERLAY_TAB_HELPER_H_
 #define IOS_CHROME_BROWSER_INFOBARS_OVERLAYS_INFOBAR_OVERLAY_TAB_HELPER_H_
 
-
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #include "components/infobars/core/infobar_manager.h"
 #import "ios/web/public/web_state_user_data.h"
 
@@ -46,8 +45,9 @@
     // The owning tab helper.
     InfobarOverlayTabHelper* tab_helper_ = nullptr;
     web::WebState* web_state_ = nullptr;
-    ScopedObserver<infobars::InfoBarManager, infobars::InfoBarManager::Observer>
-        scoped_observer_;
+    base::ScopedObservation<infobars::InfoBarManager,
+                            infobars::InfoBarManager::Observer>
+        scoped_observation_{this};
   };
 
   // The inserter used to add infobar OverlayRequests to the WebState's queue.
diff --git a/ios/chrome/browser/infobars/overlays/infobar_overlay_tab_helper.mm b/ios/chrome/browser/infobars/overlays/infobar_overlay_tab_helper.mm
index 089f359..02f95c5 100644
--- a/ios/chrome/browser/infobars/overlays/infobar_overlay_tab_helper.mm
+++ b/ios/chrome/browser/infobars/overlays/infobar_overlay_tab_helper.mm
@@ -34,11 +34,11 @@
 InfobarOverlayTabHelper::OverlayRequestScheduler::OverlayRequestScheduler(
     web::WebState* web_state,
     InfobarOverlayTabHelper* tab_helper)
-    : tab_helper_(tab_helper), web_state_(web_state), scoped_observer_(this) {
+    : tab_helper_(tab_helper), web_state_(web_state) {
   DCHECK(tab_helper_);
   InfoBarManager* manager = InfoBarManagerImpl::FromWebState(web_state);
   DCHECK(manager);
-  scoped_observer_.Add(manager);
+  scoped_observation_.Observe(manager);
 }
 
 InfobarOverlayTabHelper::OverlayRequestScheduler::~OverlayRequestScheduler() =
@@ -67,5 +67,6 @@
 
 void InfobarOverlayTabHelper::OverlayRequestScheduler::OnManagerShuttingDown(
     InfoBarManager* manager) {
-  scoped_observer_.Remove(manager);
+  DCHECK(scoped_observation_.IsObservingSource(manager));
+  scoped_observation_.Reset();
 }
diff --git a/ios/chrome/browser/infobars/overlays/translate_infobar_placeholder_overlay_request_cancel_handler.h b/ios/chrome/browser/infobars/overlays/translate_infobar_placeholder_overlay_request_cancel_handler.h
index ec88eb1..77c5f28 100644
--- a/ios/chrome/browser/infobars/overlays/translate_infobar_placeholder_overlay_request_cancel_handler.h
+++ b/ios/chrome/browser/infobars/overlays/translate_infobar_placeholder_overlay_request_cancel_handler.h
@@ -7,7 +7,7 @@
 
 #import "ios/chrome/browser/infobars/overlays/infobar_overlay_request_cancel_handler.h"
 
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #include "ios/chrome/browser/infobars/infobar_ios.h"
 #import "ios/chrome/browser/infobars/overlays/translate_overlay_tab_helper.h"
 
@@ -49,9 +49,9 @@
 
     PlaceholderRequestCancelHandler* cancel_handler_;
 
-    ScopedObserver<TranslateOverlayTabHelper,
-                   TranslateOverlayTabHelper::Observer>
-        scoped_observer_;
+    base::ScopedObservation<TranslateOverlayTabHelper,
+                            TranslateOverlayTabHelper::Observer>
+        scoped_observation_{this};
   };
 
   // Indicates to the cancel handler that the translation has finished.
diff --git a/ios/chrome/browser/infobars/overlays/translate_infobar_placeholder_overlay_request_cancel_handler.mm b/ios/chrome/browser/infobars/overlays/translate_infobar_placeholder_overlay_request_cancel_handler.mm
index 616f3c24..71629ae 100644
--- a/ios/chrome/browser/infobars/overlays/translate_infobar_placeholder_overlay_request_cancel_handler.mm
+++ b/ios/chrome/browser/infobars/overlays/translate_infobar_placeholder_overlay_request_cancel_handler.mm
@@ -37,8 +37,8 @@
 PlaceholderRequestCancelHandler::TranslationFinishedObserver::
     TranslationFinishedObserver(TranslateOverlayTabHelper* tab_helper,
                                 PlaceholderRequestCancelHandler* cancel_handler)
-    : cancel_handler_(cancel_handler), scoped_observer_(this) {
-  scoped_observer_.Add(tab_helper);
+    : cancel_handler_(cancel_handler) {
+  scoped_observation_.Observe(tab_helper);
 }
 
 PlaceholderRequestCancelHandler::TranslationFinishedObserver::
@@ -51,7 +51,8 @@
 
 void PlaceholderRequestCancelHandler::TranslationFinishedObserver::
     TranslateOverlayTabHelperDestroyed(TranslateOverlayTabHelper* tab_helper) {
-  scoped_observer_.Remove(tab_helper);
+  DCHECK(scoped_observation_.IsObservingSource(tab_helper));
+  scoped_observation_.Reset();
 }
 
 }  // namespace translate_infobar_overlays
diff --git a/ios/chrome/browser/infobars/overlays/translate_overlay_tab_helper.h b/ios/chrome/browser/infobars/overlays/translate_overlay_tab_helper.h
index 3dae119..76d8d9bd 100644
--- a/ios/chrome/browser/infobars/overlays/translate_overlay_tab_helper.h
+++ b/ios/chrome/browser/infobars/overlays/translate_overlay_tab_helper.h
@@ -9,7 +9,7 @@
 
 #include "base/observer_list.h"
 #include "base/observer_list_types.h"
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #include "components/infobars/core/infobar_manager.h"
 #include "components/translate/core/browser/translate_infobar_delegate.h"
 #include "ios/web/public/web_state_observer.h"
@@ -74,9 +74,9 @@
         translate::TranslateInfoBarDelegate* delegate) override;
 
     // Scoped observer that facilitates observing a TranslateInfoBarDelegate.
-    ScopedObserver<translate::TranslateInfoBarDelegate,
-                   translate::TranslateInfoBarDelegate::Observer>
-        translate_scoped_observer_;
+    base::ScopedObservation<translate::TranslateInfoBarDelegate,
+                            translate::TranslateInfoBarDelegate::Observer>
+        translate_scoped_observation_{this};
     // TranslateOverlayTabHelper instance.
     TranslateOverlayTabHelper* tab_helper_;
     infobars::InfoBar* translate_infobar_ = nil;
@@ -95,8 +95,9 @@
     void OnManagerShuttingDown(infobars::InfoBarManager* manager) override;
 
     // Scoped observer that facilitates observing an InfoBarManager
-    ScopedObserver<infobars::InfoBarManager, infobars::InfoBarManager::Observer>
-        infobar_manager_scoped_observer_;
+    base::ScopedObservation<infobars::InfoBarManager,
+                            infobars::InfoBarManager::Observer>
+        infobar_manager_scoped_observation_{this};
     // TranslateOverlayTabHelper instance.
     TranslateOverlayTabHelper* tab_helper_;
   };
@@ -114,8 +115,8 @@
     void WebStateDestroyed(web::WebState* web_state) override;
 
     // Scoped observer that facilitates observing an InfoBarManager
-    ScopedObserver<web::WebState, web::WebStateObserver>
-        web_state_scoped_observer_;
+    base::ScopedObservation<web::WebState, web::WebStateObserver>
+        web_state_scoped_observation_{this};
     // TranslateOverlayTabHelper instance.
     TranslateOverlayTabHelper* tab_helper_;
   };
diff --git a/ios/chrome/browser/infobars/overlays/translate_overlay_tab_helper.mm b/ios/chrome/browser/infobars/overlays/translate_overlay_tab_helper.mm
index e0312a6..595502a 100644
--- a/ios/chrome/browser/infobars/overlays/translate_overlay_tab_helper.mm
+++ b/ios/chrome/browser/infobars/overlays/translate_overlay_tab_helper.mm
@@ -123,7 +123,7 @@
 
 TranslateOverlayTabHelper::TranslateStepObserver::TranslateStepObserver(
     TranslateOverlayTabHelper* tab_helper)
-    : translate_scoped_observer_(this), tab_helper_(tab_helper) {}
+    : tab_helper_(tab_helper) {}
 
 TranslateOverlayTabHelper::TranslateStepObserver::~TranslateStepObserver() =
     default;
@@ -165,14 +165,15 @@
 void TranslateOverlayTabHelper::TranslateStepObserver::
     OnTranslateInfoBarDelegateDestroyed(
         translate::TranslateInfoBarDelegate* delegate) {
-  translate_scoped_observer_.Remove(delegate);
+  DCHECK(translate_scoped_observation_.IsObservingSource(delegate));
+  translate_scoped_observation_.Reset();
   translate_infobar_ = nil;
 }
 
 void TranslateOverlayTabHelper::TranslateStepObserver::SetTranslateInfoBar(
     InfoBarIOS* infobar) {
   translate_infobar_ = infobar;
-  translate_scoped_observer_.Add(
+  translate_scoped_observation_.Observe(
       infobar->delegate()->AsTranslateInfoBarDelegate());
 }
 
@@ -181,11 +182,11 @@
 TranslateOverlayTabHelper::TranslateInfobarObserver::TranslateInfobarObserver(
     web::WebState* web_state,
     TranslateOverlayTabHelper* tab_helper)
-    : infobar_manager_scoped_observer_(this), tab_helper_(tab_helper) {
+    : tab_helper_(tab_helper) {
   infobars::InfoBarManager* manager =
       InfoBarManagerImpl::FromWebState(web_state);
   DCHECK(manager);
-  infobar_manager_scoped_observer_.Add(manager);
+  infobar_manager_scoped_observation_.Observe(manager);
 }
 
 TranslateOverlayTabHelper::TranslateInfobarObserver::
@@ -202,7 +203,8 @@
 
 void TranslateOverlayTabHelper::TranslateInfobarObserver::OnManagerShuttingDown(
     infobars::InfoBarManager* manager) {
-  infobar_manager_scoped_observer_.Remove(manager);
+  DCHECK(infobar_manager_scoped_observation_.IsObservingSource(manager));
+  infobar_manager_scoped_observation_.Reset();
 }
 
 #pragma mark - WebStateDestroyedObserver
@@ -210,8 +212,8 @@
 TranslateOverlayTabHelper::WebStateDestroyedObserver::WebStateDestroyedObserver(
     web::WebState* web_state,
     TranslateOverlayTabHelper* tab_helper)
-    : web_state_scoped_observer_(this), tab_helper_(tab_helper) {
-  web_state_scoped_observer_.Add(web_state);
+    : tab_helper_(tab_helper) {
+  web_state_scoped_observation_.Observe(web_state);
 }
 
 TranslateOverlayTabHelper::WebStateDestroyedObserver::
@@ -219,6 +221,7 @@
 
 void TranslateOverlayTabHelper::WebStateDestroyedObserver::WebStateDestroyed(
     web::WebState* web_state) {
-  web_state_scoped_observer_.Remove(web_state);
+  DCHECK(web_state_scoped_observation_.IsObservingSource(web_state));
+  web_state_scoped_observation_.Reset();
   tab_helper_->UpdateForWebStateDestroyed();
 }
diff --git a/ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_responses.h b/ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_responses.h
index f80cc63..c50aea50 100644
--- a/ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_responses.h
+++ b/ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_responses.h
@@ -11,9 +11,21 @@
 
 namespace save_address_profile_infobar_modal_responses {
 
-// Response info used to create dispatched OverlayResponses that notify the
-// save address profile infobar to present the address profile settings.
-DEFINE_STATELESS_OVERLAY_RESPONSE_INFO(PresentAddressProfileSettings);
+// Response info used to create dispatched OverlayResponses once the user
+// presses "Save" action on the Edit Modal.
+class EditedProfileSaveAction
+    : public OverlayResponseInfo<EditedProfileSaveAction> {
+ public:
+  ~EditedProfileSaveAction() override;
+
+  NSDictionary* profile_data() const { return profile_data_; }
+
+ private:
+  OVERLAY_USER_DATA_SETUP(EditedProfileSaveAction);
+  EditedProfileSaveAction(NSDictionary* profileData);
+
+  NSDictionary* profile_data_;
+};
 
 }  // namespace save_address_profile_infobar_modal_responses
 
diff --git a/ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_responses.mm b/ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_responses.mm
index ade0c30..0c0f1b7d 100644
--- a/ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_responses.mm
+++ b/ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_responses.mm
@@ -10,8 +10,13 @@
 
 namespace save_address_profile_infobar_modal_responses {
 
-#pragma mark - PresentAddressProfileSettings
+#pragma mark - EditedProfileSaveAction
 
-OVERLAY_USER_DATA_SETUP_IMPL(PresentAddressProfileSettings);
+OVERLAY_USER_DATA_SETUP_IMPL(EditedProfileSaveAction);
+
+EditedProfileSaveAction::EditedProfileSaveAction(NSDictionary* profileData)
+    : profile_data_(profileData) {}
+
+EditedProfileSaveAction::~EditedProfileSaveAction() = default;
 
 }  // save_address_profile_infobar_modal_responses
diff --git a/ios/chrome/browser/signin/authentication_service.h b/ios/chrome/browser/signin/authentication_service.h
index c3f3d739..071403f4 100644
--- a/ios/chrome/browser/signin/authentication_service.h
+++ b/ios/chrome/browser/signin/authentication_service.h
@@ -10,7 +10,7 @@
 
 #import "base/ios/block_types.h"
 #include "base/memory/weak_ptr.h"
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #include "components/keyed_service/core/keyed_service.h"
 #include "components/pref_registry/pref_registry_syncable.h"
 #include "components/signin/public/base/signin_metrics.h"
@@ -234,12 +234,13 @@
   // Map between account IDs and their associated MDM error.
   mutable std::map<CoreAccountId, NSDictionary*> cached_mdm_infos_;
 
-  ScopedObserver<ios::ChromeIdentityService,
-                 ios::ChromeIdentityService::Observer>
-      identity_service_observer_;
+  base::ScopedObservation<ios::ChromeIdentityService,
+                          ios::ChromeIdentityService::Observer>
+      identity_service_observation_{this};
 
-  ScopedObserver<signin::IdentityManager, signin::IdentityManager::Observer>
-      identity_manager_observer_;
+  base::ScopedObservation<signin::IdentityManager,
+                          signin::IdentityManager::Observer>
+      identity_manager_observation_{this};
 
   base::WeakPtrFactory<AuthenticationService> weak_pointer_factory_;
 
diff --git a/ios/chrome/browser/signin/authentication_service.mm b/ios/chrome/browser/signin/authentication_service.mm
index 02d28b16..c39ba2e 100644
--- a/ios/chrome/browser/signin/authentication_service.mm
+++ b/ios/chrome/browser/signin/authentication_service.mm
@@ -77,8 +77,6 @@
       sync_setup_service_(sync_setup_service),
       identity_manager_(identity_manager),
       sync_service_(sync_service),
-      identity_service_observer_(this),
-      identity_manager_observer_(this),
       weak_pointer_factory_(this) {
   DCHECK(pref_service_);
   DCHECK(sync_setup_service_);
@@ -112,14 +110,14 @@
 
   crash_keys::SetCurrentlySignedIn(IsAuthenticated());
 
-  identity_service_observer_.Add(
+  identity_service_observation_.Observe(
       ios::GetChromeBrowserProvider()->GetChromeIdentityService());
 
   OnApplicationWillEnterForeground();
 }
 
 void AuthenticationService::Shutdown() {
-  identity_manager_observer_.RemoveAll();
+  identity_manager_observation_.Reset();
   delegate_.reset();
 }
 
@@ -127,7 +125,7 @@
   if (InForeground())
     return;
 
-  identity_manager_observer_.Add(identity_manager_);
+  identity_manager_observation_.Observe(identity_manager_);
 
   // As the SSO library does not send notification when the app is in the
   // background, reload the credentials and check whether any accounts have
@@ -172,8 +170,9 @@
 
   // Stop observing |identity_manager_| when in the background. Note that
   // this allows checking whether the app is in background without having a
-  // separate bool by using identity_manager_observer_.IsObservingSources().
-  identity_manager_observer_.Remove(identity_manager_);
+  // separate bool by using identity_manager_observation_.IsObserving().
+  DCHECK(identity_manager_observation_.IsObservingSource(identity_manager_));
+  identity_manager_observation_.Reset();
 
   // Reset the state |have_accounts_changed_while_in_background_| as the
   // application just entered background.
@@ -181,9 +180,9 @@
 }
 
 bool AuthenticationService::InForeground() const {
-  // The application is in foreground when |identity_manager_observer_| is
+  // The application is in foreground when |identity_manager_observation_| is
   // observing sources.
-  return identity_manager_observer_.IsObservingSources();
+  return identity_manager_observation_.IsObserving();
 }
 
 void AuthenticationService::SetPromptForSignIn() {
@@ -456,8 +455,8 @@
 }
 
 void AuthenticationService::ResetChromeIdentityServiceObserverForTesting() {
-  DCHECK(!identity_service_observer_.IsObservingSources());
-  identity_service_observer_.Add(
+  DCHECK(!identity_service_observation_.IsObserving());
+  identity_service_observation_.Observe(
       ios::GetChromeBrowserProvider()->GetChromeIdentityService());
 }
 
@@ -542,7 +541,7 @@
 }
 
 void AuthenticationService::OnChromeIdentityServiceWillBeDestroyed() {
-  identity_service_observer_.RemoveAll();
+  identity_service_observation_.Reset();
 }
 
 void AuthenticationService::HandleIdentityListChanged() {
diff --git a/ios/chrome/browser/signin/authentication_service_unittest.mm b/ios/chrome/browser/signin/authentication_service_unittest.mm
index 6876ad44..35d5cc2 100644
--- a/ios/chrome/browser/signin/authentication_service_unittest.mm
+++ b/ios/chrome/browser/signin/authentication_service_unittest.mm
@@ -6,7 +6,6 @@
 
 #include "base/bind.h"
 #include "base/run_loop.h"
-#include "base/scoped_observer.h"
 #include "base/strings/sys_string_conversions.h"
 #include "base/test/bind.h"
 #include "base/test/gtest_util.h"
diff --git a/ios/chrome/browser/signin/chrome_identity_service_observer_bridge.h b/ios/chrome/browser/signin/chrome_identity_service_observer_bridge.h
index 024253b..b66868b 100644
--- a/ios/chrome/browser/signin/chrome_identity_service_observer_bridge.h
+++ b/ios/chrome/browser/signin/chrome_identity_service_observer_bridge.h
@@ -8,7 +8,7 @@
 #import <Foundation/Foundation.h>
 
 #include "base/macros.h"
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #include "ios/public/provider/chrome/browser/signin/chrome_identity_service.h"
 
 // Objective-C protocol mirroring ChromeIdentityService::Observer.
@@ -38,9 +38,9 @@
   void OnChromeIdentityServiceWillBeDestroyed() override;
 
   __weak id<ChromeIdentityServiceObserver> observer_ = nil;
-  ScopedObserver<ios::ChromeIdentityService,
-                 ios::ChromeIdentityService::Observer>
-      scoped_observer_{this};
+  base::ScopedObservation<ios::ChromeIdentityService,
+                          ios::ChromeIdentityService::Observer>
+      scoped_observation_{this};
 
   DISALLOW_COPY_AND_ASSIGN(ChromeIdentityServiceObserverBridge);
 };
diff --git a/ios/chrome/browser/signin/chrome_identity_service_observer_bridge.mm b/ios/chrome/browser/signin/chrome_identity_service_observer_bridge.mm
index 1b9cb47..abcb0d0d 100644
--- a/ios/chrome/browser/signin/chrome_identity_service_observer_bridge.mm
+++ b/ios/chrome/browser/signin/chrome_identity_service_observer_bridge.mm
@@ -15,7 +15,7 @@
     id<ChromeIdentityServiceObserver> observer)
     : observer_(observer) {
   DCHECK(observer_);
-  scoped_observer_.Add(
+  scoped_observation_.Observe(
       ios::GetChromeBrowserProvider()->GetChromeIdentityService());
 }
 
diff --git a/ios/chrome/browser/signin/signin_browser_state_info_updater.h b/ios/chrome/browser/signin/signin_browser_state_info_updater.h
index 452b0241..10a7981 100644
--- a/ios/chrome/browser/signin/signin_browser_state_info_updater.h
+++ b/ios/chrome/browser/signin/signin_browser_state_info_updater.h
@@ -7,7 +7,7 @@
 
 #include "base/files/file_path.h"
 #include "base/macros.h"
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #include "build/build_config.h"
 #include "components/keyed_service/core/keyed_service.h"
 #include "components/signin/core/browser/signin_error_controller.h"
@@ -42,10 +42,12 @@
   signin::IdentityManager* identity_manager_ = nullptr;
   SigninErrorController* signin_error_controller_ = nullptr;
   const base::FilePath browser_state_path_;
-  ScopedObserver<signin::IdentityManager, signin::IdentityManager::Observer>
-      identity_manager_observer_{this};
-  ScopedObserver<SigninErrorController, SigninErrorController::Observer>
-      signin_error_controller_observer_{this};
+  base::ScopedObservation<signin::IdentityManager,
+                          signin::IdentityManager::Observer>
+      identity_manager_observation_{this};
+  base::ScopedObservation<SigninErrorController,
+                          SigninErrorController::Observer>
+      signin_error_controller_observation_{this};
 
   DISALLOW_COPY_AND_ASSIGN(SigninBrowserStateInfoUpdater);
 };
diff --git a/ios/chrome/browser/signin/signin_browser_state_info_updater.mm b/ios/chrome/browser/signin/signin_browser_state_info_updater.mm
index 5293116..45364763 100644
--- a/ios/chrome/browser/signin/signin_browser_state_info_updater.mm
+++ b/ios/chrome/browser/signin/signin_browser_state_info_updater.mm
@@ -27,9 +27,9 @@
   if (!GetApplicationContext()->GetChromeBrowserStateManager())
     return;
 
-  identity_manager_observer_.Add(identity_manager_);
+  identity_manager_observation_.Observe(identity_manager_);
 
-  signin_error_controller_observer_.Add(signin_error_controller);
+  signin_error_controller_observation_.Observe(signin_error_controller);
 
   UpdateBrowserStateInfo();
   // TODO(crbug.com/908457): Call OnErrorChanged() here, to catch any change
@@ -40,8 +40,8 @@
 SigninBrowserStateInfoUpdater::~SigninBrowserStateInfoUpdater() = default;
 
 void SigninBrowserStateInfoUpdater::Shutdown() {
-  identity_manager_observer_.RemoveAll();
-  signin_error_controller_observer_.RemoveAll();
+  identity_manager_observation_.Reset();
+  signin_error_controller_observation_.Reset();
 }
 
 void SigninBrowserStateInfoUpdater::UpdateBrowserStateInfo() {
diff --git a/ios/chrome/browser/tabs/tab_helper_delegate_installer.h b/ios/chrome/browser/tabs/tab_helper_delegate_installer.h
index ad0f6b6..3c65f39 100644
--- a/ios/chrome/browser/tabs/tab_helper_delegate_installer.h
+++ b/ios/chrome/browser/tabs/tab_helper_delegate_installer.h
@@ -6,7 +6,7 @@
 #define IOS_CHROME_BROWSER_TABS_TAB_HELPER_DELEGATE_INSTALLER_H_
 
 #include "base/check.h"
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #import "ios/chrome/browser/main/browser.h"
 #import "ios/chrome/browser/main/browser_observer.h"
 #import "ios/chrome/browser/web_state_list/web_state_list.h"
@@ -64,7 +64,7 @@
         : delegate_(delegate), web_state_list_(web_state_list) {
       DCHECK(delegate_);
       DCHECK(web_state_list_);
-      scoped_observer_.Add(web_state_list_);
+      scoped_observation_.Observe(web_state_list_);
       for (int i = 0; i < web_state_list_->count(); ++i) {
         SetTabHelperDelegate(web_state_list_->GetWebStateAt(i), delegate_);
       }
@@ -78,7 +78,8 @@
       for (int i = 0; i < web_state_list_->count(); ++i) {
         SetTabHelperDelegate(web_state_list_->GetWebStateAt(i), nullptr);
       }
-      scoped_observer_.Remove(web_state_list_);
+      DCHECK(scoped_observation_.IsObservingSource(web_state_list_));
+      scoped_observation_.Reset();
       web_state_list_ = nullptr;
     }
 
@@ -113,7 +114,8 @@
     // The WebStateList whose Helpers's delegates are being installed.
     WebStateList* web_state_list_ = nullptr;
     // Scoped observer for |web_state_list_|.
-    ScopedObserver<WebStateList, WebStateListObserver> scoped_observer_{this};
+    base::ScopedObservation<WebStateList, WebStateListObserver>
+        scoped_observation_{this};
   };
 
   // Helper object that sets up the delegate installer and tears it down
@@ -124,7 +126,7 @@
         : installer_(installer) {
       DCHECK(installer_);
       DCHECK(browser);
-      scoped_observer_.Add(browser);
+      scoped_observation_.Observe(browser);
     }
     ~BrowserShutdownHelper() override = default;
 
@@ -132,13 +134,14 @@
     // BrowserObserver:
     void BrowserDestroyed(Browser* browser) override {
       installer_->Disconnect();
-      scoped_observer_.Remove(browser);
+      DCHECK(scoped_observation_.IsObservingSource(browser));
+      scoped_observation_.Reset();
     }
 
     // The installer used to set up the Delegates.
     Installer* installer_ = nullptr;
     // Scoped observer for the Browser.
-    ScopedObserver<Browser, BrowserObserver> scoped_observer_{this};
+    base::ScopedObservation<Browser, BrowserObserver> scoped_observation_{this};
   };
 
   // Helper object that installs delegates for all the Browser's tab helpers.
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_browser_observer.h b/ios/chrome/browser/ui/fullscreen/fullscreen_browser_observer.h
index 04c18785..9c88cdb 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_browser_observer.h
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_browser_observer.h
@@ -5,7 +5,7 @@
 #ifndef IOS_CHROME_BROWSER_UI_FULLSCREEN_FULLSCREEN_BROWSER_OBSERVER_H_
 #define IOS_CHROME_BROWSER_UI_FULLSCREEN_FULLSCREEN_BROWSER_OBSERVER_H_
 
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #import "ios/chrome/browser/main/browser.h"
 #import "ios/chrome/browser/main/browser_observer.h"
 #import "ios/chrome/browser/ui/fullscreen/fullscreen_web_state_list_observer.h"
@@ -25,7 +25,7 @@
   // The FullscreenWebStateListObserver passed on construction.
   FullscreenWebStateListObserver* web_state_list_observer_;
   // Scoped observer that facilitates observing an BrowserObserver.
-  ScopedObserver<Browser, BrowserObserver> scoped_observer_;
+  base::ScopedObservation<Browser, BrowserObserver> scoped_observation_{this};
 };
 
 #endif  // IOS_CHROME_BROWSER_UI_FULLSCREEN_FULLSCREEN_BROWSER_OBSERVER_H_
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_browser_observer.mm b/ios/chrome/browser/ui/fullscreen/fullscreen_browser_observer.mm
index d921e24..7b5f9de 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_browser_observer.mm
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_browser_observer.mm
@@ -13,14 +13,13 @@
 FullscreenBrowserObserver::FullscreenBrowserObserver(
     FullscreenWebStateListObserver* web_state_list_observer,
     Browser* browser)
-    : web_state_list_observer_(web_state_list_observer),
-      scoped_observer_(this) {
+    : web_state_list_observer_(web_state_list_observer) {
   DCHECK(web_state_list_observer_);
   // TODO(crbug.com/790886): DCHECK |browser| once FullscreenController is fully
   // scoped to a Browser.
   if (browser) {
     web_state_list_observer_->SetWebStateList(browser->GetWebStateList());
-    scoped_observer_.Add(browser);
+    scoped_observation_.Observe(browser);
   }
 }
 
@@ -29,5 +28,6 @@
 void FullscreenBrowserObserver::FullscreenBrowserObserver::BrowserDestroyed(
     Browser* browser) {
   web_state_list_observer_->SetWebStateList(nullptr);
-  scoped_observer_.Remove(browser);
+  DCHECK(scoped_observation_.IsObservingSource(browser));
+  scoped_observation_.Reset();
 }
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater.h b/ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater.h
index dddbaa7..12440e9 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater.h
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater.h
@@ -5,7 +5,7 @@
 #ifndef IOS_CHROME_BROWSER_UI_FULLSCREEN_FULLSCREEN_UI_UPDATER_H_
 #define IOS_CHROME_BROWSER_UI_FULLSCREEN_FULLSCREEN_UI_UPDATER_H_
 
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #import "ios/chrome/browser/ui/fullscreen/fullscreen_controller.h"
 #import "ios/chrome/browser/ui/fullscreen/fullscreen_controller_observer.h"
 
@@ -60,7 +60,8 @@
   // The observer forwarder.
   FullscreenControllerObserverForwarder forwarder_;
   // Scoped observer for |forwarder_|.
-  ScopedObserver<FullscreenController, FullscreenControllerObserver> observer_;
+  base::ScopedObservation<FullscreenController, FullscreenControllerObserver>
+      observation_{&forwarder_};
 };
 
 #endif  // IOS_CHROME_BROWSER_UI_FULLSCREEN_FULLSCREEN_UI_UPDATER_H_
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater.mm b/ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater.mm
index 0bd4a9d..46d55f08 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater.mm
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater.mm
@@ -16,9 +16,9 @@
                                          id<FullscreenUIElement> ui_element)
     : controller_(controller),
       forwarder_(this, ui_element),
-      observer_(&forwarder_) {
+      observation_(&forwarder_) {
   DCHECK(controller_);
-  observer_.Add(controller_);
+  observation_.Observe(controller_);
 }
 
 FullscreenUIUpdater::~FullscreenUIUpdater() = default;
@@ -26,7 +26,8 @@
 void FullscreenUIUpdater::Disconnect() {
   if (!controller_)
     return;
-  observer_.Remove(controller_);
+  DCHECK(observation_.IsObservingSource(controller_));
+  observation_.Reset();
   controller_ = nullptr;
 }
 
diff --git a/ios/chrome/browser/ui/infobars/modals/BUILD.gn b/ios/chrome/browser/ui/infobars/modals/BUILD.gn
index 552ebc3..099c3b7 100644
--- a/ios/chrome/browser/ui/infobars/modals/BUILD.gn
+++ b/ios/chrome/browser/ui/infobars/modals/BUILD.gn
@@ -6,6 +6,7 @@
   configs += [ "//build/config/compiler:enable_arc" ]
   sources = [
     "infobar_edit_address_profile_modal_consumer.h",
+    "infobar_edit_address_profile_modal_delegate.h",
     "infobar_edit_address_profile_table_view_controller.h",
     "infobar_edit_address_profile_table_view_controller.mm",
     "infobar_modal_delegate.h",
diff --git a/ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_modal_delegate.h b/ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_modal_delegate.h
new file mode 100644
index 0000000..3f323efe
--- /dev/null
+++ b/ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_modal_delegate.h
@@ -0,0 +1,20 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CHROME_BROWSER_UI_INFOBARS_MODALS_INFOBAR_EDIT_ADDRESS_PROFILE_MODAL_DELEGATE_H_
+#define IOS_CHROME_BROWSER_UI_INFOBARS_MODALS_INFOBAR_EDIT_ADDRESS_PROFILE_MODAL_DELEGATE_H_
+
+#import <Foundation/Foundation.h>
+
+#import "ios/chrome/browser/ui/infobars/modals/infobar_modal_delegate.h"
+
+// Delegate to handle Edit Address Profile Infobar Modal actions.
+@protocol InfobarEditAddressProfileModalDelegate <InfobarModalDelegate>
+
+// Saves the edited profile data.
+- (void)saveEditedProfileWithData:(NSDictionary*)profileData;
+
+@end
+
+#endif  // IOS_CHROME_BROWSER_UI_INFOBARS_MODALS_INFOBAR_EDIT_ADDRESS_PROFILE_MODAL_DELEGATE_H_
diff --git a/ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_table_view_controller.mm b/ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_table_view_controller.mm
index e97fc9f..6540138 100644
--- a/ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_table_view_controller.mm
+++ b/ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_table_view_controller.mm
@@ -5,12 +5,15 @@
 #import "ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_table_view_controller.h"
 
 #include "base/mac/foundation_util.h"
+#include "base/metrics/user_metrics.h"
+#include "base/metrics/user_metrics_action.h"
 #include "base/stl_util.h"
 #include "components/autofill/core/common/autofill_features.h"
+#include "ios/chrome/browser/infobars/infobar_metrics_recorder.h"
 #import "ios/chrome/browser/ui/autofill/autofill_ui_type.h"
 #import "ios/chrome/browser/ui/autofill/autofill_ui_type_util.h"
 #import "ios/chrome/browser/ui/autofill/cells/autofill_edit_item.h"
-#import "ios/chrome/browser/ui/infobars/modals/infobar_save_address_profile_modal_delegate.h"
+#import "ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_modal_delegate.h"
 #import "ios/chrome/browser/ui/table_view/cells/table_view_cells_constants.h"
 #import "ios/chrome/browser/ui/table_view/cells/table_view_text_button_item.h"
 #import "ios/chrome/browser/ui/table_view/cells/table_view_text_edit_item.h"
@@ -30,6 +33,7 @@
 
 typedef NS_ENUM(NSInteger, SectionIdentifier) {
   SectionIdentifierFields = kSectionIdentifierEnumZero,
+  SectionIdentifierButton
 };
 
 typedef NS_ENUM(NSInteger, ItemType) {
@@ -42,7 +46,10 @@
 @interface InfobarEditAddressProfileTableViewController () <UITextFieldDelegate>
 
 // The delegate passed to this instance.
-@property(nonatomic, weak) id<InfobarSaveAddressProfileModalDelegate> delegate;
+@property(nonatomic, weak) id<InfobarEditAddressProfileModalDelegate> delegate;
+
+// Used to build and record metrics.
+@property(nonatomic, strong) InfobarMetricsRecorder* metricsRecorder;
 
 // All the data to be displayed in the edit dialog.
 @property(nonatomic, strong) NSMutableDictionary* profileData;
@@ -54,10 +61,12 @@
 #pragma mark - Initialization
 
 - (instancetype)initWithModalDelegate:
-    (id<InfobarSaveAddressProfileModalDelegate>)modalDelegate {
+    (id<InfobarEditAddressProfileModalDelegate>)modalDelegate {
   self = [super initWithStyle:UITableViewStylePlain];
   if (self) {
     _delegate = modalDelegate;
+    _metricsRecorder = [[InfobarMetricsRecorder alloc]
+        initWithType:InfobarType::kInfobarTypeSaveAutofillAddressProfile];
   }
   return self;
 }
@@ -119,13 +128,14 @@
     [model addItem:item toSectionWithIdentifier:SectionIdentifierFields];
   }
 
+  [model addSectionWithIdentifier:SectionIdentifierButton];
   TableViewTextButtonItem* saveButton =
       [[TableViewTextButtonItem alloc] initWithType:ItemTypeSaveButton];
   saveButton.textAlignment = NSTextAlignmentNatural;
   // TODO(crbug.com/1167062): Replace with proper localized string.
   saveButton.buttonText = @"Test Save";
   saveButton.disableButtonIntrinsicWidth = YES;
-  [model addItem:saveButton toSectionWithIdentifier:SectionIdentifierFields];
+  [model addItem:saveButton toSectionWithIdentifier:SectionIdentifierButton];
 }
 
 #pragma mark - UITableViewDataSource
@@ -176,11 +186,35 @@
 #pragma mark - Actions
 
 - (void)handleCancelButton {
-  // TODO(crbug.com/1167062):  Implement the functionality.
+  base::RecordAction(
+      base::UserMetricsAction("MobileMessagesModalCancelledTapped"));
+  [self.metricsRecorder recordModalEvent:MobileMessagesModalEvent::Canceled];
+  [self.delegate dismissInfobarModal:self];
 }
 
 - (void)didTapSaveButton {
-  // TODO(crbug.com/1167062):  Implement the functionality.
+  base::RecordAction(
+      base::UserMetricsAction("MobileMessagesModalAcceptedTapped"));
+  [self.metricsRecorder recordModalEvent:MobileMessagesModalEvent::Accepted];
+  [self updateProfileData];
+  [self.delegate saveEditedProfileWithData:self.profileData];
+}
+
+#pragma mark - Private
+
+- (void)updateProfileData {
+  TableViewModel* model = self.tableViewModel;
+  NSInteger section =
+      [model sectionForSectionIdentifier:SectionIdentifierFields];
+  NSInteger itemCount = [model numberOfItemsInSection:section];
+  for (NSInteger itemIndex = 0; itemIndex < itemCount; ++itemIndex) {
+    NSIndexPath* path = [NSIndexPath indexPathForItem:itemIndex
+                                            inSection:section];
+    AutofillEditItem* item = base::mac::ObjCCastStrict<AutofillEditItem>(
+        [model itemAtIndexPath:path]);
+    self.profileData[[NSNumber numberWithInt:item.autofillUIType]] =
+        item.textFieldValue;
+  }
 }
 
 @end
diff --git a/ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_table_view_controller_unittest.mm b/ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_table_view_controller_unittest.mm
index 7026be47..6095497 100644
--- a/ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_table_view_controller_unittest.mm
+++ b/ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_table_view_controller_unittest.mm
@@ -38,6 +38,7 @@
   CreateController();
   CheckController();
 
-  EXPECT_EQ(1, NumberOfSections());
-  EXPECT_EQ(11, NumberOfItemsInSection(0));
+  EXPECT_EQ(2, NumberOfSections());
+  EXPECT_EQ(10, NumberOfItemsInSection(0));
+  EXPECT_EQ(1, NumberOfItemsInSection(1));
 }
diff --git a/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/BUILD.gn b/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/BUILD.gn
index 8aa4baac..1ce3b31 100644
--- a/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/BUILD.gn
+++ b/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/BUILD.gn
@@ -28,3 +28,35 @@
     "//ui/base",
   ]
 }
+
+source_set("unit_tests") {
+  testonly = true
+  sources =
+      [ "save_address_profile_infobar_modal_overlay_mediator_unittest.mm" ]
+
+  configs += [ "//build/config/compiler:enable_arc" ]
+
+  deps = [
+    ":autofill_address_profile",
+    "//base/test:test_support",
+    "//components/autofill/core/browser",
+    "//components/autofill/core/browser:test_support",
+    "//components/infobars/core",
+    "//ios/chrome/app/strings:ios_strings_grit",
+    "//ios/chrome/browser/infobars",
+    "//ios/chrome/browser/infobars/test",
+    "//ios/chrome/browser/overlays",
+    "//ios/chrome/browser/overlays/public/infobar_modal",
+    "//ios/chrome/browser/overlays/test",
+    "//ios/chrome/browser/ui/infobars:feature_flags",
+    "//ios/chrome/browser/ui/infobars/modals",
+    "//ios/chrome/browser/ui/infobars/modals/test",
+    "//ios/chrome/browser/ui/infobars/test",
+    "//ios/chrome/browser/ui/infobars/test",
+    "//ios/chrome/browser/ui/overlays/test",
+    "//testing/gmock",
+    "//testing/gtest",
+    "//third_party/ocmock",
+    "//ui/base",
+  ]
+}
diff --git a/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator.h b/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator.h
index ab4ba33..ce561a0 100644
--- a/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator.h
+++ b/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator.h
@@ -7,6 +7,7 @@
 
 #import "ios/chrome/browser/ui/overlays/infobar_modal/infobar_modal_overlay_mediator.h"
 
+#import "ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_modal_delegate.h"
 #import "ios/chrome/browser/ui/infobars/modals/infobar_save_address_profile_modal_delegate.h"
 #import "ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator_delegate.h"
 
@@ -15,7 +16,8 @@
 
 // Mediator that configures the modal UI for save address profile infobar.
 @interface SaveAddressProfileInfobarModalOverlayMediator
-    : InfobarModalOverlayMediator <InfobarSaveAddressProfileModalDelegate>
+    : InfobarModalOverlayMediator <InfobarSaveAddressProfileModalDelegate,
+                                   InfobarEditAddressProfileModalDelegate>
 
 // The consumer that is configured by this mediator.  Setting to a new value
 // configures the new consumer.
diff --git a/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator.mm b/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator.mm
index 040576a..e105fa9 100644
--- a/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator.mm
+++ b/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator.mm
@@ -6,6 +6,7 @@
 
 #include "base/strings/sys_string_conversions.h"
 #import "ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_request_config.h"
+#import "ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_responses.h"
 #import "ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_modal_consumer.h"
 #import "ios/chrome/browser/ui/infobars/modals/infobar_save_address_profile_modal_consumer.h"
 #import "ios/chrome/browser/ui/overlays/infobar_modal/infobar_modal_overlay_coordinator+modal_configuration.h"
@@ -18,6 +19,7 @@
 
 using autofill_address_profile_infobar_overlays::
     SaveAddressProfileModalRequestConfig;
+using save_address_profile_infobar_modal_responses::EditedProfileSaveAction;
 
 @interface SaveAddressProfileInfobarModalOverlayMediator ()
 // The save address profile modal config from the request.
@@ -90,4 +92,13 @@
   [self.saveAddressProfileMediatorDelegate showEditView];
 }
 
+#pragma mark - InfobarEditAddressProfileModalDelegate
+
+- (void)saveEditedProfileWithData:(NSDictionary*)profileData {
+  [self
+      dispatchResponse:OverlayResponse::CreateWithInfo<EditedProfileSaveAction>(
+                           profileData)];
+  [self dismissOverlay];
+}
+
 @end
diff --git a/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator_unittest.mm b/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator_unittest.mm
new file mode 100644
index 0000000..8b55889
--- /dev/null
+++ b/ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator_unittest.mm
@@ -0,0 +1,85 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile/save_address_profile_infobar_modal_overlay_mediator.h"
+
+#include "base/bind.h"
+#include "base/feature_list.h"
+#include "base/guid.h"
+#include "base/strings/sys_string_conversions.h"
+#include "components/autofill/core/browser/autofill_client.h"
+#include "components/autofill/core/browser/autofill_save_update_address_profile_delegate_ios.h"
+#include "components/autofill/core/browser/autofill_test_utils.h"
+#include "ios/chrome/browser/infobars/infobar_ios.h"
+#import "ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_request_config.h"
+#import "ios/chrome/browser/overlays/public/infobar_modal/save_address_profile_infobar_modal_overlay_responses.h"
+#include "ios/chrome/browser/overlays/test/fake_overlay_request_callback_installer.h"
+#import "ios/chrome/browser/ui/infobars/modals/infobar_edit_address_profile_modal_consumer.h"
+#import "ios/chrome/browser/ui/infobars/modals/infobar_save_address_profile_modal_consumer.h"
+#include "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+#include "third_party/ocmock/gtest_support.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+using autofill_address_profile_infobar_overlays::
+    SaveAddressProfileModalRequestConfig;
+using save_address_profile_infobar_modal_responses::EditedProfileSaveAction;
+
+// Test fixture for SaveAddressProfileInfobarModalOverlayMediator.
+class SaveAddressProfileInfobarModalOverlayMediatorTest : public PlatformTest {
+ public:
+  SaveAddressProfileInfobarModalOverlayMediatorTest()
+      : callback_installer_(&callback_receiver_,
+                            {EditedProfileSaveAction::ResponseSupport()}),
+        mediator_delegate_(
+            OCMStrictProtocolMock(@protocol(OverlayRequestMediatorDelegate))) {
+    autofill::AutofillProfile profile = autofill::test::GetFullProfile();
+    std::unique_ptr<autofill::AutofillSaveUpdateAddressProfileDelegateIOS>
+        delegate = std::make_unique<
+            autofill::AutofillSaveUpdateAddressProfileDelegateIOS>(
+            profile, /*original_profile=*/nullptr, /*locale=*/"en-US",
+            base::DoNothing());
+    delegate_ = delegate.get();
+    infobar_ = std::make_unique<InfoBarIOS>(
+        InfobarType::kInfobarTypeSaveAutofillAddressProfile,
+        std::move(delegate));
+
+    request_ =
+        OverlayRequest::CreateWithConfig<SaveAddressProfileModalRequestConfig>(
+            infobar_.get());
+    callback_installer_.InstallCallbacks(request_.get());
+
+    mediator_ = [[SaveAddressProfileInfobarModalOverlayMediator alloc]
+        initWithRequest:request_.get()];
+    mediator_.delegate = mediator_delegate_;
+  }
+
+  ~SaveAddressProfileInfobarModalOverlayMediatorTest() override {
+    EXPECT_CALL(callback_receiver_, CompletionCallback(request_.get()));
+    EXPECT_OCMOCK_VERIFY(mediator_delegate_);
+  }
+
+ protected:
+  autofill::AutofillSaveUpdateAddressProfileDelegateIOS* delegate_;
+  std::unique_ptr<InfoBarIOS> infobar_;
+  MockOverlayRequestCallbackReceiver callback_receiver_;
+  FakeOverlayRequestCallbackInstaller callback_installer_;
+  std::unique_ptr<OverlayRequest> request_;
+  SaveAddressProfileInfobarModalOverlayMediator* mediator_ = nil;
+  id<OverlayRequestMediatorDelegate> mediator_delegate_ = nil;
+};
+
+// Tests that calling saveEditedProfileWithData: triggers a
+// EditedProfileSaveAction response.
+TEST_F(SaveAddressProfileInfobarModalOverlayMediatorTest, EditAction) {
+  EXPECT_CALL(callback_receiver_,
+              DispatchCallback(request_.get(),
+                               EditedProfileSaveAction::ResponseSupport()));
+  OCMExpect([mediator_delegate_ stopOverlayForMediator:mediator_]);
+  [mediator_ saveEditedProfileWithData:@{}.mutableCopy];
+}
diff --git a/ios/chrome/browser/ui/recent_tabs/synced_sessions_bridge.h b/ios/chrome/browser/ui/recent_tabs/synced_sessions_bridge.h
index 613aa03..6d1b587 100644
--- a/ios/chrome/browser/ui/recent_tabs/synced_sessions_bridge.h
+++ b/ios/chrome/browser/ui/recent_tabs/synced_sessions_bridge.h
@@ -10,7 +10,7 @@
 
 #include "base/callback_list.h"
 #include "base/macros.h"
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #include "components/signin/public/identity_manager/identity_manager.h"
 
 class ChromeBrowserState;
@@ -45,8 +45,9 @@
 
   __weak id<SyncedSessionsObserver> owner_ = nil;
   signin::IdentityManager* identity_manager_ = nullptr;
-  ScopedObserver<signin::IdentityManager, signin::IdentityManager::Observer>
-      identity_manager_observer_;
+  base::ScopedObservation<signin::IdentityManager,
+                          signin::IdentityManager::Observer>
+      identity_manager_observation_{this};
   base::CallbackListSubscription foreign_session_updated_subscription_;
 
   DISALLOW_COPY_AND_ASSIGN(SyncedSessionsObserverBridge);
diff --git a/ios/chrome/browser/ui/recent_tabs/synced_sessions_bridge.mm b/ios/chrome/browser/ui/recent_tabs/synced_sessions_bridge.mm
index 1893f124..8fe73cf 100644
--- a/ios/chrome/browser/ui/recent_tabs/synced_sessions_bridge.mm
+++ b/ios/chrome/browser/ui/recent_tabs/synced_sessions_bridge.mm
@@ -24,9 +24,8 @@
     ChromeBrowserState* browserState)
     : owner_(owner),
       identity_manager_(
-          IdentityManagerFactory::GetForBrowserState(browserState)),
-      identity_manager_observer_(this) {
-  identity_manager_observer_.Add(identity_manager_);
+          IdentityManagerFactory::GetForBrowserState(browserState)) {
+  identity_manager_observation_.Observe(identity_manager_);
 
   // base::Unretained() is safe below because the subscription itself is a class
   // member field and handles destruction well.
diff --git a/ios/chrome/browser/ui/settings/utils/content_setting_backed_boolean.mm b/ios/chrome/browser/ui/settings/utils/content_setting_backed_boolean.mm
index a0965f6..99da18f6 100644
--- a/ios/chrome/browser/ui/settings/utils/content_setting_backed_boolean.mm
+++ b/ios/chrome/browser/ui/settings/utils/content_setting_backed_boolean.mm
@@ -4,7 +4,7 @@
 
 #import "ios/chrome/browser/ui/settings/utils/content_setting_backed_boolean.h"
 
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 #include "components/content_settings/core/browser/content_settings_details.h"
 #include "components/content_settings/core/browser/content_settings_observer.h"
 #include "components/content_settings/core/browser/host_content_settings_map.h"
@@ -32,8 +32,9 @@
 
 namespace {
 
-typedef ScopedObserver<HostContentSettingsMap, content_settings::Observer>
-    ContentSettingsObserver;
+typedef base::ScopedObservation<HostContentSettingsMap,
+                                content_settings::Observer>
+    ContentSettingsObseration;
 
 class ContentSettingsObserverBridge : public content_settings::Observer {
  public:
@@ -84,7 +85,7 @@
   ContentSettingsType _settingID;
   scoped_refptr<HostContentSettingsMap> _settingsMap;
   std::unique_ptr<ContentSettingsObserverBridge> _adaptor;
-  std::unique_ptr<ContentSettingsObserver> _content_settings_observer;
+  std::unique_ptr<ContentSettingsObseration> _content_settings_observer;
 }
 
 @synthesize settingID = _settingID;
@@ -103,8 +104,8 @@
     // Listen for changes to the content setting.
     _adaptor.reset(new ContentSettingsObserverBridge(self));
     _content_settings_observer.reset(
-        new ContentSettingsObserver(_adaptor.get()));
-    _content_settings_observer->Add(settingsMap);
+        new ContentSettingsObseration(_adaptor.get()));
+    _content_settings_observer->Observe(settingsMap);
   }
   return self;
 }
diff --git a/ios/chrome/test/BUILD.gn b/ios/chrome/test/BUILD.gn
index 900f9695..a69e070 100644
--- a/ios/chrome/test/BUILD.gn
+++ b/ios/chrome/test/BUILD.gn
@@ -291,6 +291,7 @@
     "//ios/chrome/browser/ui/overlays/infobar_banner/save_card:unit_tests",
     "//ios/chrome/browser/ui/overlays/infobar_banner/translate:unit_tests",
     "//ios/chrome/browser/ui/overlays/infobar_modal:unit_tests",
+    "//ios/chrome/browser/ui/overlays/infobar_modal/autofill_address_profile:unit_tests",
     "//ios/chrome/browser/ui/overlays/infobar_modal/passwords:unit_tests",
     "//ios/chrome/browser/ui/overlays/infobar_modal/save_card:unit_tests",
     "//ios/chrome/browser/ui/overlays/infobar_modal/translate:unit_tests",
diff --git a/ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper.mm b/ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper.mm
index 87a64590..ddccdd0 100644
--- a/ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper.mm
+++ b/ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper.mm
@@ -115,7 +115,7 @@
         return false;
       });
   if (!GetMatchingDomain(navigated_domain, engaged_sites, in_target_allowlist,
-                         &matched_domain, &match_type)) {
+                         proto, &matched_domain, &match_type)) {
     if (base::FeatureList::IsEnabled(
             lookalikes::features::kLookalikeInterstitialForPunycode) &&
         ShouldBlockBySpoofCheckResult(navigated_domain)) {
diff --git a/ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper_unittest.mm b/ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper_unittest.mm
index 9c91f3c..dc109d61 100644
--- a/ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper_unittest.mm
+++ b/ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper_unittest.mm
@@ -101,7 +101,7 @@
 TEST_F(LookalikeUrlTabHelperTest, ShouldAllowResponseForAllowlistedDomains) {
   GURL lookalike_url("https://xn--googl-fsa.com/");
   reputation::InitializeSafetyTipConfig();
-  reputation::SetSafetyTipAllowlistPatterns({"xn--googl-fsa.com/"}, {});
+  reputation::SetSafetyTipAllowlistPatterns({"xn--googl-fsa.com/"}, {}, {});
 
   EXPECT_TRUE(ShouldAllowResponseUrl(lookalike_url, /*main_frame=*/true)
                   .ShouldAllowNavigation());
diff --git a/ios/public/provider/chrome/browser/discover_feed/discover_feed_observer_bridge.h b/ios/public/provider/chrome/browser/discover_feed/discover_feed_observer_bridge.h
index 5cda2ade..712bdef9 100644
--- a/ios/public/provider/chrome/browser/discover_feed/discover_feed_observer_bridge.h
+++ b/ios/public/provider/chrome/browser/discover_feed/discover_feed_observer_bridge.h
@@ -9,7 +9,7 @@
 
 #import "ios/public/provider/chrome/browser/discover_feed/discover_feed_provider.h"
 
-#include "base/scoped_observer.h"
+#include "base/scoped_observation.h"
 
 // Implement this protocol and pass your implementation into an
 // DiscoveFeedObserverBridge object to receive DiscoverFeed observer
@@ -43,8 +43,8 @@
   void OnDiscoverFeedModelRecreated() override;
 
   __weak id<DiscoverFeedObserverBridgeDelegate> observer_;
-  ScopedObserver<DiscoverFeedProvider, DiscoverFeedProvider::Observer>
-      scoped_observer_{this};
+  base::ScopedObservation<DiscoverFeedProvider, DiscoverFeedProvider::Observer>
+      scoped_observation_{this};
 };
 
 #endif  // IOS_PUBLIC_PROVIDER_CHROME_BROWSER_DISCOVER_FEED_DISCOVER_FEED_OBSERVER_BRIDGE_H_
diff --git a/ios/public/provider/chrome/browser/discover_feed/discover_feed_observer_bridge.mm b/ios/public/provider/chrome/browser/discover_feed/discover_feed_observer_bridge.mm
index 40011994..8fc34da2 100644
--- a/ios/public/provider/chrome/browser/discover_feed/discover_feed_observer_bridge.mm
+++ b/ios/public/provider/chrome/browser/discover_feed/discover_feed_observer_bridge.mm
@@ -13,7 +13,7 @@
 DiscoverFeedObserverBridge::DiscoverFeedObserverBridge(
     id<DiscoverFeedObserverBridgeDelegate> observer)
     : observer_(observer) {
-  scoped_observer_.Add(
+  scoped_observation_.Observe(
       ios::GetChromeBrowserProvider()->GetDiscoverFeedProvider());
 }
 
diff --git a/media/capture/video/fuchsia/video_capture_device_fuchsia.cc b/media/capture/video/fuchsia/video_capture_device_fuchsia.cc
index 4538182d..3315956 100644
--- a/media/capture/video/fuchsia/video_capture_device_fuchsia.cc
+++ b/media/capture/video/fuchsia/video_capture_device_fuchsia.cc
@@ -179,7 +179,6 @@
 
 void VideoCaptureDeviceFuchsia::DisconnectStream() {
   stream_.Unbind();
-  buffer_collection_creator_.reset();
   buffer_collection_.reset();
   buffers_.clear();
   frame_size_.reset();
@@ -251,40 +250,24 @@
   // Initialize the new collection.
   fuchsia::sysmem::BufferCollectionTokenPtr token;
   token.Bind(std::move(token_handle));
-  buffer_collection_creator_ =
-      sysmem_allocator_.MakeBufferPoolCreatorFromToken(std::move(token));
 
   // Request just one buffer in collection constraints: each frame is copied as
   // soon as it's received.
   const size_t kMaxUsedOutputFrames = 1;
 
+  // This is not an actual device driver, so the priority should be > 1. It's
+  // also not a high-level system, so the name should be < 100.
+  constexpr uint32_t kNamePriority = 10;
+
   // Sysmem calculates buffer size based on image constraints, so it doesn't
   // need to be specified explicitly.
   fuchsia::sysmem::BufferCollectionConstraints constraints =
       VmoBuffer::GetRecommendedConstraints(kMaxUsedOutputFrames,
                                            /*min_buffer_size=*/absl::nullopt,
                                            /*writable=*/false);
-  // This is not an actual device driver, so the priority should be > 1. It's
-  // also not a high-level system, so the name should be < 100.
-  constexpr uint32_t kNamePriority = 10;
-  buffer_collection_creator_->SetName(kNamePriority,
-                                      "CrVideoCaptureDeviceFuchsia");
-  buffer_collection_creator_->Create(
-      std::move(constraints),
-      base::BindOnce(&VideoCaptureDeviceFuchsia::OnBufferCollectionCreated,
-                     base::Unretained(this)));
-}
-
-void VideoCaptureDeviceFuchsia::OnBufferCollectionCreated(
-    std::unique_ptr<SysmemBufferPool> collection) {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-
-  // Buffer collection allocation has failed. This case is not treated as an
-  // error because the camera may create a new collection.
-  if (!collection)
-    return;
-
-  buffer_collection_ = std::move(collection);
+  buffer_collection_ = sysmem_allocator_.BindSharedCollection(std::move(token));
+  buffer_collection_->Initialize(std::move(constraints), "CrVideoCaptureDevice",
+                                 kNamePriority);
   buffer_collection_->AcquireBuffers(base::BindOnce(
       &VideoCaptureDeviceFuchsia::OnBuffersAcquired, base::Unretained(this)));
 }
diff --git a/media/capture/video/fuchsia/video_capture_device_fuchsia.h b/media/capture/video/fuchsia/video_capture_device_fuchsia.h
index 76d7bb21..2df3beef 100644
--- a/media/capture/video/fuchsia/video_capture_device_fuchsia.h
+++ b/media/capture/video/fuchsia/video_capture_device_fuchsia.h
@@ -12,7 +12,7 @@
 #include "base/threading/thread_checker.h"
 #include "base/time/time.h"
 #include "media/capture/video/video_capture_device.h"
-#include "media/fuchsia/common/sysmem_buffer_pool.h"
+#include "media/fuchsia/common/sysmem_client.h"
 #include "media/fuchsia/common/vmo_buffer.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
@@ -77,10 +77,7 @@
       fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken>
           token_handle);
 
-  // Callback for SysmemBufferPool::Creator.
-  void OnBufferCollectionCreated(std::unique_ptr<SysmemBufferPool> collection);
-
-  // Callback for SysmemBufferPool::AcquireBuffers().
+  // Callback for SysmemCollectionClient::AcquireBuffers().
   void OnBuffersAcquired(
       std::vector<VmoBuffer> buffers,
       const fuchsia::sysmem::SingleBufferSettings& buffer_settings);
@@ -97,9 +94,8 @@
 
   std::unique_ptr<Client> client_;
 
-  media::BufferAllocator sysmem_allocator_;
-  std::unique_ptr<SysmemBufferPool::Creator> buffer_collection_creator_;
-  std::unique_ptr<SysmemBufferPool> buffer_collection_;
+  SysmemAllocatorClient sysmem_allocator_;
+  std::unique_ptr<SysmemCollectionClient> buffer_collection_;
   std::vector<VmoBuffer> buffers_;
   fuchsia::sysmem::ImageFormatConstraints buffers_format_;
 
diff --git a/media/filters/fuchsia/fuchsia_video_decoder.cc b/media/filters/fuchsia/fuchsia_video_decoder.cc
index 72bdfe7..44ad52ac 100644
--- a/media/filters/fuchsia/fuchsia_video_decoder.cc
+++ b/media/filters/fuchsia/fuchsia_video_decoder.cc
@@ -41,7 +41,7 @@
 #include "media/fuchsia/cdm/fuchsia_decryptor.h"
 #include "media/fuchsia/cdm/fuchsia_stream_decryptor.h"
 #include "media/fuchsia/common/stream_processor_helper.h"
-#include "media/fuchsia/common/sysmem_buffer_pool.h"
+#include "media/fuchsia/common/sysmem_client.h"
 #include "media/fuchsia/common/vmo_buffer_writer_queue.h"
 #include "third_party/libyuv/include/libyuv/video_common.h"
 #include "ui/gfx/buffer_types.h"
@@ -243,11 +243,12 @@
   // Called on errors to shutdown the decoder and notify the client.
   void OnError();
 
-  // Callback for |input_buffer_collection_creator_->Create()|.
-  void OnInputBufferPoolCreated(std::unique_ptr<SysmemBufferPool> pool);
+  // Callback for |input_buffer_collection_->GetSharedToken()|.
+  void SetInputBufferCollection(
+      fuchsia::sysmem::BufferCollectionTokenPtr token);
 
-  // Callback for |input_buffer_collection_->CreateWriter()|.
-  void OnBuffersAcquired(
+  // Callback for |input_buffer_collection_->AcquireBuffers()|.
+  void OnInputBuffersAcquired(
       std::vector<VmoBuffer> buffers,
       const fuchsia::sysmem::SingleBufferSettings& buffer_settings);
 
@@ -292,7 +293,7 @@
   absl::optional<fuchsia::media::StreamBufferConstraints>
       decoder_input_constraints_;
 
-  BufferAllocator sysmem_allocator_;
+  SysmemAllocatorClient sysmem_allocator_;
   std::unique_ptr<gfx::ClientNativePixmapFactory> client_native_pixmap_factory_;
 
   uint64_t stream_lifetime_ordinal_ = 1;
@@ -310,8 +311,7 @@
 
   // Input buffers for |decoder_|.
   uint64_t input_buffer_lifetime_ordinal_ = 1;
-  std::unique_ptr<SysmemBufferPool::Creator> input_buffer_collection_creator_;
-  std::unique_ptr<SysmemBufferPool> input_buffer_collection_;
+  std::unique_ptr<SysmemCollectionClient> input_buffer_collection_;
   base::flat_map<size_t, InputDecoderPacket> in_flight_input_packets_;
 
   // Output buffers for |decoder_|.
@@ -590,14 +590,20 @@
 
   ReleaseInputBuffers();
 
-  // Create buffer constrains for the input buffer collection.
-  size_t num_tokens;
-  fuchsia::sysmem::BufferCollectionConstraints buffer_constraints;
+  input_buffer_collection_ = sysmem_allocator_.AllocateNewCollection();
+
+  input_buffer_collection_->CreateSharedToken(base::BindOnce(
+      &FuchsiaVideoDecoder::SetInputBufferCollection, base::Unretained(this)));
 
   if (decryptor_) {
-    // For encrypted streams the sysmem buffer collection is used for decryptor
-    // output and decoder input. It is not used directly.
-    num_tokens = 2;
+    input_buffer_collection_->CreateSharedToken(base::BindOnce(
+        &FuchsiaSecureStreamDecryptor::SetOutputBufferCollectionToken,
+        base::Unretained(decryptor_.get())));
+  }
+
+  // Create buffer constrains for the input buffer collection.
+  fuchsia::sysmem::BufferCollectionConstraints buffer_constraints;
+  if (decryptor_) {
     buffer_constraints.usage.none = fuchsia::sysmem::noneUsage;
     buffer_constraints.min_buffer_count = kNumInputBuffers;
     buffer_constraints.has_buffer_memory_constraints = true;
@@ -608,46 +614,30 @@
     buffer_constraints.buffer_memory_constraints.inaccessible_domain_supported =
         true;
   } else {
-    num_tokens = 1;
     buffer_constraints = VmoBuffer::GetRecommendedConstraints(
         kNumInputBuffers, kInputBufferSize, /*writable=*/true);
   }
 
-  input_buffer_collection_creator_ =
-      sysmem_allocator_.MakeBufferPoolCreator(num_tokens);
-  input_buffer_collection_creator_->Create(
-      std::move(buffer_constraints),
-      base::BindOnce(&FuchsiaVideoDecoder::OnInputBufferPoolCreated,
-                     base::Unretained(this)));
+  input_buffer_collection_->Initialize(std::move(buffer_constraints),
+                                       "CrVideoDecoderInput");
+
+  if (!decryptor_) {
+    input_buffer_collection_->AcquireBuffers(base::BindOnce(
+        &FuchsiaVideoDecoder::OnInputBuffersAcquired, base::Unretained(this)));
+  }
 }
 
-void FuchsiaVideoDecoder::OnInputBufferPoolCreated(
-    std::unique_ptr<SysmemBufferPool> pool) {
-  if (!pool) {
-    DLOG(ERROR) << "Fail to allocate input buffers for the codec.";
-    OnError();
-    return;
-  }
-
-  input_buffer_collection_ = std::move(pool);
-
+void FuchsiaVideoDecoder::SetInputBufferCollection(
+    fuchsia::sysmem::BufferCollectionTokenPtr token) {
   fuchsia::media::StreamBufferPartialSettings settings;
   settings.set_buffer_lifetime_ordinal(input_buffer_lifetime_ordinal_);
   settings.set_buffer_constraints_version_ordinal(
       decoder_input_constraints_->buffer_constraints_version_ordinal());
-  settings.set_sysmem_token(input_buffer_collection_->TakeToken());
+  settings.set_sysmem_token(std::move(token));
   decoder_->SetInputBufferPartialSettings(std::move(settings));
-
-  if (decryptor_) {
-    decryptor_->SetOutputBufferCollectionToken(
-        input_buffer_collection_->TakeToken());
-  } else {
-    input_buffer_collection_->AcquireBuffers(base::BindOnce(
-        &FuchsiaVideoDecoder::OnBuffersAcquired, base::Unretained(this)));
-  }
 }
 
-void FuchsiaVideoDecoder::OnBuffersAcquired(
+void FuchsiaVideoDecoder::OnInputBuffersAcquired(
     std::vector<VmoBuffer> buffers,
     const fuchsia::sysmem::SingleBufferSettings& buffer_settings) {
   if (buffers.empty()) {
@@ -1056,7 +1046,6 @@
 
 void FuchsiaVideoDecoder::ReleaseInputBuffers() {
   input_writer_queue_.ResetBuffers();
-  input_buffer_collection_creator_.reset();
   input_buffer_collection_.reset();
 
   // |in_flight_input_packets_| must be destroyed after
diff --git a/media/filters/stream_parser_factory.cc b/media/filters/stream_parser_factory.cc
index 58a60ad..098ebd4 100644
--- a/media/filters/stream_parser_factory.cc
+++ b/media/filters/stream_parser_factory.cc
@@ -488,6 +488,18 @@
         bool found_codec = false;
         std::string codec_id = codecs[j];
         for (int k = 0; type_info.codecs[k]; ++k) {
+          // Only check a codec pattern if there is one to check. Some types,
+          // like audio/mpeg and audio/aac require there be no codecs parameter,
+          // and instead have implicit codec. If a codec is provided for such a
+          // type then it is not supported by MSE. We don't check any other
+          // potential matches because none should be configured.
+          if (!type_info.codecs[k]->pattern) {
+            DCHECK(k == 0 && !type_info.codecs[1])
+                << "For a type with implicit codec, then only one codec must "
+                   "be configured";
+            break;
+          }
+
           if (base::MatchPattern(codec_id, type_info.codecs[k]->pattern) &&
               (!type_info.codecs[k]->validator ||
                type_info.codecs[k]->validator(codec_id, media_log))) {
diff --git a/media/fuchsia/cdm/fuchsia_stream_decryptor.cc b/media/fuchsia/cdm/fuchsia_stream_decryptor.cc
index 26f5498..586d22ac 100644
--- a/media/fuchsia/cdm/fuchsia_stream_decryptor.cc
+++ b/media/fuchsia/cdm/fuchsia_stream_decryptor.cc
@@ -131,15 +131,16 @@
     const fuchsia::media::StreamBufferConstraints& stream_constraints) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
-  auto buffer_constraints = VmoBuffer::GetRecommendedConstraints(
-      kMinBufferCount, min_buffer_size_, /*writable=*/true);
-
-  input_pool_creator_ =
-      allocator_.MakeBufferPoolCreator(/*num_shared_token=*/1);
-
-  input_pool_creator_->Create(
-      std::move(buffer_constraints),
-      base::BindOnce(&FuchsiaStreamDecryptorBase::OnInputBufferPoolCreated,
+  input_buffer_collection_ = allocator_.AllocateNewCollection();
+  input_buffer_collection_->CreateSharedToken(
+      base::BindOnce(&StreamProcessorHelper::CompleteInputBuffersAllocation,
+                     base::Unretained(&processor_)));
+  input_buffer_collection_->Initialize(
+      VmoBuffer::GetRecommendedConstraints(kMinBufferCount, min_buffer_size_,
+                                           /*writable=*/true),
+      "CrStreamDecryptorInput");
+  input_buffer_collection_->AcquireBuffers(
+      base::BindOnce(&FuchsiaStreamDecryptorBase::OnInputBuffersAcquired,
                      base::Unretained(this)));
 }
 
@@ -148,27 +149,6 @@
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 }
 
-void FuchsiaStreamDecryptorBase::OnInputBufferPoolCreated(
-    std::unique_ptr<SysmemBufferPool> pool) {
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-
-  if (!pool) {
-    DLOG(ERROR) << "Fail to allocate input buffer.";
-    OnError();
-    return;
-  }
-
-  input_pool_ = std::move(pool);
-
-  // Provide token before enabling writer. Tokens must be provided to
-  // StreamProcessor before getting the allocated buffers.
-  processor_.CompleteInputBuffersAllocation(input_pool_->TakeToken());
-
-  input_pool_->AcquireBuffers(
-      base::BindOnce(&FuchsiaStreamDecryptorBase::OnInputBuffersAcquired,
-                     base::Unretained(this)));
-}
-
 void FuchsiaStreamDecryptorBase::OnInputBuffersAcquired(
     std::vector<VmoBuffer> buffers,
     const fuchsia::sysmem::SingleBufferSettings& buffer_settings) {
@@ -248,12 +228,16 @@
 void FuchsiaClearStreamDecryptor::AllocateOutputBuffers(
     const fuchsia::media::StreamBufferConstraints& stream_constraints) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  output_pool_creator_ =
-      allocator_.MakeBufferPoolCreator(1 /* num_shared_token */);
-  output_pool_creator_->Create(
+  output_buffer_collection_ = allocator_.AllocateNewCollection();
+  output_buffer_collection_->CreateSharedToken(
+      base::BindOnce(&StreamProcessorHelper::CompleteOutputBuffersAllocation,
+                     base::Unretained(&processor_)));
+  output_buffer_collection_->Initialize(
       VmoBuffer::GetRecommendedConstraints(kMinBufferCount, min_buffer_size_,
                                            /*writable=*/false),
-      base::BindOnce(&FuchsiaClearStreamDecryptor::OnOutputBufferPoolCreated,
+      "CrFuchsiaStreamDecryptor");
+  output_buffer_collection_->AcquireBuffers(
+      base::BindOnce(&FuchsiaClearStreamDecryptor::OnOutputBuffersAcquired,
                      base::Unretained(this)));
 }
 
@@ -269,12 +253,6 @@
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK(decrypt_cb_);
 
-  DCHECK(output_buffers_.empty());
-  if (!output_pool_->is_live()) {
-    DLOG(ERROR) << "Output buffer pool is dead.";
-    return;
-  }
-
   size_t buffer_index = packet.buffer_index();
   if (buffer_index >= output_buffers_.size()) {
     DLOG(ERROR) << "Received output packet with invalid buffer index: "
@@ -352,27 +330,6 @@
     std::move(decrypt_cb_).Run(Decryptor::kError, nullptr);
 }
 
-void FuchsiaClearStreamDecryptor::OnOutputBufferPoolCreated(
-    std::unique_ptr<SysmemBufferPool> pool) {
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-
-  if (!pool) {
-    LOG(ERROR) << "Fail to allocate output buffer.";
-    OnError();
-    return;
-  }
-
-  output_pool_ = std::move(pool);
-
-  // Provide token before enabling reader. Tokens must be provided to
-  // StreamProcessor before getting the allocated buffers.
-  processor_.CompleteOutputBuffersAllocation(output_pool_->TakeToken());
-
-  output_pool_->AcquireBuffers(
-      base::BindOnce(&FuchsiaClearStreamDecryptor::OnOutputBuffersAcquired,
-                     base::Unretained(this)));
-}
-
 void FuchsiaClearStreamDecryptor::OnOutputBuffersAcquired(
     std::vector<VmoBuffer> buffers,
     const fuchsia::sysmem::SingleBufferSettings& buffer_settings) {
diff --git a/media/fuchsia/cdm/fuchsia_stream_decryptor.h b/media/fuchsia/cdm/fuchsia_stream_decryptor.h
index 067b8212..377c69a 100644
--- a/media/fuchsia/cdm/fuchsia_stream_decryptor.h
+++ b/media/fuchsia/cdm/fuchsia_stream_decryptor.h
@@ -12,7 +12,7 @@
 #include "base/sequence_checker.h"
 #include "media/base/decryptor.h"
 #include "media/fuchsia/common/stream_processor_helper.h"
-#include "media/fuchsia/common/sysmem_buffer_pool.h"
+#include "media/fuchsia/common/sysmem_client.h"
 #include "media/fuchsia/common/vmo_buffer_writer_queue.h"
 
 namespace media {
@@ -39,7 +39,7 @@
 
   const size_t min_buffer_size_;
 
-  BufferAllocator allocator_;
+  SysmemAllocatorClient allocator_;
 
   VmoBufferWriterQueue input_writer_queue_;
 
@@ -49,7 +49,6 @@
   SEQUENCE_CHECKER(sequence_checker_);
 
  private:
-  void OnInputBufferPoolCreated(std::unique_ptr<SysmemBufferPool> pool);
   void OnInputBuffersAcquired(
       std::vector<VmoBuffer> buffers,
       const fuchsia::sysmem::SingleBufferSettings& buffer_settings);
@@ -57,8 +56,7 @@
                        StreamProcessorHelper::IoPacket packet);
   void ProcessEndOfStream();
 
-  std::unique_ptr<SysmemBufferPool::Creator> input_pool_creator_;
-  std::unique_ptr<SysmemBufferPool> input_pool_;
+  std::unique_ptr<SysmemCollectionClient> input_buffer_collection_;
 
   DISALLOW_COPY_AND_ASSIGN(FuchsiaStreamDecryptorBase);
 };
@@ -89,15 +87,13 @@
   void OnNoKey() final;
   void OnError() final;
 
-  void OnOutputBufferPoolCreated(std::unique_ptr<SysmemBufferPool> pool);
   void OnOutputBuffersAcquired(
       std::vector<VmoBuffer> buffers,
       const fuchsia::sysmem::SingleBufferSettings& buffer_settings);
 
   Decryptor::DecryptCB decrypt_cb_;
 
-  std::unique_ptr<SysmemBufferPool::Creator> output_pool_creator_;
-  std::unique_ptr<SysmemBufferPool> output_pool_;
+  std::unique_ptr<SysmemCollectionClient> output_buffer_collection_;
   std::vector<VmoBuffer> output_buffers_;
 
   // Used to re-assemble decrypted output that was split between multiple sysmem
diff --git a/media/fuchsia/common/BUILD.gn b/media/fuchsia/common/BUILD.gn
index bda259b..cd3f1c12 100644
--- a/media/fuchsia/common/BUILD.gn
+++ b/media/fuchsia/common/BUILD.gn
@@ -8,8 +8,8 @@
   sources = [
     "stream_processor_helper.cc",
     "stream_processor_helper.h",
-    "sysmem_buffer_pool.cc",
-    "sysmem_buffer_pool.h",
+    "sysmem_client.cc",
+    "sysmem_client.h",
     "vmo_buffer.cc",
     "vmo_buffer.h",
     "vmo_buffer_writer_queue.cc",
diff --git a/media/fuchsia/common/sysmem_buffer_pool.cc b/media/fuchsia/common/sysmem_buffer_pool.cc
deleted file mode 100644
index de58ebf..0000000
--- a/media/fuchsia/common/sysmem_buffer_pool.cc
+++ /dev/null
@@ -1,189 +0,0 @@
-// Copyright 2019 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "media/fuchsia/common/sysmem_buffer_pool.h"
-
-#include <zircon/rights.h>
-#include <algorithm>
-
-#include "base/bind.h"
-#include "base/fuchsia/fuchsia_logging.h"
-#include "base/fuchsia/process_context.h"
-#include "base/process/process_handle.h"
-#include "media/fuchsia/common/vmo_buffer.h"
-
-namespace media {
-
-SysmemBufferPool::Creator::Creator(
-    fuchsia::sysmem::BufferCollectionPtr collection,
-    std::vector<fuchsia::sysmem::BufferCollectionTokenPtr> shared_tokens)
-    : collection_(std::move(collection)),
-      shared_tokens_(std::move(shared_tokens)) {
-  collection_.set_error_handler(
-      [this](zx_status_t status) {
-        ZX_DLOG(ERROR, status)
-            << "Connection to BufferCollection was disconnected.";
-        collection_.Unbind();
-
-        if (create_cb_)
-          std::move(create_cb_).Run(nullptr);
-      });
-}
-
-SysmemBufferPool::Creator::~Creator() {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-}
-
-void SysmemBufferPool::Creator::SetName(uint32_t priority,
-                                        base::StringPiece name) {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  DCHECK(!create_cb_);
-  collection_->SetName(priority, std::string(name));
-}
-
-void SysmemBufferPool::Creator::Create(
-    fuchsia::sysmem::BufferCollectionConstraints constraints,
-    CreateCB create_cb) {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  DCHECK(!create_cb_);
-  create_cb_ = std::move(create_cb);
-  // BufferCollection needs to be synchronized to ensure that all token
-  // duplicate requests have been processed and sysmem knows about all clients
-  // that will be using this buffer collection.
-  collection_->Sync([this, constraints = std::move(constraints)]() mutable {
-    bool writable = constraints.usage.cpu & fuchsia::sysmem::cpuUsageWrite;
-
-    collection_->SetConstraints(true /* has constraints */,
-                                std::move(constraints));
-
-    DCHECK(create_cb_);
-    std::move(create_cb_)
-        .Run(std::make_unique<SysmemBufferPool>(
-            std::move(collection_), std::move(shared_tokens_), writable));
-  });
-}
-
-SysmemBufferPool::SysmemBufferPool(
-    fuchsia::sysmem::BufferCollectionPtr collection,
-    std::vector<fuchsia::sysmem::BufferCollectionTokenPtr> shared_tokens,
-    bool writable)
-    : collection_(std::move(collection)),
-      shared_tokens_(std::move(shared_tokens)),
-      writable_(writable) {
-  collection_.set_error_handler([this](zx_status_t status) {
-    ZX_LOG(ERROR, status) << "fuchsia.sysmem.BufferCollection disconnected.";
-    OnError();
-  });
-}
-
-SysmemBufferPool::~SysmemBufferPool() {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  if (collection_)
-    collection_->Close();
-}
-
-fuchsia::sysmem::BufferCollectionTokenPtr SysmemBufferPool::TakeToken() {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  DCHECK(!shared_tokens_.empty());
-  auto token = std::move(shared_tokens_.back());
-  shared_tokens_.pop_back();
-  return token;
-}
-
-void SysmemBufferPool::AcquireBuffers(AcquireBuffersCB cb) {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  DCHECK(!acquire_buffers_cb_);
-  acquire_buffers_cb_ = std::move(cb);
-  collection_->WaitForBuffersAllocated(
-      fit::bind_member(this, &SysmemBufferPool::OnBuffersAllocated));
-}
-
-void SysmemBufferPool::OnBuffersAllocated(
-    zx_status_t status,
-    fuchsia::sysmem::BufferCollectionInfo_2 buffer_collection_info) {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-
-  if (status != ZX_OK) {
-    ZX_LOG(ERROR, status) << "Fail to allocate sysmem buffers.";
-    OnError();
-    return;
-  }
-
-  if (acquire_buffers_cb_) {
-    auto buffers = VmoBuffer::CreateBuffersFromSysmemCollection(
-        &buffer_collection_info, writable_);
-
-    std::move(acquire_buffers_cb_)
-        .Run(std::move(buffers), buffer_collection_info.settings);
-  }
-}
-
-void SysmemBufferPool::OnError() {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  collection_.Unbind();
-  if (acquire_buffers_cb_)
-    std::move(acquire_buffers_cb_).Run({}, {});
-}
-
-BufferAllocator::BufferAllocator(base::StringPiece client_name) {
-  allocator_ = base::ComponentContextForProcess()
-                   ->svc()
-                   ->Connect<fuchsia::sysmem::Allocator>();
-
-  allocator_->SetDebugClientInfo(std::string(client_name),
-                                 base::GetCurrentProcId());
-
-  allocator_.set_error_handler([](zx_status_t status) {
-    // Just log a warning. We will handle BufferCollection the failure when
-    // trying to create a new BufferCollection.
-    ZX_DLOG(WARNING, status)
-        << "The fuchsia.sysmem.Allocator channel was disconnected.";
-  });
-}
-
-BufferAllocator::~BufferAllocator() = default;
-
-fuchsia::sysmem::BufferCollectionTokenPtr BufferAllocator::CreateNewToken() {
-  fuchsia::sysmem::BufferCollectionTokenPtr collection_token;
-  allocator_->AllocateSharedCollection(collection_token.NewRequest());
-  return collection_token;
-}
-
-std::unique_ptr<SysmemBufferPool::Creator>
-BufferAllocator::MakeBufferPoolCreator(size_t num_of_tokens) {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-
-  // Create a new sysmem buffer collection token for the allocated buffers.
-  fuchsia::sysmem::BufferCollectionTokenPtr collection_token = CreateNewToken();
-
-  // Create collection token for sharing with other components.
-  std::vector<fuchsia::sysmem::BufferCollectionTokenPtr> shared_tokens;
-  for (size_t i = 0; i < num_of_tokens; i++) {
-    fuchsia::sysmem::BufferCollectionTokenPtr token;
-    collection_token->Duplicate(ZX_RIGHT_SAME_RIGHTS, token.NewRequest());
-    shared_tokens.push_back(std::move(token));
-  }
-
-  fuchsia::sysmem::BufferCollectionPtr buffer_collection;
-
-  // Convert the token to a BufferCollection connection.
-  allocator_->BindSharedCollection(std::move(collection_token),
-                                   buffer_collection.NewRequest());
-
-  return std::make_unique<SysmemBufferPool::Creator>(
-      std::move(buffer_collection), std::move(shared_tokens));
-}
-
-std::unique_ptr<SysmemBufferPool::Creator>
-BufferAllocator::MakeBufferPoolCreatorFromToken(
-    fuchsia::sysmem::BufferCollectionTokenPtr token) {
-  fuchsia::sysmem::BufferCollectionPtr buffer_collection;
-  allocator_->BindSharedCollection(std::move(token),
-                                   buffer_collection.NewRequest());
-  return std::make_unique<SysmemBufferPool::Creator>(
-      std::move(buffer_collection),
-      std::vector<fuchsia::sysmem::BufferCollectionTokenPtr>{});
-}
-
-}  // namespace media
diff --git a/media/fuchsia/common/sysmem_buffer_pool.h b/media/fuchsia/common/sysmem_buffer_pool.h
deleted file mode 100644
index 73618c4f..0000000
--- a/media/fuchsia/common/sysmem_buffer_pool.h
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright 2019 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef MEDIA_FUCHSIA_COMMON_SYSMEM_BUFFER_POOL_H_
-#define MEDIA_FUCHSIA_COMMON_SYSMEM_BUFFER_POOL_H_
-
-#include <fuchsia/media/cpp/fidl.h>
-#include <fuchsia/sysmem/cpp/fidl.h>
-#include <lib/sys/cpp/component_context.h>
-
-#include <list>
-#include <vector>
-
-#include "base/callback.h"
-#include "base/containers/span.h"
-#include "base/macros.h"
-#include "base/threading/thread_checker.h"
-
-namespace media {
-
-class VmoBuffer;
-
-// Pool of buffers allocated by sysmem. It owns BufferCollection. It doesn't
-// provide any function read/write the buffers. Call should use
-// ReadableBufferPool/WritableBufferPool for read/write.
-class SysmemBufferPool {
- public:
-  // Callback for AcquireBuffers(). Called with an empty |buffers| if buffers
-  // allocation failed.
-  using AcquireBuffersCB = base::OnceCallback<void(
-      std::vector<VmoBuffer> buffers,
-      const fuchsia::sysmem::SingleBufferSettings& settings)>;
-
-  // Creates SysmemBufferPool asynchronously. It also owns the channel to
-  // fuchsia services.
-  class Creator {
-   public:
-    using CreateCB =
-        base::OnceCallback<void(std::unique_ptr<SysmemBufferPool>)>;
-    Creator(
-        fuchsia::sysmem::BufferCollectionPtr collection,
-        std::vector<fuchsia::sysmem::BufferCollectionTokenPtr> shared_tokens);
-    ~Creator();
-
-    // Sets the name of the created buffers. Priority is a number used to choose
-    // which name to use when multiple clients set names. Must be called before
-    // Create. See fuchsia.sysmem/BufferCollection.SetName for a description of
-    // the arguments.
-    void SetName(uint32_t priority, base::StringPiece name);
-    void Create(fuchsia::sysmem::BufferCollectionConstraints constraints,
-                CreateCB build_cb);
-
-   private:
-    fuchsia::sysmem::BufferCollectionPtr collection_;
-    std::vector<fuchsia::sysmem::BufferCollectionTokenPtr> shared_tokens_;
-    CreateCB create_cb_;
-
-    THREAD_CHECKER(thread_checker_);
-
-    DISALLOW_COPY_AND_ASSIGN(Creator);
-  };
-
-  SysmemBufferPool(
-      fuchsia::sysmem::BufferCollectionPtr collection,
-      std::vector<fuchsia::sysmem::BufferCollectionTokenPtr> shared_tokens,
-      bool writable);
-  ~SysmemBufferPool();
-
-  fuchsia::sysmem::BufferCollectionTokenPtr TakeToken();
-
-  // Create VmoBuffers to access raw memory.
-  void AcquireBuffers(AcquireBuffersCB cb);
-
-  // Returns if this object is still usable. Caller must check this before
-  // calling SysmemBufferReader/Writer APIs.
-  bool is_live() const { return collection_.is_bound(); }
-
- private:
-  friend class BufferAllocator;
-
-  void OnBuffersAllocated(
-      zx_status_t status,
-      fuchsia::sysmem::BufferCollectionInfo_2 buffer_collection_info);
-  void OnError();
-
-  fuchsia::sysmem::BufferCollectionPtr collection_;
-  std::vector<fuchsia::sysmem::BufferCollectionTokenPtr> shared_tokens_;
-  const bool writable_;
-
-  AcquireBuffersCB acquire_buffers_cb_;
-
-  // FIDL interfaces are thread-affine (see crbug.com/1012875).
-  THREAD_CHECKER(thread_checker_);
-
-  DISALLOW_COPY_AND_ASSIGN(SysmemBufferPool);
-};
-
-// Wrapper of sysmem Allocator.
-class BufferAllocator {
- public:
-  explicit BufferAllocator(base::StringPiece client_name);
-  ~BufferAllocator();
-
-  fuchsia::sysmem::BufferCollectionTokenPtr CreateNewToken();
-
-  std::unique_ptr<SysmemBufferPool::Creator> MakeBufferPoolCreator(
-      size_t num_shared_token);
-
-  std::unique_ptr<SysmemBufferPool::Creator> MakeBufferPoolCreatorFromToken(
-      fuchsia::sysmem::BufferCollectionTokenPtr token);
-
-  // TODO(crbug.com/1131183): Update FuchsiaVideoDecoder to use SysmemBufferPool
-  // and remove this function.
-  fuchsia::sysmem::Allocator* raw() { return allocator_.get(); }
-
- private:
-  fuchsia::sysmem::AllocatorPtr allocator_;
-
-  THREAD_CHECKER(thread_checker_);
-
-  DISALLOW_COPY_AND_ASSIGN(BufferAllocator);
-};
-
-}  // namespace media
-
-#endif  // MEDIA_FUCHSIA_COMMON_SYSMEM_BUFFER_POOL_H_
diff --git a/media/fuchsia/common/sysmem_client.cc b/media/fuchsia/common/sysmem_client.cc
new file mode 100644
index 0000000..f2a198df
--- /dev/null
+++ b/media/fuchsia/common/sysmem_client.cc
@@ -0,0 +1,163 @@
+// 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 "media/fuchsia/common/sysmem_client.h"
+
+#include <lib/sys/cpp/component_context.h>
+#include <zircon/rights.h>
+
+#include <algorithm>
+
+#include "base/bind.h"
+#include "base/fuchsia/fuchsia_logging.h"
+#include "base/fuchsia/process_context.h"
+#include "base/process/process_handle.h"
+#include "media/fuchsia/common/vmo_buffer.h"
+
+namespace media {
+
+SysmemCollectionClient::SysmemCollectionClient(
+    fuchsia::sysmem::Allocator* allocator,
+    fuchsia::sysmem::BufferCollectionTokenPtr collection_token)
+    : allocator_(allocator), collection_token_(std::move(collection_token)) {
+  DCHECK(allocator_);
+  DCHECK(collection_token_);
+}
+
+SysmemCollectionClient::~SysmemCollectionClient() {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+}
+
+void SysmemCollectionClient::Initialize(
+    fuchsia::sysmem::BufferCollectionConstraints constraints,
+    base::StringPiece name,
+    uint32_t name_priority) {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+
+  writable_ = (constraints.usage.cpu & fuchsia::sysmem::cpuUsageWrite) ==
+              fuchsia::sysmem::cpuUsageWrite;
+
+  allocator_->BindSharedCollection(std::move(collection_token_),
+                                   collection_.NewRequest());
+
+  collection_.set_error_handler(
+      fit::bind_member(this, &SysmemCollectionClient::OnError));
+  collection_->SetName(name_priority, std::string(name));
+
+  // If Sync() is not required then constraints can be set immediately.
+  if (sync_completion_closures_.empty()) {
+    collection_->SetConstraints(/*have_constraints=*/true,
+                                std::move(constraints));
+    return;
+  }
+
+  sync_completion_closures_.push_back(
+      base::BindOnce(&fuchsia::sysmem::BufferCollection::SetConstraints,
+                     base::Unretained(collection_.get()),
+                     /*have_constraints=*/true, std::move(constraints)));
+  collection_->Sync(
+      fit::bind_member(this, &SysmemCollectionClient::OnSyncComplete));
+}
+
+void SysmemCollectionClient::CreateSharedToken(GetSharedTokenCB cb) {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  DCHECK(collection_token_);
+
+  fuchsia::sysmem::BufferCollectionTokenPtr token;
+  collection_token_->Duplicate(ZX_RIGHT_SAME_RIGHTS, token.NewRequest());
+
+  sync_completion_closures_.push_back(
+      base::BindOnce(std::move(cb), std::move(token)));
+}
+
+void SysmemCollectionClient::AcquireBuffers(AcquireBuffersCB cb) {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  DCHECK(!collection_token_);
+
+  if (!collection_) {
+    std::move(cb).Run({}, {});
+    return;
+  }
+
+  acquire_buffers_cb_ = std::move(cb);
+  collection_->WaitForBuffersAllocated(
+      fit::bind_member(this, &SysmemCollectionClient::OnBuffersAllocated));
+}
+
+void SysmemCollectionClient::OnSyncComplete() {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+
+  std::vector<base::OnceClosure> sync_closures =
+      std::move(sync_completion_closures_);
+  for (auto& cb : sync_closures) {
+    std::move(cb).Run();
+  }
+}
+
+void SysmemCollectionClient::OnBuffersAllocated(
+    zx_status_t status,
+    fuchsia::sysmem::BufferCollectionInfo_2 buffer_collection_info) {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+
+  if (status != ZX_OK) {
+    ZX_LOG(ERROR, status) << "Failed to allocate sysmem buffers.";
+    OnError(status);
+    return;
+  }
+
+  if (acquire_buffers_cb_) {
+    auto buffers = VmoBuffer::CreateBuffersFromSysmemCollection(
+        &buffer_collection_info, writable_);
+    std::move(acquire_buffers_cb_)
+        .Run(std::move(buffers), buffer_collection_info.settings);
+  }
+}
+
+void SysmemCollectionClient::OnError(zx_status_t status) {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  ZX_DLOG(ERROR, status) << "Connection to BufferCollection was disconnected.";
+  collection_.Unbind();
+  if (acquire_buffers_cb_)
+    std::move(acquire_buffers_cb_).Run({}, {});
+}
+
+SysmemAllocatorClient::SysmemAllocatorClient(base::StringPiece client_name) {
+  allocator_ = base::ComponentContextForProcess()
+                   ->svc()
+                   ->Connect<fuchsia::sysmem::Allocator>();
+
+  allocator_->SetDebugClientInfo(std::string(client_name),
+                                 base::GetCurrentProcId());
+
+  allocator_.set_error_handler([](zx_status_t status) {
+    // Just log a warning. We will handle BufferCollection the failure when
+    // trying to create a new BufferCollection.
+    ZX_DLOG(WARNING, status)
+        << "The fuchsia.sysmem.Allocator channel was disconnected.";
+  });
+}
+
+SysmemAllocatorClient::~SysmemAllocatorClient() = default;
+
+fuchsia::sysmem::BufferCollectionTokenPtr
+SysmemAllocatorClient::CreateNewToken() {
+  fuchsia::sysmem::BufferCollectionTokenPtr collection_token;
+  allocator_->AllocateSharedCollection(collection_token.NewRequest());
+  return collection_token;
+}
+
+std::unique_ptr<SysmemCollectionClient>
+SysmemAllocatorClient::AllocateNewCollection() {
+  return std::make_unique<SysmemCollectionClient>(allocator_.get(),
+                                                  CreateNewToken());
+}
+
+std::unique_ptr<SysmemCollectionClient>
+SysmemAllocatorClient::BindSharedCollection(
+    fuchsia::sysmem::BufferCollectionTokenPtr token) {
+  return std::make_unique<SysmemCollectionClient>(allocator_.get(),
+                                                  std::move(token));
+}
+
+}  // namespace media
diff --git a/media/fuchsia/common/sysmem_client.h b/media/fuchsia/common/sysmem_client.h
new file mode 100644
index 0000000..acedf70
--- /dev/null
+++ b/media/fuchsia/common/sysmem_client.h
@@ -0,0 +1,110 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MEDIA_FUCHSIA_COMMON_SYSMEM_CLIENT_H_
+#define MEDIA_FUCHSIA_COMMON_SYSMEM_CLIENT_H_
+
+#include <fuchsia/media/cpp/fidl.h>
+#include <fuchsia/sysmem/cpp/fidl.h>
+
+#include <vector>
+
+#include "base/callback.h"
+#include "base/threading/thread_checker.h"
+
+namespace media {
+
+class VmoBuffer;
+
+// Wrapper for fuchsia.sysmem.BufferCollection . It provides the following two
+// features:
+//  1. Calls Sync() and ensures that it completes before buffer constrains are
+//  set and shared tokens are passed to other participants.
+//  2. Provides AcquireBuffers() that allows to acquire buffers and handle
+//  possible errors.
+class SysmemCollectionClient {
+ public:
+  static constexpr uint32_t kDefaultNamePriority = 100;
+
+  // Callback for GetSharedToken().
+  using GetSharedTokenCB =
+      base::OnceCallback<void(fuchsia::sysmem::BufferCollectionTokenPtr token)>;
+
+  // Callback for AcquireBuffers(). Called with an empty |buffers| if buffers
+  // allocation failed.
+  using AcquireBuffersCB = base::OnceCallback<void(
+      std::vector<VmoBuffer> buffers,
+      const fuchsia::sysmem::SingleBufferSettings& settings)>;
+
+  SysmemCollectionClient(
+      fuchsia::sysmem::Allocator* allocator,
+      fuchsia::sysmem::BufferCollectionTokenPtr collection_token);
+  ~SysmemCollectionClient();
+
+  SysmemCollectionClient(const SysmemCollectionClient&) = delete;
+  SysmemCollectionClient& operator=(const SysmemCollectionClient&) = delete;
+
+  // Creates one shared token to be shared with other participants and returns
+  // it asynchronously, when it's safe to pass it (i.e. after Sync()). Must be
+  // called before Initialize().
+  void CreateSharedToken(GetSharedTokenCB cb);
+
+  // Initializes the collection with the given name and constraints.
+  void Initialize(fuchsia::sysmem::BufferCollectionConstraints constraints,
+                  base::StringPiece name,
+                  uint32_t name_priority = kDefaultNamePriority);
+
+  // Create VmoBuffers to access raw memory. Should be called only after
+  // GetSharedToken() has been called for all shared tokens.
+  void AcquireBuffers(AcquireBuffersCB cb);
+
+ private:
+  void OnSyncComplete();
+  void OnBuffersAllocated(
+      zx_status_t status,
+      fuchsia::sysmem::BufferCollectionInfo_2 buffer_collection_info);
+  void OnError(zx_status_t status);
+
+  fuchsia::sysmem::Allocator* const allocator_;
+  fuchsia::sysmem::BufferCollectionTokenPtr collection_token_;
+  fuchsia::sysmem::BufferCollectionPtr collection_;
+
+  bool writable_ = false;
+  std::vector<base::OnceClosure> sync_completion_closures_;
+  AcquireBuffersCB acquire_buffers_cb_;
+
+  THREAD_CHECKER(thread_checker_);
+};
+
+// Helper fuchsia.sysmem.Allocator .
+class SysmemAllocatorClient {
+ public:
+  explicit SysmemAllocatorClient(base::StringPiece client_name);
+  ~SysmemAllocatorClient();
+
+  SysmemAllocatorClient(const SysmemAllocatorClient&) = delete;
+  SysmemAllocatorClient& operator=(const SysmemAllocatorClient&) = delete;
+
+  fuchsia::sysmem::BufferCollectionTokenPtr CreateNewToken();
+
+  // Creates new buffer collection.
+  std::unique_ptr<SysmemCollectionClient> AllocateNewCollection();
+
+  // Binds the specified token to a SysmemCollectionClient.
+  std::unique_ptr<SysmemCollectionClient> BindSharedCollection(
+      fuchsia::sysmem::BufferCollectionTokenPtr token);
+
+  // TODO(crbug.com/1131183): Update FuchsiaVideoDecoder to use
+  // SysmemCollectionClient and remove this function.
+  fuchsia::sysmem::Allocator* raw() { return allocator_.get(); }
+
+ private:
+  friend SysmemCollectionClient;
+
+  fuchsia::sysmem::AllocatorPtr allocator_;
+};
+
+}  // namespace media
+
+#endif  // MEDIA_FUCHSIA_COMMON_SYSMEM_CLIENT_H_
diff --git a/net/http/transport_security_persister.cc b/net/http/transport_security_persister.cc
index 7523178f..9603507 100644
--- a/net/http/transport_security_persister.cc
+++ b/net/http/transport_security_persister.cc
@@ -361,25 +361,25 @@
   return true;
 }
 
-bool TransportSecurityPersister::LoadEntries(const std::string& serialized) {
+void TransportSecurityPersister::LoadEntries(const std::string& serialized) {
   DCHECK(foreground_runner_->RunsTasksInCurrentSequence());
 
   transport_security_state_->ClearDynamicData();
-  return Deserialize(serialized, transport_security_state_);
+  Deserialize(serialized, transport_security_state_);
 }
 
-bool TransportSecurityPersister::Deserialize(const std::string& serialized,
+void TransportSecurityPersister::Deserialize(const std::string& serialized,
                                              TransportSecurityState* state) {
   absl::optional<base::Value> value = base::JSONReader::Read(serialized);
   if (!value || !value->is_dict())
-    return false;
+    return;
 
   absl::optional<int> version = value->FindIntKey(kVersionKey);
 
   // Stop if the data is out of date (or in the previous format that didn't have
   // a version number).
   if (!version || *version != kCurrentVersionValue)
-    return false;
+    return;
 
   base::Value* sts_value = value->FindKey(kSTSKey);
   if (sts_value)
@@ -392,7 +392,6 @@
   UMA_HISTOGRAM_CUSTOM_COUNTS("Net.ExpectCT.EntriesOnLoad",
                               state->num_expect_ct_entries(), 1 /* min */,
                               2000 /* max */, 40 /* buckets */);
-  return true;
 }
 
 void TransportSecurityPersister::CompleteLoad(const std::string& state) {
diff --git a/net/http/transport_security_persister.h b/net/http/transport_security_persister.h
index 71b27ed9..aa30cbbe 100644
--- a/net/http/transport_security_persister.h
+++ b/net/http/transport_security_persister.h
@@ -106,12 +106,11 @@
 
   // Clears any existing non-static entries, and then re-populates
   // |transport_security_state_|.
-  bool LoadEntries(const std::string& serialized);
+  void LoadEntries(const std::string& serialized);
 
  private:
-  // Populates |state| from the JSON string |serialized|. Returns true if
-  // all entries were parsed and deserialized correctly.
-  static bool Deserialize(const std::string& serialized,
+  // Populates |state| from the JSON string |serialized|.
+  static void Deserialize(const std::string& serialized,
                           TransportSecurityState* state);
 
   void CompleteLoad(const std::string& state);
diff --git a/net/http/transport_security_persister_unittest.cc b/net/http/transport_security_persister_unittest.cc
index 03cd04f5..530e98ff 100644
--- a/net/http/transport_security_persister_unittest.cc
+++ b/net/http/transport_security_persister_unittest.cc
@@ -105,18 +105,24 @@
   EXPECT_TRUE(state_->GetDynamicExpectCTState(
       kYahooDomain, NetworkIsolationKey(), &expect_ct_state));
 
-  EXPECT_TRUE(persister_->LoadEntries("{\"version\":2}"));
+  persister_->LoadEntries("{\"version\":2}");
 
   EXPECT_FALSE(state_->GetDynamicSTSState(kYahooDomain, &sts_state));
   EXPECT_FALSE(state_->GetDynamicExpectCTState(
       kYahooDomain, NetworkIsolationKey(), &expect_ct_state));
 }
 
+// Tests that serializing -> deserializing -> reserializing results in the same
+// output.
 TEST_P(TransportSecurityPersisterTest, SerializeData1) {
   std::string output;
 
   EXPECT_TRUE(persister_->SerializeData(&output));
-  EXPECT_TRUE(persister_->LoadEntries(output));
+  persister_->LoadEntries(output);
+
+  std::string output2;
+  EXPECT_TRUE(persister_->SerializeData(&output2));
+  EXPECT_EQ(output, output2);
 }
 
 TEST_P(TransportSecurityPersisterTest, SerializeData2) {
@@ -132,7 +138,7 @@
 
   std::string output;
   EXPECT_TRUE(persister_->SerializeData(&output));
-  EXPECT_TRUE(persister_->LoadEntries(output));
+  persister_->LoadEntries(output);
 
   EXPECT_TRUE(state_->GetDynamicSTSState(kYahooDomain, &sts_state));
   EXPECT_EQ(sts_state.upgrade_mode,
@@ -196,7 +202,7 @@
   std::string persisted;
   EXPECT_TRUE(base::ReadFileToString(path, &persisted));
   EXPECT_EQ(persisted, serialized);
-  EXPECT_TRUE(persister_->LoadEntries(persisted));
+  persister_->LoadEntries(persisted);
 
   // Check that states are the same as saved.
   size_t count = 0;
@@ -216,12 +222,28 @@
   EXPECT_EQ(count, expect_ct_saved.size());
 }
 
+// Tests that deserializing bad data shouldn't result in any ExpectCT or STS
+// entries being added to the transport security state.
 TEST_P(TransportSecurityPersisterTest, DeserializeBadData) {
-  EXPECT_FALSE(persister_->LoadEntries(""));
-  EXPECT_FALSE(persister_->LoadEntries("Foopy"));
-  EXPECT_FALSE(persister_->LoadEntries("15"));
-  EXPECT_FALSE(persister_->LoadEntries("[15]"));
-  EXPECT_FALSE(persister_->LoadEntries("{\"version\":1}"));
+  persister_->LoadEntries("");
+  EXPECT_EQ(0u, state_->num_expect_ct_entries());
+  EXPECT_EQ(0u, state_->num_sts_entries());
+
+  persister_->LoadEntries("Foopy");
+  EXPECT_EQ(0u, state_->num_expect_ct_entries());
+  EXPECT_EQ(0u, state_->num_sts_entries());
+
+  persister_->LoadEntries("15");
+  EXPECT_EQ(0u, state_->num_expect_ct_entries());
+  EXPECT_EQ(0u, state_->num_sts_entries());
+
+  persister_->LoadEntries("[15]");
+  EXPECT_EQ(0u, state_->num_expect_ct_entries());
+  EXPECT_EQ(0u, state_->num_sts_entries());
+
+  persister_->LoadEntries("{\"version\":1}");
+  EXPECT_EQ(0u, state_->num_expect_ct_entries());
+  EXPECT_EQ(0u, state_->num_sts_entries());
 }
 
 TEST_P(TransportSecurityPersisterTest, DeserializeDataOldWithoutCreationDate) {
@@ -235,7 +257,9 @@
       "\"mode\": \"strict\" "
       "}"
       "}";
-  EXPECT_FALSE(persister_->LoadEntries(kInput));
+  persister_->LoadEntries(kInput);
+  EXPECT_EQ(0u, state_->num_expect_ct_entries());
+  EXPECT_EQ(0u, state_->num_sts_entries());
 }
 
 TEST_P(TransportSecurityPersisterTest, DeserializeDataOldMergedDictionary) {
@@ -275,7 +299,9 @@
       "   }"
       "}";
 
-  EXPECT_FALSE(persister_->LoadEntries(kInput));
+  persister_->LoadEntries(kInput);
+  EXPECT_EQ(0u, state_->num_expect_ct_entries());
+  EXPECT_EQ(0u, state_->num_sts_entries());
 }
 
 // Tests that dynamic Expect-CT state is serialized and deserialized correctly.
@@ -298,7 +324,7 @@
   EXPECT_TRUE(persister_->SerializeData(&serialized));
   // LoadEntries() clears existing dynamic data before loading entries from
   // |serialized|.
-  EXPECT_TRUE(persister_->LoadEntries(serialized));
+  persister_->LoadEntries(serialized);
 
   TransportSecurityState::ExpectCTState new_expect_ct_state;
   EXPECT_TRUE(state_->GetDynamicExpectCTState(
@@ -312,7 +338,7 @@
   state_->AddExpectCT(kTestDomain, expiry, false /* enforce */, report_uri,
                       NetworkIsolationKey());
   EXPECT_TRUE(persister_->SerializeData(&serialized));
-  EXPECT_TRUE(persister_->LoadEntries(serialized));
+  persister_->LoadEntries(serialized);
   EXPECT_TRUE(state_->GetDynamicExpectCTState(
       kTestDomain, NetworkIsolationKey(), &new_expect_ct_state));
   EXPECT_FALSE(new_expect_ct_state.enforce);
@@ -343,7 +369,7 @@
   EXPECT_TRUE(persister_->SerializeData(&serialized));
   // LoadEntries() clears existing dynamic data before loading entries from
   // |serialized|.
-  EXPECT_TRUE(persister_->LoadEntries(serialized));
+  persister_->LoadEntries(serialized);
 
   TransportSecurityState::ExpectCTState new_expect_ct_state;
   EXPECT_TRUE(state_->GetDynamicExpectCTState(
@@ -377,7 +403,7 @@
                       NetworkIsolationKey());
   std::string serialized;
   EXPECT_TRUE(persister_->SerializeData(&serialized));
-  EXPECT_TRUE(persister_->LoadEntries(serialized));
+  persister_->LoadEntries(serialized);
 
   TransportSecurityState::ExpectCTState new_expect_ct_state;
   EXPECT_FALSE(state_->GetDynamicExpectCTState(
@@ -437,7 +463,7 @@
   }
 
   // Load entries into the other persister.
-  EXPECT_TRUE(persister_->LoadEntries(serialized));
+  persister_->LoadEntries(serialized);
 
   if (partition_expect_ct_state()) {
     TransportSecurityState::ExpectCTState new_expect_ct_state;
@@ -521,7 +547,7 @@
                                          "\"Not a valid NIK\"");
 
   // Load entries into the other persister.
-  EXPECT_TRUE(persister_->LoadEntries(serialized));
+  persister_->LoadEntries(serialized);
 
   // The entry with the non-empty NetworkIsolationKey should be dropped, since
   // its NIK is now invalid. The other entry should be preserved.
diff --git a/net/http/transport_security_state.cc b/net/http/transport_security_state.cc
index c736aef8..857a7b4d 100644
--- a/net/http/transport_security_state.cc
+++ b/net/http/transport_security_state.cc
@@ -1072,6 +1072,10 @@
   return enabled_expect_ct_hosts_.size();
 }
 
+size_t TransportSecurityState::num_sts_entries() const {
+  return enabled_sts_hosts_.size();
+}
+
 // static
 bool TransportSecurityState::IsBuildTimely() {
   const base::Time build_time = base::GetBuildTime();
diff --git a/net/http/transport_security_state.h b/net/http/transport_security_state.h
index 94b1ffc..aa1c837 100644
--- a/net/http/transport_security_state.h
+++ b/net/http/transport_security_state.h
@@ -576,6 +576,9 @@
   // The number of cached ExpectCTState entries.
   size_t num_expect_ct_entries() const;
 
+  // The number of cached STSState entries.
+  size_t num_sts_entries() const;
+
  private:
   friend class TransportSecurityStateTest;
   friend class TransportSecurityStateStaticFuzzer;
diff --git a/printing/backend/print_backend_cups.cc b/printing/backend/print_backend_cups.cc
index 20f31aa..dc0baf895 100644
--- a/printing/backend/print_backend_cups.cc
+++ b/printing/backend/print_backend_cups.cc
@@ -81,7 +81,7 @@
   const char* drv_info = cupsGetOption(kCUPSOptPrinterMakeAndModel,
                                        printer.num_options, printer.options);
   if (drv_info)
-    printer_info->options[kDriverInfoTagName] = *drv_info;
+    printer_info->options[kDriverInfoTagName] = drv_info;
 
   // Store printer options.
   for (int opt_index = 0; opt_index < printer.num_options; ++opt_index) {
diff --git a/printing/backend/print_backend_cups_unittest.cc b/printing/backend/print_backend_cups_unittest.cc
index e7b4318..39ac7fc 100644
--- a/printing/backend/print_backend_cups_unittest.cc
+++ b/printing/backend/print_backend_cups_unittest.cc
@@ -81,6 +81,17 @@
   EXPECT_EQ(kName, printer_info.display_name);
 #endif
   EXPECT_EQ(kDescription, printer_info.printer_description);
+
+  // The option value of `kCUPSOptPrinterMakeAndModel` is used to set the value
+  // for `kDriverInfoTagName`.
+  auto driver = printer_info.options.find(kDriverInfoTagName);
+#if defined(OS_MAC)
+  ASSERT_NE(driver, printer_info.options.end());
+  EXPECT_EQ(kDescription, driver->second);
+#else
+  // Didn't set option for `kCUPSOptPrinterMakeAndModel`.
+  EXPECT_EQ(driver, printer_info.options.end());
+#endif
 }
 
 TEST(PrintBackendCupsTest, PrinterDriverInfoFromCUPS) {
diff --git a/remoting/host/installer/linux/BUILD.gn b/remoting/host/installer/linux/BUILD.gn
index 22ab53a..638f381 100644
--- a/remoting/host/installer/linux/BUILD.gn
+++ b/remoting/host/installer/linux/BUILD.gn
@@ -53,7 +53,9 @@
     "debian/control",
     "debian/copyright",
     "debian/postinst",
+    "debian/postrm",
     "debian/preinst",
+    "debian/prerm",
     "debian/rules",
     "debian/triggers",
   ]
diff --git a/remoting/host/installer/linux/debian/prerm b/remoting/host/installer/linux/debian/prerm
new file mode 100755
index 0000000..d116fec2
--- /dev/null
+++ b/remoting/host/installer/linux/debian/prerm
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+# 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.
+
+set -e
+
+if [ "$1" = "remove" ]; then
+  # Stop the service when the package is being removed.
+  echo "Stopping Chrome Remote Desktop service..."
+  systemctl stop 'chrome-remote-desktop@*'
+fi
+
+#DEBHELPER#
diff --git a/remoting/host/linux/linux_me2me_host.py b/remoting/host/linux/linux_me2me_host.py
index 024b82a7..1800ced 100755
--- a/remoting/host/linux/linux_me2me_host.py
+++ b/remoting/host/linux/linux_me2me_host.py
@@ -95,6 +95,8 @@
 
 USER_SESSION_PATH = os.path.join(SCRIPT_DIR, "user-session")
 
+SETUP_URL_FORWARDER_PATH = os.path.join(SCRIPT_DIR, "setup-url-forwarder")
+
 CHROME_REMOTING_GROUP_NAME = "chrome-remote-desktop"
 
 HOME_DIR = os.environ["HOME"]
@@ -825,6 +827,21 @@
     finally:
       self.host_proc.stdin.close()
 
+  def restore_default_browser(self):
+    # Restores the previous default browser settings in case the host crashes
+    # during a remote session. It's noop if the current default browser is not
+    # the CRD URL forwarder.
+
+    if not os.path.exists(SETUP_URL_FORWARDER_PATH):
+      print('Cannot find the URL forwarder setup script', file=sys.stderr)
+      return
+    print('Attempting to restore previous default browser...')
+    retcode = subprocess.call([SETUP_URL_FORWARDER_PATH, "--restore"],
+                              env=self.child_env)
+    if retcode != 0:
+      print('URL forwarder setup script returned a non-zero code:', retcode,
+            file=sys.stderr)
+
   def shutdown_all_procs(self):
     """Send SIGTERM to all procs and wait for them to exit. Will fallback to
     SIGKILL if a process doesn't exit within 10 seconds.
@@ -1284,6 +1301,7 @@
 
   global g_desktop
   if g_desktop is not None:
+    g_desktop.restore_default_browser()
     g_desktop.shutdown_all_procs()
     if g_desktop.xorg_conf is not None:
       os.remove(g_desktop.xorg_conf)
@@ -1834,6 +1852,11 @@
       if desktop.host_proc is None:
         logging.info("Launching host process")
 
+        # Restore the previous default browser in case the daemon script has
+        # crashed or the system has been rebooted in the middle of a remote
+        # session.
+        desktop.restore_default_browser()
+
         extra_start_host_args = []
         if HOST_EXTRA_PARAMS_ENV_VAR in os.environ:
             extra_start_host_args = \
diff --git a/sandbox/policy/mac/BUILD.gn b/sandbox/policy/mac/BUILD.gn
index 79c930e5..984fca8 100644
--- a/sandbox/policy/mac/BUILD.gn
+++ b/sandbox/policy/mac/BUILD.gn
@@ -9,6 +9,7 @@
   "cdm.sb",
   "common.sb",
   "gpu.sb",
+  "mirroring.sb",
   "nacl_loader.sb",
   "network.sb",
   "ppapi.sb",
diff --git a/sandbox/policy/mac/mirroring.sb b/sandbox/policy/mac/mirroring.sb
new file mode 100644
index 0000000..0b7794b
--- /dev/null
+++ b/sandbox/policy/mac/mirroring.sb
@@ -0,0 +1,11 @@
+; Copyright 2021 The Chromium Authors. All rights reserved.
+; Use of this source code is governed by a BSD-style license that can be
+; found in the LICENSE file.
+
+; --- The contents of common.sb implicitly included here. ---
+
+; Needed for IOSurface GpuMemoryBuffer video frame access
+; https://crbug.com/1204603
+(allow iokit-open
+  (iokit-registry-entry-class "IOSurfaceRootUserClient")
+)
diff --git a/sandbox/policy/mac/sandbox_mac.mm b/sandbox/policy/mac/sandbox_mac.mm
index db147c48..c660515 100644
--- a/sandbox/policy/mac/sandbox_mac.mm
+++ b/sandbox/policy/mac/sandbox_mac.mm
@@ -16,6 +16,7 @@
 #include "sandbox/policy/mac/cdm.sb.h"
 #include "sandbox/policy/mac/common.sb.h"
 #include "sandbox/policy/mac/gpu.sb.h"
+#include "sandbox/policy/mac/mirroring.sb.h"
 #include "sandbox/policy/mac/nacl_loader.sb.h"
 #include "sandbox/policy/mac/network.sb.h"
 #include "sandbox/policy/mac/ppapi.sb.h"
@@ -57,6 +58,9 @@
     case SandboxType::kGpu:
       profile += kSeatbeltPolicyString_gpu;
       break;
+    case SandboxType::kMirroring:
+      profile += kSeatbeltPolicyString_mirroring;
+      break;
     case SandboxType::kNaClLoader:
       profile += kSeatbeltPolicyString_nacl_loader;
       break;
diff --git a/sandbox/policy/sandbox_type.cc b/sandbox/policy/sandbox_type.cc
index 870a6c6..21b207ca3 100644
--- a/sandbox/policy/sandbox_type.cc
+++ b/sandbox/policy/sandbox_type.cc
@@ -53,6 +53,7 @@
 #endif
     case SandboxType::kPrintCompositor:
 #if defined(OS_MAC)
+    case SandboxType::kMirroring:
     case SandboxType::kNaClLoader:
 #endif
 #if BUILDFLAG(IS_CHROMEOS_ASH)
@@ -133,6 +134,9 @@
     case SandboxType::kLibassistant:
 #endif  // BUILDFLAG(ENABLE_LIBASSISTANT_SANDBOX)
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
+#if defined(OS_MAC)
+    case SandboxType::kMirroring:
+#endif  // defined(OS_MAC)
 #if !defined(OS_MAC)
     case SandboxType::kService:
 #endif
@@ -258,6 +262,10 @@
     case SandboxType::kMediaFoundationCdm:
       return switches::kMediaFoundationCdmSandbox;
 #endif  // defined(OS_WIN)
+#if defined(OS_MAC)
+    case SandboxType::kMirroring:
+      return switches::kMirroringSandbox;
+#endif
 #if BUILDFLAG(IS_CHROMEOS_ASH)
     case SandboxType::kIme:
       return switches::kImeSandbox;
@@ -319,6 +327,10 @@
   if (sandbox_string == switches::kMediaFoundationCdmSandbox)
     return SandboxType::kMediaFoundationCdm;
 #endif
+#if defined(OS_MAC)
+  if (sandbox_string == switches::kMirroringSandbox)
+    return SandboxType::kMirroring;
+#endif
   if (sandbox_string == switches::kAudioSandbox)
     return SandboxType::kAudio;
   if (sandbox_string == switches::kSpeechRecognitionSandbox)
diff --git a/sandbox/policy/sandbox_type.h b/sandbox/policy/sandbox_type.h
index 14f1695..88903f7 100644
--- a/sandbox/policy/sandbox_type.h
+++ b/sandbox/policy/sandbox_type.h
@@ -76,6 +76,9 @@
 #if defined(OS_MAC)
   // The NaCl loader process.
   kNaClLoader,
+
+  // The mirroring service needs IOSurface access on macOS.
+  kMirroring,
 #endif  // defined(OS_MAC)
 
 #if BUILDFLAG(ENABLE_PRINTING)
diff --git a/sandbox/policy/switches.cc b/sandbox/policy/switches.cc
index 4862627..c0438f6 100644
--- a/sandbox/policy/switches.cc
+++ b/sandbox/policy/switches.cc
@@ -46,6 +46,10 @@
 const char kMediaFoundationCdmSandbox[] = "mf_cdm";
 #endif  // OS_WIN
 
+#if defined(OS_MAC)
+const char kMirroringSandbox[] = "mirroring";
+#endif  // OS_MAC
+
 #if BUILDFLAG(IS_CHROMEOS_ASH)
 const char kImeSandbox[] = "ime";
 const char kTtsSandbox[] = "tts";
diff --git a/sandbox/policy/switches.h b/sandbox/policy/switches.h
index 6754f7c..a0315cd 100644
--- a/sandbox/policy/switches.h
+++ b/sandbox/policy/switches.h
@@ -47,6 +47,10 @@
 SANDBOX_POLICY_EXPORT extern const char kMediaFoundationCdmSandbox[];
 #endif  // OS_WIN
 
+#if defined(OS_MAC)
+SANDBOX_POLICY_EXPORT extern const char kMirroringSandbox[];
+#endif  // OS_MAC
+
 #if BUILDFLAG(IS_CHROMEOS_ASH)
 SANDBOX_POLICY_EXPORT extern const char kImeSandbox[];
 SANDBOX_POLICY_EXPORT extern const char kTtsSandbox[];
diff --git a/sandbox/policy/win/sandbox_win_unittest.cc b/sandbox/policy/win/sandbox_win_unittest.cc
index 591653a..44a6ea7 100644
--- a/sandbox/policy/win/sandbox_win_unittest.cc
+++ b/sandbox/policy/win/sandbox_win_unittest.cc
@@ -328,7 +328,8 @@
   CheckCapabilities(profile.get(), {L"cap1", L"cap2"});
 }
 
-TEST_F(SandboxWinTest, BlocklistAddOneDllCheckInBrowser) {
+// Disabled due to crbug.com/1210614
+TEST_F(SandboxWinTest, DISABLED_BlocklistAddOneDllCheckInBrowser) {
   {  // Block loaded module.
     TestTargetPolicy policy;
     BlocklistAddOneDllForTesting(L"kernel32.dll", true, &policy);
diff --git a/testing/buildbot/chrome.json b/testing/buildbot/chrome.json
index 12f5fc26..70774c7 100644
--- a/testing/buildbot/chrome.json
+++ b/testing/buildbot/chrome.json
@@ -2812,6 +2812,37 @@
       }
     ]
   },
+  "lacros-arm-generic-chrome": {
+    "additional_compile_targets": [
+      "chrome",
+      "lacros_version_metadata",
+      "linux_symbols",
+      "symupload",
+      "strip_chrome_binary"
+    ],
+    "isolated_scripts": [
+      {
+        "isolate_name": "chrome_sizes",
+        "merge": {
+          "script": "//tools/perf/process_perf_results.py"
+        },
+        "name": "chrome_sizes",
+        "resultdb": {
+          "enable": false
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "dimension_sets": [
+            {
+              "pool": "chrome.tests"
+            }
+          ],
+          "service_account": "chrome-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test_id_prefix": "ninja://chrome/test:chrome_sizes/"
+      }
+    ]
+  },
   "linux-chrome": {
     "additional_compile_targets": [
       "chrome",
diff --git a/testing/buildbot/chromium.fyi.json b/testing/buildbot/chromium.fyi.json
index 0a87d9d..d4aea48 100644
--- a/testing/buildbot/chromium.fyi.json
+++ b/testing/buildbot/chromium.fyi.json
@@ -1736,36 +1736,100 @@
   "android-backuprefptr-arm-fyi-rel": {
     "gtest_tests": [
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "absl_hardening_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "absl_hardening_tests",
         "test_id_prefix": "ninja://third_party/abseil-cpp:absl_hardening_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_browsertests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "android_browsertests",
@@ -1773,38 +1837,100 @@
       },
       {
         "args": [
-          "--test-launcher-batch-limit=1"
+          "--test-launcher-batch-limit=1",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
         ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_sync_integration_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "android_sync_integration_tests",
         "test_id_prefix": "ninja://chrome/test:android_sync_integration_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_webview_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "android_webview_unittests",
@@ -1812,16 +1938,30 @@
       },
       {
         "args": [
-          "angle_unittests"
+          "angle_unittests",
+          "-v"
         ],
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
         },
+        "resultdb": {
+          "enable": true
+        },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
@@ -1833,163 +1973,452 @@
         "use_isolated_scripts_api": true
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "base_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "base_unittests",
         "test_id_prefix": "ninja://base:base_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "base_util_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "base_util_unittests",
         "test_id_prefix": "ninja://base/util:base_util_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "blink_common_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "blink_common_unittests",
         "test_id_prefix": "ninja://third_party/blink/common:blink_common_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "blink_heap_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "blink_heap_unittests",
         "test_id_prefix": "ninja://third_party/blink/renderer/platform/heap:blink_heap_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "blink_platform_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "blink_platform_unittests",
         "test_id_prefix": "ninja://third_party/blink/renderer/platform:blink_platform_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webkit_unit_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "name": "webkit_unit_tests",
+        "resultdb": {
+          "enable": true
+        },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
         },
         "test": "blink_unittests",
         "test_id_prefix": "ninja://third_party/blink/renderer/controller:blink_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "boringssl_crypto_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "boringssl_crypto_tests",
         "test_id_prefix": "ninja://third_party/boringssl:boringssl_crypto_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "boringssl_ssl_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "boringssl_ssl_tests",
         "test_id_prefix": "ninja://third_party/boringssl:boringssl_ssl_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "breakpad_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "breakpad_unittests",
@@ -1997,74 +2426,200 @@
       },
       {
         "args": [
-          "--gtest_filter=-*UsingRealWebcam*"
+          "--gtest_filter=-*UsingRealWebcam*",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
         ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "capture_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "capture_unittests",
         "test_id_prefix": "ninja://media/capture:capture_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "cast_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "cast_unittests",
         "test_id_prefix": "ninja://media/cast:cast_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "cc_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "cc_unittests",
         "test_id_prefix": "ninja://cc:cc_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_smoke_test"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "chrome_public_smoke_test",
@@ -2072,11 +2627,18 @@
       },
       {
         "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
           "--git-revision=${got_revision}"
         ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "precommit_args": [
           "--gerrit-issue=${patch_issue}",
@@ -2088,12 +2650,32 @@
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chrome-gold@chops-service-accounts.iam.gserviceaccount.com",
           "shards": 20
         },
@@ -2103,20 +2685,50 @@
       {
         "args": [
           "--shared-prefs-file=//chrome/android/shared_preference_files/test/vr_cardboard_skipdon_setupcomplete.json",
-          "--additional-apk=//third_party/gvr-android-sdk/test-apks/vr_services/vr_services_current.apk"
+          "--additional-apk=//third_party/gvr-android-sdk/test-apks/vr_services/vr_services_current.apk",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
         ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_test_vr_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
           "shards": 2
         },
@@ -2124,73 +2736,202 @@
         "test_id_prefix": "ninja://chrome/android:chrome_public_test_vr_apk/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "components_browsertests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "components_browsertests",
         "test_id_prefix": "ninja://components:components_browsertests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "components_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
         },
         "test": "components_unittests",
         "test_id_prefix": "ninja://components:components_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "content_browsertests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 6
+          "shards": 9
         },
         "test": "content_browsertests",
         "test_id_prefix": "ninja://content/test:content_browsertests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "content_shell_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
           "shards": 3
         },
@@ -2198,162 +2939,451 @@
         "test_id_prefix": "ninja://content/shell/android:content_shell_test_apk/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "content_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 3
         },
         "test": "content_unittests",
         "test_id_prefix": "ninja://content/test:content_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "crashpad_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "crashpad_tests",
         "test_id_prefix": "ninja://third_party/crashpad/crashpad:crashpad_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "crypto_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "crypto_unittests",
         "test_id_prefix": "ninja://crypto:crypto_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "device_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "device_unittests",
         "test_id_prefix": "ninja://device:device_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "display_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "display_unittests",
         "test_id_prefix": "ninja://ui/display:display_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "events_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "events_unittests",
         "test_id_prefix": "ninja://ui/events:events_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gcm_unit_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gcm_unit_tests",
         "test_id_prefix": "ninja://google_apis/gcm:gcm_unit_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gfx_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gfx_unittests",
         "test_id_prefix": "ninja://ui/gfx:gfx_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gin_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gin_unittests",
@@ -2361,597 +3391,1653 @@
       },
       {
         "args": [
-          "--use-cmd-decoder=validating"
+          "--use-cmd-decoder=validating",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
         ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gl_tests_validating"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "name": "gl_tests_validating",
+        "resultdb": {
+          "enable": true
+        },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gl_tests",
         "test_id_prefix": "ninja://gpu:gl_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gl_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gl_unittests",
         "test_id_prefix": "ninja://ui/gl:gl_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "google_apis_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "google_apis_unittests",
         "test_id_prefix": "ninja://google_apis:google_apis_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gpu_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gpu_unittests",
         "test_id_prefix": "ninja://gpu:gpu_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gwp_asan_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gwp_asan_unittests",
         "test_id_prefix": "ninja://components/gwp_asan:gwp_asan_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ipc_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "ipc_tests",
         "test_id_prefix": "ninja://ipc:ipc_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "jingle_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "jingle_unittests",
         "test_id_prefix": "ninja://jingle:jingle_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "latency_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "latency_unittests",
         "test_id_prefix": "ninja://ui/latency:latency_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "libjingle_xmpp_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "libjingle_xmpp_unittests",
         "test_id_prefix": "ninja://third_party/libjingle_xmpp:libjingle_xmpp_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "liburlpattern_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "liburlpattern_unittests",
         "test_id_prefix": "ninja://third_party/liburlpattern:liburlpattern_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "media_blink_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "media_blink_unittests",
         "test_id_prefix": "ninja://media/blink:media_blink_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "media_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "media_unittests",
         "test_id_prefix": "ninja://media:media_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "midi_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "midi_unittests",
         "test_id_prefix": "ninja://media/midi:midi_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "mojo_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "mojo_test_apk",
         "test_id_prefix": "ninja://mojo/public/java/system:mojo_test_apk/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "mojo_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "mojo_unittests",
         "test_id_prefix": "ninja://mojo:mojo_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "net_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 3
         },
         "test": "net_unittests",
         "test_id_prefix": "ninja://net:net_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "perfetto_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "perfetto_unittests",
         "test_id_prefix": "ninja://third_party/perfetto:perfetto_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "sandbox_linux_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "sandbox_linux_unittests",
         "test_id_prefix": "ninja://sandbox/linux:sandbox_linux_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "services_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "services_unittests",
         "test_id_prefix": "ninja://services:services_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "shell_dialogs_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "shell_dialogs_unittests",
         "test_id_prefix": "ninja://ui/shell_dialogs:shell_dialogs_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "skia_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "skia_unittests",
         "test_id_prefix": "ninja://skia:skia_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "sql_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "sql_unittests",
         "test_id_prefix": "ninja://sql:sql_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "storage_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "storage_unittests",
         "test_id_prefix": "ninja://storage:storage_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ui_android_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "ui_android_unittests",
         "test_id_prefix": "ninja://ui/android:ui_android_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ui_base_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "ui_base_unittests",
         "test_id_prefix": "ninja://ui/base:ui_base_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ui_touch_selection_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "ui_touch_selection_unittests",
         "test_id_prefix": "ninja://ui/touch_selection:ui_touch_selection_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "unit_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
         },
         "test": "unit_tests",
         "test_id_prefix": "ninja://chrome/test:unit_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "url_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "url_unittests",
         "test_id_prefix": "ninja://url:url_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "viz_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "viz_unittests",
         "test_id_prefix": "ninja://components/viz:viz_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "vr_android_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "vr_android_unittests",
         "test_id_prefix": "ninja://chrome/browser/android/vr:vr_android_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "vr_common_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "vr_common_unittests",
         "test_id_prefix": "ninja://chrome/browser/vr:vr_common_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "vr_pixeltests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "vr_pixeltests",
         "test_id_prefix": "ninja://chrome/browser/vr:vr_pixeltests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webview_instrumentation_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
           "shards": 7
         },
@@ -2959,36 +5045,100 @@
         "test_id_prefix": "ninja://android_webview/test:webview_instrumentation_test_apk/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "wtf_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "wtf_unittests",
         "test_id_prefix": "ninja://third_party/blink/renderer/platform/wtf:wtf_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "zlib_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "resultdb": {
+          "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "zlib_unittests",
@@ -2999,42 +5149,100 @@
   "android-backuprefptr-arm64-fyi-rel": {
     "gtest_tests": [
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "absl_hardening_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "absl_hardening_tests",
         "test_id_prefix": "ninja://third_party/abseil-cpp:absl_hardening_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_browsertests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "android_browsertests",
@@ -3042,44 +5250,100 @@
       },
       {
         "args": [
-          "--test-launcher-batch-limit=1"
+          "--test-launcher-batch-limit=1",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
         ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_sync_integration_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "android_sync_integration_tests",
         "test_id_prefix": "ninja://chrome/test:android_sync_integration_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_webview_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "android_webview_unittests",
@@ -3087,7 +5351,8 @@
       },
       {
         "args": [
-          "angle_unittests"
+          "angle_unittests",
+          "-v"
         ],
         "merge": {
           "args": [],
@@ -3098,8 +5363,18 @@
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
@@ -3111,114 +5386,268 @@
         "use_isolated_scripts_api": true
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "base_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "base_unittests",
         "test_id_prefix": "ninja://base:base_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "base_util_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "base_util_unittests",
         "test_id_prefix": "ninja://base/util:base_util_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "blink_common_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "blink_common_unittests",
         "test_id_prefix": "ninja://third_party/blink/common:blink_common_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "blink_heap_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "blink_heap_unittests",
         "test_id_prefix": "ninja://third_party/blink/renderer/platform/heap:blink_heap_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "blink_platform_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "blink_platform_unittests",
         "test_id_prefix": "ninja://third_party/blink/renderer/platform:blink_platform_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webkit_unit_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "name": "webkit_unit_tests",
         "resultdb": {
@@ -3226,75 +5655,183 @@
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
         },
         "test": "blink_unittests",
         "test_id_prefix": "ninja://third_party/blink/renderer/controller:blink_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "boringssl_crypto_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "boringssl_crypto_tests",
         "test_id_prefix": "ninja://third_party/boringssl:boringssl_crypto_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "boringssl_ssl_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "boringssl_ssl_tests",
         "test_id_prefix": "ninja://third_party/boringssl:boringssl_ssl_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "breakpad_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "breakpad_unittests",
@@ -3302,86 +5839,200 @@
       },
       {
         "args": [
-          "--gtest_filter=-*UsingRealWebcam*"
+          "--gtest_filter=-*UsingRealWebcam*",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
         ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "capture_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "capture_unittests",
         "test_id_prefix": "ninja://media/capture:capture_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "cast_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "cast_unittests",
         "test_id_prefix": "ninja://media/cast:cast_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "cc_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "cc_unittests",
         "test_id_prefix": "ninja://cc:cc_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_smoke_test"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "chrome_public_smoke_test",
@@ -3389,11 +6040,18 @@
       },
       {
         "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices",
           "--git-revision=${got_revision}"
         ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "precommit_args": [
           "--gerrit-issue=${patch_issue}",
@@ -3405,12 +6063,32 @@
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chrome-gold@chops-service-accounts.iam.gserviceaccount.com",
           "shards": 20
         },
@@ -3420,23 +6098,50 @@
       {
         "args": [
           "--shared-prefs-file=//chrome/android/shared_preference_files/test/vr_cardboard_skipdon_setupcomplete.json",
-          "--additional-apk=//third_party/gvr-android-sdk/test-apks/vr_services/vr_services_current.apk"
+          "--additional-apk=//third_party/gvr-android-sdk/test-apks/vr_services/vr_services_current.apk",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
         ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_test_vr_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
           "shards": 2
         },
@@ -3444,85 +6149,202 @@
         "test_id_prefix": "ninja://chrome/android:chrome_public_test_vr_apk/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "components_browsertests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "components_browsertests",
         "test_id_prefix": "ninja://components:components_browsertests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "components_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
         },
         "test": "components_unittests",
         "test_id_prefix": "ninja://components:components_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "content_browsertests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 6
+          "shards": 9
         },
         "test": "content_browsertests",
         "test_id_prefix": "ninja://content/test:content_browsertests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "content_shell_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
           "shards": 3
         },
@@ -3530,189 +6352,451 @@
         "test_id_prefix": "ninja://content/shell/android:content_shell_test_apk/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "content_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 3
         },
         "test": "content_unittests",
         "test_id_prefix": "ninja://content/test:content_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "crashpad_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "crashpad_tests",
         "test_id_prefix": "ninja://third_party/crashpad/crashpad:crashpad_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "crypto_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "crypto_unittests",
         "test_id_prefix": "ninja://crypto:crypto_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "device_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "device_unittests",
         "test_id_prefix": "ninja://device:device_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "display_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "display_unittests",
         "test_id_prefix": "ninja://ui/display:display_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "events_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "events_unittests",
         "test_id_prefix": "ninja://ui/events:events_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gcm_unit_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gcm_unit_tests",
         "test_id_prefix": "ninja://google_apis/gcm:gcm_unit_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gfx_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gfx_unittests",
         "test_id_prefix": "ninja://ui/gfx:gfx_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gin_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gin_unittests",
@@ -3720,11 +6804,18 @@
       },
       {
         "args": [
-          "--use-cmd-decoder=validating"
+          "--use-cmd-decoder=validating",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
         ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gl_tests_validating"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "name": "gl_tests_validating",
         "resultdb": {
@@ -3732,684 +6823,1634 @@
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gl_tests",
         "test_id_prefix": "ninja://gpu:gl_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gl_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gl_unittests",
         "test_id_prefix": "ninja://ui/gl:gl_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "google_apis_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "google_apis_unittests",
         "test_id_prefix": "ninja://google_apis:google_apis_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gpu_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gpu_unittests",
         "test_id_prefix": "ninja://gpu:gpu_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "gwp_asan_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "gwp_asan_unittests",
         "test_id_prefix": "ninja://components/gwp_asan:gwp_asan_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ipc_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "ipc_tests",
         "test_id_prefix": "ninja://ipc:ipc_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "jingle_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "jingle_unittests",
         "test_id_prefix": "ninja://jingle:jingle_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "latency_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "latency_unittests",
         "test_id_prefix": "ninja://ui/latency:latency_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "libjingle_xmpp_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "libjingle_xmpp_unittests",
         "test_id_prefix": "ninja://third_party/libjingle_xmpp:libjingle_xmpp_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "liburlpattern_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "liburlpattern_unittests",
         "test_id_prefix": "ninja://third_party/liburlpattern:liburlpattern_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "media_blink_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "media_blink_unittests",
         "test_id_prefix": "ninja://media/blink:media_blink_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "media_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "media_unittests",
         "test_id_prefix": "ninja://media:media_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "midi_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "midi_unittests",
         "test_id_prefix": "ninja://media/midi:midi_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "mojo_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "mojo_test_apk",
         "test_id_prefix": "ninja://mojo/public/java/system:mojo_test_apk/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "mojo_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "mojo_unittests",
         "test_id_prefix": "ninja://mojo:mojo_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "net_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 3
         },
         "test": "net_unittests",
         "test_id_prefix": "ninja://net:net_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "perfetto_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "perfetto_unittests",
         "test_id_prefix": "ninja://third_party/perfetto:perfetto_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "sandbox_linux_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "sandbox_linux_unittests",
         "test_id_prefix": "ninja://sandbox/linux:sandbox_linux_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "services_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "services_unittests",
         "test_id_prefix": "ninja://services:services_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "shell_dialogs_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "shell_dialogs_unittests",
         "test_id_prefix": "ninja://ui/shell_dialogs:shell_dialogs_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "skia_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "skia_unittests",
         "test_id_prefix": "ninja://skia:skia_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "sql_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "sql_unittests",
         "test_id_prefix": "ninja://sql:sql_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "storage_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "storage_unittests",
         "test_id_prefix": "ninja://storage:storage_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ui_android_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "ui_android_unittests",
         "test_id_prefix": "ninja://ui/android:ui_android_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ui_base_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "ui_base_unittests",
         "test_id_prefix": "ninja://ui/base:ui_base_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "ui_touch_selection_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "ui_touch_selection_unittests",
         "test_id_prefix": "ninja://ui/touch_selection:ui_touch_selection_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "unit_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 2
         },
         "test": "unit_tests",
         "test_id_prefix": "ninja://chrome/test:unit_tests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "url_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "url_unittests",
         "test_id_prefix": "ninja://url:url_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "viz_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "viz_unittests",
         "test_id_prefix": "ninja://components/viz:viz_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "vr_android_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "vr_android_unittests",
         "test_id_prefix": "ninja://chrome/browser/android/vr:vr_android_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "vr_common_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "vr_common_unittests",
         "test_id_prefix": "ninja://chrome/browser/vr:vr_common_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "vr_pixeltests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "vr_pixeltests",
         "test_id_prefix": "ninja://chrome/browser/vr:vr_pixeltests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "webview_instrumentation_test_apk"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
           "shards": 7
         },
@@ -4417,42 +8458,100 @@
         "test_id_prefix": "ninja://android_webview/test:webview_instrumentation_test_apk/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "wtf_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "wtf_unittests",
         "test_id_prefix": "ninja://third_party/blink/renderer/platform/wtf:wtf_unittests/"
       },
       {
+        "args": [
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "zlib_unittests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
         "resultdb": {
           "enable": true
         },
         "swarming": {
           "can_use_on_swarming_builders": true,
+          "cipd_packages": [
+            {
+              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
+              "location": "bin",
+              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
+            }
+          ],
           "dimension_sets": [
             {
+              "device_os": "PQ3A.190801.002",
+              "device_os_flavor": "google",
+              "device_os_type": "userdebug",
               "device_type": "walleye",
               "os": "Android"
             }
           ],
+          "output_links": [
+            {
+              "link": [
+                "https://luci-logdog.appspot.com/v/?s",
+                "=android%2Fswarming%2Flogcats%2F",
+                "${TASK_ID}%2F%2B%2Funified_logcats"
+              ],
+              "name": "shard #${SHARD_INDEX} logcats"
+            }
+          ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "zlib_unittests",
diff --git a/testing/buildbot/waterfalls.pyl b/testing/buildbot/waterfalls.pyl
index a476a7e..f25f15e3 100644
--- a/testing/buildbot/waterfalls.pyl
+++ b/testing/buildbot/waterfalls.pyl
@@ -145,6 +145,22 @@
         },
         'os_type': 'chromeos',
       },
+      'lacros-arm-generic-chrome': {
+        'additional_compile_targets': [
+          'chrome',
+          'lacros_version_metadata',
+          'linux_symbols',
+          'symupload',
+          'strip_chrome_binary'
+        ],
+        'mixins': [
+          'chrome-swarming-pool',
+        ],
+        'test_suites': {
+          'isolated_scripts': 'chrome_sizes',
+        },
+        'os_type': 'chromeos',
+      },
       'linux-chrome': {
         'additional_compile_targets': [
           'chrome',
@@ -2689,8 +2705,11 @@
           'gtest_tests': 'chromium_android_gtests',
         },
         'mixins': [
+          'enable_resultdb',
+          'pie_fleet',
           'walleye',
         ],
+        'os_type': 'android',
       },
       'android-backuprefptr-arm64-fyi-rel': {
         'test_suites': {
@@ -2698,8 +2717,10 @@
         },
         'mixins': [
           'enable_resultdb',
+          'pie_fleet',
           'walleye',
         ],
+        'os_type': 'android',
       },
       'android-code-coverage': {
         'mixins': [
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 58c4cc7..6176d48e 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -4140,10 +4140,11 @@
                 {
                     "name": "G2_20201202",
                     "params": {
-                        "BlockedTypes": "28",
-                        "Gen": "2",
-                        "Max": "10",
-                        "Rho": "100"
+                        "BlockedHashes": "44033, 44289, 286465, 680961, 681473, 693249, 693505,693761, 694017, 696065, 880129, 881409, 881665, 881921",
+                        "BlockedTypes": "13, 25, 28",
+                        "Gen": "5",
+                        "Max": "40",
+                        "Rho": "19"
                     },
                     "enable_features": [
                         "IdentifiabilityStudy"
@@ -5722,22 +5723,6 @@
             ]
         }
     ],
-    "PageInfoV2": [
-        {
-            "platforms": [
-                "android",
-                "android_weblayer"
-            ],
-            "experiments": [
-                {
-                    "name": "Enabled",
-                    "enable_features": [
-                        "PageInfoV2"
-                    ]
-                }
-            ]
-        }
-    ],
     "PageLoadMetricsBufferTimer": [
         {
             "platforms": [
diff --git a/testing/xvfb.py b/testing/xvfb.py
index 05acea14..699cc42 100755
--- a/testing/xvfb.py
+++ b/testing/xvfb.py
@@ -97,9 +97,8 @@
 
   Args:
     cmd: Command to be executed.
-    env: A copy of environment variables, "DISPLAY" and
-      "_CHROMIUM_INSIDE_XVFB" will be set if Xvfb is used. "WAYLAND_DISPLAY"
-      will be set if Weston is used.
+    env: A copy of environment variables. "DISPLAY" and will be set if Xvfb is
+      used. "WAYLAND_DISPLAY" will be set if Weston is used.
     stdoutfile: If provided, symbolization via script is disabled and stdout
       is written to this file as well as to stdout.
     use_openbox: A flag to use openbox process.
@@ -141,7 +140,6 @@
 
 
 def _run_with_xvfb(cmd, env, stdoutfile, use_openbox, use_xcompmgr):
-  env['_CHROMIUM_INSIDE_XVFB'] = '1'
   openbox_proc = None
   xcompmgr_proc = None
   xvfb_proc = None
diff --git a/testing/xvfb_test_script.py b/testing/xvfb_test_script.py
index e1dcdae..8a5b17cd 100755
--- a/testing/xvfb_test_script.py
+++ b/testing/xvfb_test_script.py
@@ -23,9 +23,6 @@
   signal.signal(signal.SIGTERM, print_signal)
   signal.signal(signal.SIGINT, print_signal)
 
-  # test if inside xvfb flag is set.
-  print 'Inside_xvfb :{}'.format(
-      os.environ.get('_CHROMIUM_INSIDE_XVFB', 'None'))
   # test the subprocess display number.
   print 'Display :{}'.format(os.environ.get('DISPLAY', 'None'))
 
diff --git a/testing/xvfb_unittest.py b/testing/xvfb_unittest.py
index b84196f4..f28b6d2 100755
--- a/testing/xvfb_unittest.py
+++ b/testing/xvfb_unittest.py
@@ -66,14 +66,10 @@
   def test_no_xvfb_flag(self):
     proc = launch_process(['--no-xvfb'])
     proc.wait()
-    environ_flag = read_subprocess_message(proc, 'Inside_xvfb :')
-    self.assertEqual(environ_flag, 'None')
 
   def test_xvfb_flag(self):
     proc = launch_process([])
     proc.wait()
-    environ_flag = read_subprocess_message(proc, 'Inside_xvfb :')
-    self.assertEqual(environ_flag, '1')
 
   def test_xvfb_race_condition(self):
     proc_list = [launch_process([]) for _ in range(15)]
diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc
index 9e96a9b..058bdab 100644
--- a/third_party/blink/common/features.cc
+++ b/third_party/blink/common/features.cc
@@ -345,6 +345,10 @@
 const base::Feature kFontAccessPersistent{"FontAccessPersistent",
                                           base::FEATURE_DISABLED_BY_DEFAULT};
 
+// Kill switch for the Compute Pressure API. https://crbug.com/1067627
+const base::Feature kComputePressure{"ComputePressure",
+                                     base::FEATURE_ENABLED_BY_DEFAULT};
+
 // Prefetch request properties are updated to be privacy-preserving. See
 // crbug.com/988956.
 const base::Feature kPrefetchPrivacyChanges{"PrefetchPrivacyChanges",
@@ -939,7 +943,7 @@
 // have their rendering throttled on display:none or zero-area.
 const base::Feature kThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes{
     "ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes",
-    base::FEATURE_ENABLED_BY_DEFAULT};
+    base::FEATURE_DISABLED_BY_DEFAULT};
 
 // Kill switch for the Fledge Interest Group API, i.e. if disabled, the
 // API exposure will be disabled regardless of the OT config.
diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h
index 3556584..e9fa9d8 100644
--- a/third_party/blink/public/common/features.h
+++ b/third_party/blink/public/common/features.h
@@ -97,6 +97,7 @@
 BLINK_COMMON_EXPORT extern const base::Feature kTextFragmentAnchor;
 BLINK_COMMON_EXPORT extern const base::Feature kFontAccess;
 BLINK_COMMON_EXPORT extern const base::Feature kFontAccessPersistent;
+BLINK_COMMON_EXPORT extern const base::Feature kComputePressure;
 BLINK_COMMON_EXPORT extern const base::Feature kFileHandlingAPI;
 BLINK_COMMON_EXPORT extern const base::Feature kAllowSyncXHRInPageDismissal;
 BLINK_COMMON_EXPORT extern const base::Feature kPrefetchPrivacyChanges;
diff --git a/third_party/blink/public/devtools_protocol/browser_protocol.pdl b/third_party/blink/public/devtools_protocol/browser_protocol.pdl
index c049c77..b55e897 100644
--- a/third_party/blink/public/devtools_protocol/browser_protocol.pdl
+++ b/third_party/blink/public/devtools_protocol/browser_protocol.pdl
@@ -6499,6 +6499,7 @@
       usb
       vertical-scroll
       web-share
+      window-placement
       xr-spatial-tracking
 
   # Reason for a permissions policy feature to be disabled.
diff --git a/third_party/blink/public/mojom/permissions_policy/permissions_policy_feature.mojom b/third_party/blink/public/mojom/permissions_policy/permissions_policy_feature.mojom
index 5695984..41de86e 100644
--- a/third_party/blink/public/mojom/permissions_policy/permissions_policy_feature.mojom
+++ b/third_party/blink/public/mojom/permissions_policy/permissions_policy_feature.mojom
@@ -141,6 +141,9 @@
   // Client Hint for the preferred color scheme.
   kClientHintPrefersColorScheme = 85,
 
+  // Controls use of Multi-Screen Window Placement API.
+  kWindowPlacement = 86,
+
   // Don't change assigned numbers of any item, and don't reuse removed slots.
   // Add new features at the end of the enum.
   // Also, run update_permissions_policy_enum.py in
diff --git a/third_party/blink/renderer/core/dom/events/event_dispatcher.cc b/third_party/blink/renderer/core/dom/events/event_dispatcher.cc
index 504d551..37c1b9a 100644
--- a/third_party/blink/renderer/core/dom/events/event_dispatcher.cc
+++ b/third_party/blink/renderer/core/dom/events/event_dispatcher.cc
@@ -381,7 +381,7 @@
     // If a keypress event is prevented, the cursor position may be out of
     // sync as RenderWidgetHostViewCocoa::insertText assumes that the text
     // has been accepted. See https://crbug.com/1204523 for details.
-    if (event_->type() == event_type_names::kKeypress)
+    if (event_->type() == event_type_names::kKeypress && view_)
       view_->GetFrame().GetEditor().SyncSelection(SyncCondition::kForced);
 #endif  // defined(OS_MAC)
   }
diff --git a/third_party/blink/renderer/core/html/forms/base_button_input_type.cc b/third_party/blink/renderer/core/html/forms/base_button_input_type.cc
index fface02..cf415b2 100644
--- a/third_party/blink/renderer/core/html/forms/base_button_input_type.cc
+++ b/third_party/blink/renderer/core/html/forms/base_button_input_type.cc
@@ -77,7 +77,7 @@
 LayoutObject* BaseButtonInputType::CreateLayoutObject(
     const ComputedStyle& style,
     LegacyLayout legacy) const {
-  return LayoutObjectFactory::CreateButton(GetElement(), style, legacy);
+  return LayoutObjectFactory::CreateButton(GetElement(), legacy);
 }
 
 InputType::ValueMode BaseButtonInputType::GetValueMode() const {
diff --git a/third_party/blink/renderer/core/html/forms/file_input_type.cc b/third_party/blink/renderer/core/html/forms/file_input_type.cc
index 3b04500..121aa25 100644
--- a/third_party/blink/renderer/core/html/forms/file_input_type.cc
+++ b/third_party/blink/renderer/core/html/forms/file_input_type.cc
@@ -209,8 +209,7 @@
 
 LayoutObject* FileInputType::CreateLayoutObject(const ComputedStyle& style,
                                                 LegacyLayout legacy) const {
-  return LayoutObjectFactory::CreateFileUploadControl(GetElement(), style,
-                                                      legacy);
+  return LayoutObjectFactory::CreateFileUploadControl(GetElement(), legacy);
 }
 
 InputType::ValueMode FileInputType::GetValueMode() const {
diff --git a/third_party/blink/renderer/core/html/forms/html_button_element.cc b/third_party/blink/renderer/core/html/forms/html_button_element.cc
index 5b52558..3d178fb 100644
--- a/third_party/blink/renderer/core/html/forms/html_button_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_button_element.cc
@@ -57,7 +57,7 @@
       display == EDisplay::kInlineLayoutCustom ||
       display == EDisplay::kLayoutCustom)
     return HTMLFormControlElement::CreateLayoutObject(style, legacy);
-  return LayoutObjectFactory::CreateButton(*this, style, legacy);
+  return LayoutObjectFactory::CreateButton(*this, legacy);
 }
 
 const AtomicString& HTMLButtonElement::FormControlType() const {
diff --git a/third_party/blink/renderer/core/html/forms/html_field_set_element.cc b/third_party/blink/renderer/core/html/forms/html_field_set_element.cc
index 93ab4d0..bf9a257 100644
--- a/third_party/blink/renderer/core/html/forms/html_field_set_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_field_set_element.cc
@@ -121,7 +121,7 @@
 LayoutObject* HTMLFieldSetElement::CreateLayoutObject(
     const ComputedStyle& style,
     LegacyLayout legacy) {
-  return LayoutObjectFactory::CreateFieldset(*this, style, legacy);
+  return LayoutObjectFactory::CreateFieldset(*this, legacy);
 }
 
 LayoutBox* HTMLFieldSetElement::GetLayoutBoxForScrolling() const {
diff --git a/third_party/blink/renderer/core/html/forms/html_select_element.cc b/third_party/blink/renderer/core/html/forms/html_select_element.cc
index 88e95ee..cbc847f8 100644
--- a/third_party/blink/renderer/core/html/forms/html_select_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_select_element.cc
@@ -406,7 +406,7 @@
     const ComputedStyle& style,
     LegacyLayout legacy_layout) {
   if (UsesMenuList())
-    return LayoutObjectFactory::CreateFlexibleBox(*this, style, legacy_layout);
+    return LayoutObjectFactory::CreateFlexibleBox(*this, legacy_layout);
   return LayoutObjectFactory::CreateBlockFlow(*this, style, legacy_layout);
 }
 
diff --git a/third_party/blink/renderer/core/html/forms/html_text_area_element.cc b/third_party/blink/renderer/core/html/forms/html_text_area_element.cc
index 89d0fcc..d9e756d 100644
--- a/third_party/blink/renderer/core/html/forms/html_text_area_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_text_area_element.cc
@@ -263,7 +263,7 @@
 LayoutObject* HTMLTextAreaElement::CreateLayoutObject(
     const ComputedStyle& style,
     LegacyLayout legacy) {
-  return LayoutObjectFactory::CreateTextControlMultiLine(*this, style, legacy);
+  return LayoutObjectFactory::CreateTextControlMultiLine(*this, legacy);
 }
 
 void HTMLTextAreaElement::AppendToFormData(FormData& form_data) {
diff --git a/third_party/blink/renderer/core/html/forms/range_input_type.cc b/third_party/blink/renderer/core/html/forms/range_input_type.cc
index f978b04..f58a5393 100644
--- a/third_party/blink/renderer/core/html/forms/range_input_type.cc
+++ b/third_party/blink/renderer/core/html/forms/range_input_type.cc
@@ -254,7 +254,7 @@
                                                  LegacyLayout legacy) const {
   // TODO(crbug.com/1131352): input[type=range] should not use
   // LayoutFlexibleBox.
-  return LayoutObjectFactory::CreateFlexibleBox(GetElement(), style, legacy);
+  return LayoutObjectFactory::CreateFlexibleBox(GetElement(), legacy);
 }
 
 Decimal RangeInputType::ParseToNumber(const String& src,
diff --git a/third_party/blink/renderer/core/html/forms/slider_thumb_element.cc b/third_party/blink/renderer/core/html/forms/slider_thumb_element.cc
index 21676c6..872142a 100644
--- a/third_party/blink/renderer/core/html/forms/slider_thumb_element.cc
+++ b/third_party/blink/renderer/core/html/forms/slider_thumb_element.cc
@@ -327,7 +327,7 @@
 LayoutObject* SliderContainerElement::CreateLayoutObject(
     const ComputedStyle& style,
     LegacyLayout legacy) {
-  return LayoutObjectFactory::CreateFlexibleBox(*this, style, legacy);
+  return LayoutObjectFactory::CreateFlexibleBox(*this, legacy);
 }
 
 void SliderContainerElement::DefaultEventHandler(Event& event) {
diff --git a/third_party/blink/renderer/core/html/forms/slider_track_element.cc b/third_party/blink/renderer/core/html/forms/slider_track_element.cc
index 55a7d074..01b6f77 100644
--- a/third_party/blink/renderer/core/html/forms/slider_track_element.cc
+++ b/third_party/blink/renderer/core/html/forms/slider_track_element.cc
@@ -13,7 +13,7 @@
 
 LayoutObject* SliderTrackElement::CreateLayoutObject(const ComputedStyle& style,
                                                      LegacyLayout legacy) {
-  return LayoutObjectFactory::CreateSliderTrack(*this, style, legacy);
+  return LayoutObjectFactory::CreateSliderTrack(*this, legacy);
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/html/forms/text_control_inner_elements.cc b/third_party/blink/renderer/core/html/forms/text_control_inner_elements.cc
index 17ead81c..a434fb46 100644
--- a/third_party/blink/renderer/core/html/forms/text_control_inner_elements.cc
+++ b/third_party/blink/renderer/core/html/forms/text_control_inner_elements.cc
@@ -131,8 +131,7 @@
 LayoutObject* TextControlInnerEditorElement::CreateLayoutObject(
     const ComputedStyle& style,
     LegacyLayout legacy) {
-  return LayoutObjectFactory::CreateTextControlInnerEditor(*this, style,
-                                                           legacy);
+  return LayoutObjectFactory::CreateTextControlInnerEditor(*this, legacy);
 }
 
 scoped_refptr<ComputedStyle>
diff --git a/third_party/blink/renderer/core/html/forms/text_field_input_type.cc b/third_party/blink/renderer/core/html/forms/text_field_input_type.cc
index bf44e5b..b55cc1c 100644
--- a/third_party/blink/renderer/core/html/forms/text_field_input_type.cc
+++ b/third_party/blink/renderer/core/html/forms/text_field_input_type.cc
@@ -285,8 +285,7 @@
 LayoutObject* TextFieldInputType::CreateLayoutObject(
     const ComputedStyle& style,
     LegacyLayout legacy) const {
-  return LayoutObjectFactory::CreateTextControlSingleLine(GetElement(), style,
-                                                          legacy);
+  return LayoutObjectFactory::CreateTextControlSingleLine(GetElement(), legacy);
 }
 
 void TextFieldInputType::CreateShadowSubtree() {
diff --git a/third_party/blink/renderer/core/html/html_progress_element.cc b/third_party/blink/renderer/core/html/html_progress_element.cc
index e7ba5e9a..7410e1e 100644
--- a/third_party/blink/renderer/core/html/html_progress_element.cc
+++ b/third_party/blink/renderer/core/html/html_progress_element.cc
@@ -54,7 +54,7 @@
   }
   UseCounter::Count(GetDocument(),
                     WebFeature::kProgressElementWithProgressBarAppearance);
-  return LayoutObjectFactory::CreateProgress(this, style, legacy);
+  return LayoutObjectFactory::CreateProgress(this, legacy);
 }
 
 LayoutProgress* HTMLProgressElement::GetLayoutProgress() const {
diff --git a/third_party/blink/renderer/core/html/html_rt_element.cc b/third_party/blink/renderer/core/html/html_rt_element.cc
index 548e222..8b3921c7 100644
--- a/third_party/blink/renderer/core/html/html_rt_element.cc
+++ b/third_party/blink/renderer/core/html/html_rt_element.cc
@@ -17,7 +17,7 @@
 LayoutObject* HTMLRTElement::CreateLayoutObject(const ComputedStyle& style,
                                                 LegacyLayout legacy) {
   if (style.Display() == EDisplay::kBlock)
-    return LayoutObjectFactory::CreateRubyText(this, style, legacy);
+    return LayoutObjectFactory::CreateRubyText(this, legacy);
   return LayoutObject::CreateObject(this, style, legacy);
 }
 
diff --git a/third_party/blink/renderer/core/html/html_ruby_element.cc b/third_party/blink/renderer/core/html/html_ruby_element.cc
index 96c3ae0..d0cbef5 100644
--- a/third_party/blink/renderer/core/html/html_ruby_element.cc
+++ b/third_party/blink/renderer/core/html/html_ruby_element.cc
@@ -20,7 +20,7 @@
     return new LayoutRubyAsInline(this);
   if (style.Display() == EDisplay::kBlock) {
     UseCounter::Count(GetDocument(), WebFeature::kRubyElementWithDisplayBlock);
-    return LayoutObjectFactory::CreateRubyAsBlock(this, style, legacy);
+    return LayoutObjectFactory::CreateRubyAsBlock(this, legacy);
   }
   return LayoutObject::CreateObject(this, style, legacy);
 }
diff --git a/third_party/blink/renderer/core/html/media/html_media_element.cc b/third_party/blink/renderer/core/html/media/html_media_element.cc
index 079c7fb..2c4bff5 100644
--- a/third_party/blink/renderer/core/html/media/html_media_element.cc
+++ b/third_party/blink/renderer/core/html/media/html_media_element.cc
@@ -59,6 +59,7 @@
 #include "third_party/blink/renderer/core/core_initializer.h"
 #include "third_party/blink/renderer/core/core_probes_inl.h"
 #include "third_party/blink/renderer/core/css/media_list.h"
+#include "third_party/blink/renderer/core/css/style_change_reason.h"
 #include "third_party/blink/renderer/core/dom/attribute.h"
 #include "third_party/blink/renderer/core/dom/dom_exception.h"
 #include "third_party/blink/renderer/core/dom/dom_node_ids.h"
@@ -703,6 +704,10 @@
     DVLOG(2) << "parseAttribute(" << *this
              << ", kSrcAttr, old=" << params.old_value
              << ", new=" << params.new_value << ")";
+    // A change to the src attribute can affect intrinsic size, which in turn
+    // requires a style recalc.
+    SetNeedsStyleRecalc(kLocalStyleChange,
+                        StyleChangeReasonForTracing::FromAttribute(name));
     // Trigger a reload, as long as the 'src' attribute is present.
     if (!params.new_value.IsNull()) {
       ignore_preload_none_ = false;
diff --git a/third_party/blink/renderer/core/inspector/inspector_network_agent.cc b/third_party/blink/renderer/core/inspector/inspector_network_agent.cc
index 0e17a62..7dc28895 100644
--- a/third_party/blink/renderer/core/inspector/inspector_network_agent.cc
+++ b/third_party/blink/renderer/core/inspector/inspector_network_agent.cc
@@ -1016,7 +1016,8 @@
       InspectorPageAgent::ToResourceType(resource_type);
 
   WillSendRequestInternal(loader, fetch_context_url, request,
-                          ResourceResponse(), options, type);
+                          ResourceResponse(), options, type,
+                          base::TimeTicks::Now());
 
   String request_id =
       IdentifiersFactory::RequestId(loader, request.InspectorId());
@@ -1057,7 +1058,8 @@
     const ResourceRequest& request,
     const ResourceResponse& redirect_response,
     const ResourceLoaderOptions& options,
-    InspectorPageAgent::ResourceType type) {
+    InspectorPageAgent::ResourceType type,
+    base::TimeTicks timestamp) {
   String loader_id = IdentifiersFactory::LoaderId(loader);
   String request_id =
       IdentifiersFactory::RequestId(loader, request.InspectorId());
@@ -1127,8 +1129,8 @@
     maybe_frame_id = frame_id;
   GetFrontend()->requestWillBeSent(
       request_id, loader_id, documentURL, std::move(request_info),
-      base::TimeTicks::Now().since_origin().InSecondsF(),
-      base::Time::Now().ToDoubleT(), std::move(initiator_object),
+      timestamp.since_origin().InSecondsF(), base::Time::Now().ToDoubleT(),
+      std::move(initiator_object),
       BuildObjectForResourceResponse(redirect_response), resource_type,
       std::move(maybe_frame_id), request.HasUserGesture());
   if (options.synchronous_policy == SynchronousPolicy::kRequestSynchronously)
@@ -1261,7 +1263,8 @@
     const ResourceResponse& redirect_response,
     const ResourceLoaderOptions& options,
     ResourceType resource_type,
-    RenderBlockingBehavior render_blocking_behavior) {
+    RenderBlockingBehavior render_blocking_behavior,
+    base::TimeTicks timestamp) {
   // Ignore the request initiated internally.
   if (options.initiator_info.name == fetch_initiator_type_names::kInternal)
     return;
@@ -1270,7 +1273,7 @@
       InspectorPageAgent::ToResourceType(resource_type);
 
   WillSendRequestInternal(loader, fetch_context_url, request, redirect_response,
-                          options, type);
+                          options, type, timestamp);
 }
 
 void InspectorNetworkAgent::MarkResourceAsCached(DocumentLoader* loader,
diff --git a/third_party/blink/renderer/core/inspector/inspector_network_agent.h b/third_party/blink/renderer/core/inspector/inspector_network_agent.h
index b4173970..ec74040 100644
--- a/third_party/blink/renderer/core/inspector/inspector_network_agent.h
+++ b/third_party/blink/renderer/core/inspector/inspector_network_agent.h
@@ -108,7 +108,8 @@
                        const ResourceResponse& redirect_response,
                        const ResourceLoaderOptions&,
                        ResourceType,
-                       RenderBlockingBehavior);
+                       RenderBlockingBehavior,
+                       base::TimeTicks timestamp);
   void WillSendNavigationRequest(uint64_t identifier,
                                  DocumentLoader*,
                                  const KURL&,
@@ -272,7 +273,8 @@
                                const ResourceRequest&,
                                const ResourceResponse& redirect_response,
                                const ResourceLoaderOptions&,
-                               InspectorPageAgent::ResourceType);
+                               InspectorPageAgent::ResourceType,
+                               base::TimeTicks timestamp);
 
   bool CanGetResponseBodyBlob(const String& request_id);
   void GetResponseBodyBlob(const String& request_id,
diff --git a/third_party/blink/renderer/core/inspector/inspector_trace_events.cc b/third_party/blink/renderer/core/inspector/inspector_trace_events.cc
index 7603fe7..995ab53e9 100644
--- a/third_party/blink/renderer/core/inspector/inspector_trace_events.cc
+++ b/third_party/blink/renderer/core/inspector/inspector_trace_events.cc
@@ -125,11 +125,16 @@
     const ResourceResponse& redirect_response,
     const ResourceLoaderOptions&,
     ResourceType,
-    RenderBlockingBehavior render_blocking_behavior) {
+    RenderBlockingBehavior render_blocking_behavior,
+    base::TimeTicks timestamp) {
   LocalFrame* frame = loader ? loader->GetFrame() : nullptr;
-  DEVTOOLS_TIMELINE_TRACE_EVENT_INSTANT(
-      "ResourceSendRequest", inspector_send_request_event::Data, loader,
-      request.InspectorId(), frame, request, render_blocking_behavior);
+  TRACE_EVENT_INSTANT_WITH_TIMESTAMP1(
+      "devtools.timeline", "ResourceSendRequest", TRACE_EVENT_SCOPE_THREAD,
+      timestamp, "data", [&](perfetto::TracedValue ctx) {
+        inspector_send_request_event::Data(std::move(ctx), loader,
+                                           request.InspectorId(), frame,
+                                           request, render_blocking_behavior);
+      });
 }
 
 void InspectorTraceEvents::WillSendNavigationRequest(
diff --git a/third_party/blink/renderer/core/inspector/inspector_trace_events.h b/third_party/blink/renderer/core/inspector/inspector_trace_events.h
index 14ffd41..1976e6c 100644
--- a/third_party/blink/renderer/core/inspector/inspector_trace_events.h
+++ b/third_party/blink/renderer/core/inspector/inspector_trace_events.h
@@ -93,7 +93,8 @@
                        const ResourceResponse& redirect_response,
                        const ResourceLoaderOptions&,
                        ResourceType,
-                       RenderBlockingBehavior);
+                       RenderBlockingBehavior,
+                       base::TimeTicks timestamp);
   void WillSendNavigationRequest(uint64_t identifier,
                                  DocumentLoader*,
                                  const KURL&,
diff --git a/third_party/blink/renderer/core/layout/layout_block.cc b/third_party/blink/renderer/core/layout/layout_block.cc
index bddde61..011d045 100644
--- a/third_party/blink/renderer/core/layout/layout_block.cc
+++ b/third_party/blink/renderer/core/layout/layout_block.cc
@@ -2270,11 +2270,11 @@
   parent->UpdateAnonymousChildStyle(nullptr, *new_style);
   LayoutBlock* layout_block;
   if (new_display == EDisplay::kFlex) {
-    layout_block = LayoutObjectFactory::CreateFlexibleBox(parent->GetDocument(),
-                                                          *new_style, legacy);
+    layout_block =
+        LayoutObjectFactory::CreateFlexibleBox(parent->GetDocument(), legacy);
   } else if (new_display == EDisplay::kGrid) {
-    layout_block = LayoutObjectFactory::CreateGrid(parent->GetDocument(),
-                                                   *new_style, legacy);
+    layout_block =
+        LayoutObjectFactory::CreateGrid(parent->GetDocument(), legacy);
   } else {
     DCHECK(new_display == EDisplay::kBlock ||
            new_display == EDisplay::kFlowRoot);
diff --git a/third_party/blink/renderer/core/layout/layout_object.cc b/third_party/blink/renderer/core/layout/layout_object.cc
index 564d550..f53e6b9 100644
--- a/third_party/blink/renderer/core/layout/layout_object.cc
+++ b/third_party/blink/renderer/core/layout/layout_object.cc
@@ -288,38 +288,37 @@
       return LayoutObjectFactory::CreateBlockFlow(*element, style, legacy);
     case EDisplay::kTable:
     case EDisplay::kInlineTable:
-      return LayoutObjectFactory::CreateTable(*element, style, legacy);
+      return LayoutObjectFactory::CreateTable(*element, legacy);
     case EDisplay::kTableRowGroup:
     case EDisplay::kTableHeaderGroup:
     case EDisplay::kTableFooterGroup:
-      return LayoutObjectFactory::CreateTableSection(*element, style, legacy);
+      return LayoutObjectFactory::CreateTableSection(*element, legacy);
     case EDisplay::kTableRow:
-      return LayoutObjectFactory::CreateTableRow(*element, style, legacy);
+      return LayoutObjectFactory::CreateTableRow(*element, legacy);
     case EDisplay::kTableColumnGroup:
     case EDisplay::kTableColumn:
-      return LayoutObjectFactory::CreateTableColumn(*element, style, legacy);
+      return LayoutObjectFactory::CreateTableColumn(*element, legacy);
     case EDisplay::kTableCell:
-      return LayoutObjectFactory::CreateTableCell(*element, style, legacy);
+      return LayoutObjectFactory::CreateTableCell(*element, legacy);
     case EDisplay::kTableCaption:
-      return LayoutObjectFactory::CreateTableCaption(*element, style, legacy);
+      return LayoutObjectFactory::CreateTableCaption(*element, legacy);
     case EDisplay::kWebkitBox:
     case EDisplay::kWebkitInlineBox:
       if (style.IsDeprecatedWebkitBoxWithVerticalLineClamp()) {
-        return LayoutObjectFactory::CreateBlockForLineClamp(*element, style,
-                                                            legacy);
+        return LayoutObjectFactory::CreateBlockForLineClamp(*element, legacy);
       }
-      return LayoutObjectFactory::CreateFlexibleBox(*element, style, legacy);
+      return LayoutObjectFactory::CreateFlexibleBox(*element, legacy);
     case EDisplay::kFlex:
     case EDisplay::kInlineFlex:
       UseCounter::Count(element->GetDocument(), WebFeature::kCSSFlexibleBox);
-      return LayoutObjectFactory::CreateFlexibleBox(*element, style, legacy);
+      return LayoutObjectFactory::CreateFlexibleBox(*element, legacy);
     case EDisplay::kGrid:
     case EDisplay::kInlineGrid:
       UseCounter::Count(element->GetDocument(), WebFeature::kCSSGridLayout);
-      return LayoutObjectFactory::CreateGrid(*element, style, legacy);
+      return LayoutObjectFactory::CreateGrid(*element, legacy);
     case EDisplay::kMath:
     case EDisplay::kBlockMath:
-      return LayoutObjectFactory::CreateMath(*element, style, legacy);
+      return LayoutObjectFactory::CreateMath(*element, legacy);
     case EDisplay::kLayoutCustom:
     case EDisplay::kInlineLayoutCustom:
       DCHECK(RuntimeEnabledFeatures::LayoutNGEnabled());
diff --git a/third_party/blink/renderer/core/layout/layout_object_factory.cc b/third_party/blink/renderer/core/layout/layout_object_factory.cc
index 6906d4f..8cd5dc09 100644
--- a/third_party/blink/renderer/core/layout/layout_object_factory.cc
+++ b/third_party/blink/renderer/core/layout/layout_object_factory.cc
@@ -121,21 +121,18 @@
 // static
 LayoutBlock* LayoutObjectFactory::CreateBlockForLineClamp(
     Node& node,
-    const ComputedStyle& style,
     LegacyLayout legacy) {
   return CreateObject<LayoutBlock, LayoutNGBlockFlow,
                       LayoutDeprecatedFlexibleBox>(node, legacy);
 }
 
 LayoutBlock* LayoutObjectFactory::CreateFlexibleBox(Node& node,
-                                                    const ComputedStyle& style,
                                                     LegacyLayout legacy) {
   return CreateObject<LayoutBlock, LayoutNGFlexibleBox, LayoutFlexibleBox>(
       node, legacy);
 }
 
 LayoutBlock* LayoutObjectFactory::CreateGrid(Node& node,
-                                             const ComputedStyle& style,
                                              LegacyLayout legacy) {
   bool disable_ng_for_type = !RuntimeEnabledFeatures::LayoutNGGridEnabled();
   if (disable_ng_for_type)
@@ -145,7 +142,6 @@
 }
 
 LayoutBlock* LayoutObjectFactory::CreateMath(Node& node,
-                                             const ComputedStyle& style,
                                              LegacyLayout legacy) {
   DCHECK(IsA<MathMLElement>(node));
   DCHECK_NE(legacy, LegacyLayout::kForce);
@@ -183,7 +179,6 @@
 }
 
 LayoutBlock* LayoutObjectFactory::CreateTable(Node& node,
-                                              const ComputedStyle& style,
                                               LegacyLayout legacy) {
   bool disable_ng_for_type = !RuntimeEnabledFeatures::LayoutNGTableEnabled();
   if (disable_ng_for_type)
@@ -194,14 +189,12 @@
 
 LayoutTableCaption* LayoutObjectFactory::CreateTableCaption(
     Node& node,
-    const ComputedStyle& style,
     LegacyLayout legacy) {
   return CreateObject<LayoutTableCaption, LayoutNGTableCaption>(node, legacy);
 }
 
 LayoutBlockFlow* LayoutObjectFactory::CreateTableCell(
     Node& node,
-    const ComputedStyle& style,
     LegacyLayout legacy) {
   if (RuntimeEnabledFeatures::LayoutNGTableEnabled()) {
     return CreateObject<LayoutBlockFlow, LayoutNGTableCell, LayoutTableCell>(
@@ -213,7 +206,6 @@
 }
 
 LayoutBox* LayoutObjectFactory::CreateTableColumn(Node& node,
-                                                  const ComputedStyle& style,
                                                   LegacyLayout legacy) {
   bool disable_ng_for_type = !RuntimeEnabledFeatures::LayoutNGTableEnabled();
   if (disable_ng_for_type)
@@ -223,7 +215,6 @@
 }
 
 LayoutBox* LayoutObjectFactory::CreateTableRow(Node& node,
-                                               const ComputedStyle& style,
                                                LegacyLayout legacy) {
   bool disable_ng_for_type = !RuntimeEnabledFeatures::LayoutNGTableEnabled();
   if (disable_ng_for_type)
@@ -233,7 +224,6 @@
 }
 
 LayoutBox* LayoutObjectFactory::CreateTableSection(Node& node,
-                                                   const ComputedStyle& style,
                                                    LegacyLayout legacy) {
   bool disable_ng_for_type = !RuntimeEnabledFeatures::LayoutNGTableEnabled();
   if (disable_ng_for_type)
@@ -243,13 +233,11 @@
 }
 
 LayoutObject* LayoutObjectFactory::CreateButton(Node& node,
-                                                const ComputedStyle& style,
                                                 LegacyLayout legacy) {
   return CreateObject<LayoutBlock, LayoutNGButton, LayoutButton>(node, legacy);
 }
 
 LayoutBlock* LayoutObjectFactory::CreateFieldset(Node& node,
-                                                 const ComputedStyle& style,
                                                  LegacyLayout legacy) {
   return CreateObject<LayoutBlock, LayoutNGFieldset, LayoutFieldset>(node,
                                                                      legacy);
@@ -257,14 +245,12 @@
 
 LayoutBlockFlow* LayoutObjectFactory::CreateFileUploadControl(
     Node& node,
-    const ComputedStyle& style,
     LegacyLayout legacy) {
   return CreateObject<LayoutBlockFlow, LayoutNGBlockFlow,
                       LayoutFileUploadControl>(node, legacy);
 }
 
 LayoutObject* LayoutObjectFactory::CreateSliderTrack(Node& node,
-                                                     const ComputedStyle& style,
                                                      LegacyLayout legacy) {
   return CreateObject<LayoutBlock, LayoutNGBlockFlow, LayoutSliderTrack>(
       node, legacy);
@@ -272,7 +258,6 @@
 
 LayoutObject* LayoutObjectFactory::CreateTextControlInnerEditor(
     Node& node,
-    const ComputedStyle& style,
     LegacyLayout legacy) {
   return CreateObject<LayoutBlockFlow, LayoutNGTextControlInnerEditor,
                       LayoutTextControlInnerEditor>(node, legacy);
@@ -280,7 +265,6 @@
 
 LayoutObject* LayoutObjectFactory::CreateTextControlMultiLine(
     Node& node,
-    const ComputedStyle& style,
     LegacyLayout legacy) {
   return CreateObject<LayoutBlockFlow, LayoutNGTextControlMultiLine,
                       LayoutTextControlMultiLine>(node, legacy);
@@ -288,7 +272,6 @@
 
 LayoutObject* LayoutObjectFactory::CreateTextControlSingleLine(
     Node& node,
-    const ComputedStyle& style,
     LegacyLayout legacy) {
   return CreateObject<LayoutBlockFlow, LayoutNGTextControlSingleLine,
                       LayoutTextControlSingleLine>(node, legacy);
@@ -329,26 +312,22 @@
 }
 
 LayoutProgress* LayoutObjectFactory::CreateProgress(Node* node,
-                                                    const ComputedStyle& style,
                                                     LegacyLayout legacy) {
   return CreateObject<LayoutProgress, LayoutNGProgress>(*node, legacy);
 }
 
 LayoutRubyAsBlock* LayoutObjectFactory::CreateRubyAsBlock(
     Node* node,
-    const ComputedStyle& style,
     LegacyLayout legacy) {
   return CreateObject<LayoutRubyAsBlock, LayoutNGRubyAsBlock>(*node, legacy);
 }
 
 LayoutObject* LayoutObjectFactory::CreateRubyText(Node* node,
-                                                  const ComputedStyle& style,
                                                   LegacyLayout legacy) {
   return CreateObject<LayoutRubyText, LayoutNGRubyText>(*node, legacy);
 }
 
 LayoutObject* LayoutObjectFactory::CreateSVGText(Node& node,
-                                                 const ComputedStyle& style,
                                                  LegacyLayout legacy) {
   const bool disable_ng_for_type = !RuntimeEnabledFeatures::SVGTextNGEnabled();
   return CreateObject<LayoutBlockFlow, LayoutNGSVGText, LayoutSVGText>(
@@ -376,8 +355,7 @@
                             ? LegacyLayout::kForce
                             : LegacyLayout::kAuto;
 
-  LayoutBlock* new_table =
-      CreateTable(parent.GetDocument(), *new_style, legacy);
+  LayoutBlock* new_table = CreateTable(parent.GetDocument(), legacy);
   new_table->SetDocumentForAnonymous(&parent.GetDocument());
   new_table->SetStyle(std::move(new_style));
   return new_table;
@@ -391,8 +369,7 @@
   LegacyLayout legacy =
       parent.ForceLegacyLayout() ? LegacyLayout::kForce : LegacyLayout::kAuto;
 
-  LayoutBox* new_section =
-      CreateTableSection(parent.GetDocument(), *new_style, legacy);
+  LayoutBox* new_section = CreateTableSection(parent.GetDocument(), legacy);
   new_section->SetDocumentForAnonymous(&parent.GetDocument());
   new_section->SetStyle(std::move(new_style));
   return new_section;
@@ -405,7 +382,7 @@
           parent.StyleRef(), EDisplay::kTableRow);
   LegacyLayout legacy =
       parent.ForceLegacyLayout() ? LegacyLayout::kForce : LegacyLayout::kAuto;
-  LayoutBox* new_row = CreateTableRow(parent.GetDocument(), *new_style, legacy);
+  LayoutBox* new_row = CreateTableRow(parent.GetDocument(), legacy);
   new_row->SetDocumentForAnonymous(&parent.GetDocument());
   new_row->SetStyle(std::move(new_style));
   return new_row;
@@ -418,8 +395,7 @@
           parent.StyleRef(), EDisplay::kTableCell);
   LegacyLayout legacy =
       parent.ForceLegacyLayout() ? LegacyLayout::kForce : LegacyLayout::kAuto;
-  LayoutBlockFlow* new_cell =
-      CreateTableCell(parent.GetDocument(), *new_style, legacy);
+  LayoutBlockFlow* new_cell = CreateTableCell(parent.GetDocument(), legacy);
   new_cell->SetDocumentForAnonymous(&parent.GetDocument());
   new_cell->SetStyle(std::move(new_style));
   return new_cell;
diff --git a/third_party/blink/renderer/core/layout/layout_object_factory.h b/third_party/blink/renderer/core/layout/layout_object_factory.h
index 1c53fd7..e4824706 100644
--- a/third_party/blink/renderer/core/layout/layout_object_factory.h
+++ b/third_party/blink/renderer/core/layout/layout_object_factory.h
@@ -41,50 +41,38 @@
                                           const ComputedStyle&,
                                           LegacyLayout);
   static LayoutBlock* CreateBlockForLineClamp(Node& node,
-                                              const ComputedStyle& style,
                                               LegacyLayout legacy);
   static LayoutBlock* CreateFlexibleBox(Node&,
-                                        const ComputedStyle&,
                                         LegacyLayout);
-  static LayoutBlock* CreateGrid(Node&, const ComputedStyle&, LegacyLayout);
-  static LayoutBlock* CreateMath(Node&, const ComputedStyle&, LegacyLayout);
+  static LayoutBlock* CreateGrid(Node&, LegacyLayout);
+  static LayoutBlock* CreateMath(Node&, LegacyLayout);
   static LayoutObject* CreateListMarker(Node&,
                                         const ComputedStyle&,
                                         LegacyLayout);
-  static LayoutBlock* CreateTable(Node&, const ComputedStyle&, LegacyLayout);
+  static LayoutBlock* CreateTable(Node&, LegacyLayout);
   static LayoutTableCaption* CreateTableCaption(Node&,
-                                                const ComputedStyle&,
                                                 LegacyLayout);
   static LayoutBlockFlow* CreateTableCell(Node&,
-                                          const ComputedStyle&,
                                           LegacyLayout);
   static LayoutBox* CreateTableColumn(Node&,
-                                      const ComputedStyle&,
                                       LegacyLayout);
 
-  static LayoutBox* CreateTableRow(Node&, const ComputedStyle&, LegacyLayout);
+  static LayoutBox* CreateTableRow(Node&, LegacyLayout);
   static LayoutBox* CreateTableSection(Node&,
-                                       const ComputedStyle&,
                                        LegacyLayout);
 
   static LayoutObject* CreateButton(Node& node,
-                                    const ComputedStyle& style,
                                     LegacyLayout legacy);
-  static LayoutBlock* CreateFieldset(Node&, const ComputedStyle&, LegacyLayout);
+  static LayoutBlock* CreateFieldset(Node&, LegacyLayout);
   static LayoutBlockFlow* CreateFileUploadControl(Node& node,
-                                                  const ComputedStyle& style,
                                                   LegacyLayout legacy);
   static LayoutObject* CreateSliderTrack(Node& node,
-                                         const ComputedStyle& style,
                                          LegacyLayout legacy);
   static LayoutObject* CreateTextControlInnerEditor(Node& node,
-                                                    const ComputedStyle& style,
                                                     LegacyLayout legacy);
   static LayoutObject* CreateTextControlMultiLine(Node& node,
-                                                  const ComputedStyle& style,
                                                   LegacyLayout legacy);
   static LayoutObject* CreateTextControlSingleLine(Node& node,
-                                                   const ComputedStyle& style,
                                                    LegacyLayout legacy);
 
   static LayoutText* CreateText(Node*, scoped_refptr<StringImpl>, LegacyLayout);
@@ -94,17 +82,13 @@
                                                 int length,
                                                 LegacyLayout);
   static LayoutProgress* CreateProgress(Node* node,
-                                        const ComputedStyle& style,
                                         LegacyLayout legacy);
   static LayoutRubyAsBlock* CreateRubyAsBlock(Node* node,
-                                              const ComputedStyle& style,
                                               LegacyLayout legacy);
   static LayoutObject* CreateRubyText(Node* node,
-                                      const ComputedStyle& style,
                                       LegacyLayout legacy);
 
   static LayoutObject* CreateSVGText(Node& node,
-                                     const ComputedStyle& style,
                                      LegacyLayout legacy);
 
   static LayoutObject* CreateBR(Node*, LegacyLayout);
diff --git a/third_party/blink/renderer/core/layout/layout_object_factory_test.cc b/third_party/blink/renderer/core/layout/layout_object_factory_test.cc
index d1eedc52..b2141ca 100644
--- a/third_party/blink/renderer/core/layout/layout_object_factory_test.cc
+++ b/third_party/blink/renderer/core/layout/layout_object_factory_test.cc
@@ -35,4 +35,14 @@
     EXPECT_FALSE(layout_object.IsLayoutNGObject());
 }
 
+TEST_P(LayoutObjectFactoryTest, WordBreak) {
+  SetBodyInnerHTML("<wbr id=sample>");
+  const auto& layout_object = *GetLayoutObjectByElementId("sample");
+
+  if (LayoutNGEnabled())
+    EXPECT_TRUE(layout_object.IsLayoutNGObject());
+  else
+    EXPECT_FALSE(layout_object.IsLayoutNGObject());
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/layout/layout_table_cell.cc b/third_party/blink/renderer/core/layout/layout_table_cell.cc
index e30bc902..9b4f0fcc 100644
--- a/third_party/blink/renderer/core/layout/layout_table_cell.cc
+++ b/third_party/blink/renderer/core/layout/layout_table_cell.cc
@@ -1227,7 +1227,7 @@
     scoped_refptr<ComputedStyle> style,
     LegacyLayout legacy) {
   LayoutBlockFlow* layout_object =
-      LayoutObjectFactory::CreateTableCell(*document, *style, legacy);
+      LayoutObjectFactory::CreateTableCell(*document, legacy);
   layout_object->SetDocumentForAnonymous(document);
   layout_object->SetStyle(std::move(style));
   return To<LayoutTableCell>(layout_object);
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.cc b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.cc
index b3a608a..b977ecd 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.cc
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.cc
@@ -2973,6 +2973,12 @@
     // precedence. Returns 'true' if |a| < |b| and 'false' otherwise.
     auto IsBeforeInGridOrder = [&](const GridArea& a,
                                    const GridArea& b) -> bool {
+      // Do not consider items that span tracks for container baselines.
+      if (a.rows.IntegerSpan() > 1 || a.columns.IntegerSpan() > 1 ||
+          b.rows.IntegerSpan() > 1 || b.columns.IntegerSpan() > 1) {
+        return false;
+      }
+
       return (a.rows < b.rows) || (a.rows == b.rows && (a.columns < b.columns));
     };
 
@@ -2994,9 +3000,7 @@
   // Propagate the baseline from the appropriate child.
   // TODO(kschmi): Synthesize baseline from alignment context if no grid items.
   if (!grid_items.IsEmpty()) {
-    if (alignment_baseline &&
-        (!fallback_baseline || alignment_baseline->resolved_position.rows <=
-                                   fallback_baseline->resolved_position.rows)) {
+    if (alignment_baseline) {
       container_builder_.SetBaseline(alignment_baseline->baseline);
     } else {
       DCHECK(fallback_baseline);
diff --git a/third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm_utils.cc b/third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm_utils.cc
index c637297..f30d9417 100644
--- a/third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm_utils.cc
+++ b/third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm_utils.cc
@@ -496,7 +496,8 @@
   builder.SetTableCellBorders(cell_borders);
   builder.SetTableCellAlignmentBaseline(alignment_baseline);
   builder.SetTableCellColumnIndex(column_index);
-  builder.SetIsRestrictedBlockSizeTableCell(is_table_block_size_specified);
+  builder.SetIsRestrictedBlockSizeTableCell(
+      is_table_block_size_specified || !cell_style.LogicalHeight().IsAuto());
   builder.SetIsTableCellHiddenForPaint(is_hidden_for_paint);
   builder.SetIsTableCellWithCollapsedBorders(has_collapsed_borders);
   builder.SetHideTableCellIfEmpty(
diff --git a/third_party/blink/renderer/core/loader/document_load_timing.cc b/third_party/blink/renderer/core/loader/document_load_timing.cc
index 56d4471a..a43e765 100644
--- a/third_party/blink/renderer/core/loader/document_load_timing.cc
+++ b/third_party/blink/renderer/core/loader/document_load_timing.cc
@@ -38,7 +38,10 @@
 namespace blink {
 
 DocumentLoadTiming::DocumentLoadTiming(DocumentLoader& document_loader)
-    : redirect_count_(0),
+    : user_timing_mark_fully_loaded_(absl::nullopt),
+      user_timing_mark_fully_visible_(absl::nullopt),
+      user_timing_mark_interactive_(absl::nullopt),
+      redirect_count_(0),
       has_cross_origin_redirect_(false),
       can_request_from_previous_document_(false),
       clock_(base::DefaultClock::GetInstance()),
diff --git a/third_party/blink/renderer/core/loader/resource_load_observer_for_frame.cc b/third_party/blink/renderer/core/loader/resource_load_observer_for_frame.cc
index 8925c1c..f1dc930 100644
--- a/third_party/blink/renderer/core/loader/resource_load_observer_for_frame.cc
+++ b/third_party/blink/renderer/core/loader/resource_load_observer_for_frame.cc
@@ -91,7 +91,7 @@
       GetProbe(), document_loader_,
       fetcher_properties_->GetFetchClientSettingsObject().GlobalObjectUrl(),
       request, redirect_response, options, resource_type,
-      render_blocking_behavior);
+      render_blocking_behavior, base::TimeTicks::Now());
   if (auto* idleness_detector = frame->GetIdlenessDetector())
     idleness_detector->OnWillSendRequest(document_->Fetcher());
   if (auto* interactive_detector = InteractiveDetector::From(*document_))
diff --git a/third_party/blink/renderer/core/loader/resource_load_observer_for_worker.cc b/third_party/blink/renderer/core/loader/resource_load_observer_for_worker.cc
index 3cd73224..b952abff 100644
--- a/third_party/blink/renderer/core/loader/resource_load_observer_for_worker.cc
+++ b/third_party/blink/renderer/core/loader/resource_load_observer_for_worker.cc
@@ -39,7 +39,7 @@
       probe_, nullptr,
       fetcher_properties_->GetFetchClientSettingsObject().GlobalObjectUrl(),
       request, redirect_response, options, resource_type,
-      render_blocking_behavior);
+      render_blocking_behavior, base::TimeTicks::Now());
 }
 
 void ResourceLoadObserverForWorker::DidChangePriority(
diff --git a/third_party/blink/renderer/core/origin_trials/origin_trial_context.cc b/third_party/blink/renderer/core/origin_trials/origin_trial_context.cc
index 5d411e0..030322d 100644
--- a/third_party/blink/renderer/core/origin_trials/origin_trial_context.cc
+++ b/third_party/blink/renderer/core/origin_trials/origin_trial_context.cc
@@ -440,6 +440,10 @@
       !base::FeatureList::IsEnabled(features::kAppCache)) {
     return false;
   }
+  if (trial_name == "ComputePressure" &&
+      !base::FeatureList::IsEnabled(features::kComputePressure)) {
+    return false;
+  }
   if (trial_name == "FledgeInterestGroupAPI" &&
       !base::FeatureList::IsEnabled(features::kFledgeInterestGroups)) {
     return false;
diff --git a/third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5 b/third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5
index 2ce8e7d..66327a8 100644
--- a/third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5
+++ b/third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5
@@ -308,5 +308,10 @@
       permissions_policy_name: "xr-spatial-tracking",
       depends_on: ["WebXR"],
     },
+    {
+      name: "WindowPlacement",
+      permissions_policy_name: "window-placement",
+      depends_on: ["WindowPlacement"],
+    },
   ],
 }
diff --git a/third_party/blink/renderer/core/probe/core_probes.pidl b/third_party/blink/renderer/core/probe/core_probes.pidl
index 3f58882a..c2cf11a 100644
--- a/third_party/blink/renderer/core/probe/core_probes.pidl
+++ b/third_party/blink/renderer/core/probe/core_probes.pidl
@@ -91,7 +91,7 @@
   void DidBlockRequest(CoreProbeSink*, const ResourceRequest&, DocumentLoader*, const KURL& fetch_context_url, const ResourceLoaderOptions&, ResourceRequestBlockedReason, ResourceType);
   void DidChangeResourcePriority(LocalFrame*, DocumentLoader*, uint64_t identifier, ResourceLoadPriority load_priority);
   void PrepareRequest(CoreProbeSink*, DocumentLoader*, ResourceRequest&, ResourceLoaderOptions&, ResourceType);
-  void WillSendRequest(CoreProbeSink*, DocumentLoader*, const KURL& fetch_context_url, const ResourceRequest&, const ResourceResponse& redirect_response, const ResourceLoaderOptions&, ResourceType, RenderBlockingBehavior);
+  void WillSendRequest(CoreProbeSink*, DocumentLoader*, const KURL& fetch_context_url, const ResourceRequest&, const ResourceResponse& redirect_response, const ResourceLoaderOptions&, ResourceType, RenderBlockingBehavior, base::TimeTicks timestamp);
   void WillSendNavigationRequest(CoreProbeSink*, uint64_t identifier, DocumentLoader*, const KURL&, const AtomicString& http_method, EncodedFormData*);
   void MarkResourceAsCached(LocalFrame*, DocumentLoader*, uint64_t identifier);
   void DidReceiveResourceResponse(CoreProbeSink*, uint64_t identifier, DocumentLoader*, const ResourceResponse&, const Resource*);
diff --git a/third_party/blink/renderer/core/svg/svg_text_element.cc b/third_party/blink/renderer/core/svg/svg_text_element.cc
index e311c65..0e9a2d1 100644
--- a/third_party/blink/renderer/core/svg/svg_text_element.cc
+++ b/third_party/blink/renderer/core/svg/svg_text_element.cc
@@ -29,7 +29,7 @@
 
 LayoutObject* SVGTextElement::CreateLayoutObject(const ComputedStyle& style,
                                                  LegacyLayout legacy) {
-  return LayoutObjectFactory::CreateSVGText(*this, style, legacy);
+  return LayoutObjectFactory::CreateSVGText(*this, legacy);
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/timing/performance_timing.cc b/third_party/blink/renderer/core/timing/performance_timing.cc
index 58a026f6..7838348 100644
--- a/third_party/blink/renderer/core/timing/performance_timing.cc
+++ b/third_party/blink/renderer/core/timing/performance_timing.cc
@@ -666,6 +666,8 @@
 absl::optional<base::TimeDelta> PerformanceTiming::UserTimingMarkFullyLoaded()
     const {
   DocumentLoadTiming* timing = GetDocumentLoadTiming();
+  if (!timing)
+    return absl::nullopt;
 
   return timing->UserTimingMarkFullyLoaded();
 }
@@ -673,6 +675,8 @@
 absl::optional<base::TimeDelta> PerformanceTiming::UserTimingMarkFullyVisible()
     const {
   DocumentLoadTiming* timing = GetDocumentLoadTiming();
+  if (!timing)
+    return absl::nullopt;
 
   return timing->UserTimingMarkFullyVisible();
 }
@@ -680,6 +684,8 @@
 absl::optional<base::TimeDelta> PerformanceTiming::UserTimingMarkInteractive()
     const {
   DocumentLoadTiming* timing = GetDocumentLoadTiming();
+  if (!timing)
+    return absl::nullopt;
 
   return timing->UserTimingMarkInteractive();
 }
diff --git a/third_party/blink/renderer/modules/media_controls/media_controls_impl.cc b/third_party/blink/renderer/modules/media_controls/media_controls_impl.cc
index 6461909..c1b9ba12 100644
--- a/third_party/blink/renderer/modules/media_controls/media_controls_impl.cc
+++ b/third_party/blink/renderer/modules/media_controls/media_controls_impl.cc
@@ -699,6 +699,11 @@
 }
 
 void MediaControlsImpl::UpdateCSSClassFromState() {
+  // Skip CSS class updates when not needed in order to avoid triggering
+  // unnecessary style calculation.
+  if (!MediaElement().ShouldShowControls() && !is_hiding_controls_)
+    return;
+
   const ControlsState state = State();
 
   Vector<String> toAdd;
@@ -959,6 +964,8 @@
 }
 
 void MediaControlsImpl::Hide() {
+  base::AutoReset<bool> auto_reset_hiding_controls(&is_hiding_controls_, true);
+
   panel_->SetIsWanted(false);
   panel_->SetIsDisplayed(false);
 
diff --git a/third_party/blink/renderer/modules/media_controls/media_controls_impl.h b/third_party/blink/renderer/modules/media_controls/media_controls_impl.h
index e64e1ec5e..07e1ad1 100644
--- a/third_party/blink/renderer/modules/media_controls/media_controls_impl.h
+++ b/third_party/blink/renderer/modules/media_controls/media_controls_impl.h
@@ -401,6 +401,12 @@
   bool is_paused_for_scrubbing_ : 1;
   bool is_scrubbing_ = false;
 
+  // When controls are hidden, we defer CSS updates on them in order to avoid
+  // unnecessary style calculation. When controls transition from shown to
+  // hidden, we set this flag to true to ensure that one final style update
+  // takes place in order to eliminate states such as scrubbing.
+  bool is_hiding_controls_ = false;
+
   // Watches the video element for resize and updates media controls as
   // necessary.
   Member<ResizeObserver> resize_observer_;
diff --git a/third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc b/third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc
index b556f28..1b53f8fc 100644
--- a/third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc
+++ b/third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc
@@ -208,6 +208,22 @@
 
   void SimulateOnSeeking() { media_controls_->OnSeeking(); }
   void SimulateOnSeeked() { media_controls_->OnSeeked(); }
+  void SimulateOnWaiting() { media_controls_->OnWaiting(); }
+  void SimulateOnPlaying() { media_controls_->OnPlaying(); }
+
+  void SimulateMediaControlPlaying() {
+    MediaControls().MediaElement().SetReadyState(
+        HTMLMediaElement::kHaveEnoughData);
+    MediaControls().MediaElement().SetNetworkState(
+        WebMediaPlayer::NetworkState::kNetworkStateLoading);
+  }
+
+  void SimulateMediaControlBuffering() {
+    MediaControls().MediaElement().SetReadyState(
+        HTMLMediaElement::kHaveCurrentData);
+    MediaControls().MediaElement().SetNetworkState(
+        WebMediaPlayer::NetworkState::kNetworkStateLoading);
+  }
 
   MediaControlsImpl& MediaControls() { return *media_controls_; }
   MediaControlVolumeSliderElement* VolumeSliderElement() const {
@@ -1293,4 +1309,84 @@
   EXPECT_EQ(30, MediaControls().MediaElement().currentTime());
 }
 
+TEST_F(MediaControlsImplTest, HideControlsDefersStyleCalculationOnPlaying) {
+  MediaControls().MediaElement().SetBooleanAttribute(html_names::kControlsAttr,
+                                                     false);
+  MediaControls().MediaElement().SetSrc("https://example.com/foo.mp4");
+  MediaControls().MediaElement().Play();
+  test::RunPendingTasks();
+
+  Element* panel = GetElementByShadowPseudoId(MediaControls(),
+                                              "-webkit-media-controls-panel");
+  ASSERT_NE(nullptr, panel);
+  EXPECT_FALSE(IsElementVisible(*panel));
+  UpdateAllLifecyclePhasesForTest();
+  Document& document = this->GetDocument();
+  EXPECT_FALSE(document.NeedsLayoutTreeUpdate());
+
+  int old_element_count = document.GetStyleEngine().StyleForElementCount();
+
+  SimulateMediaControlPlaying();
+  SimulateOnPlaying();
+  EXPECT_EQ(MediaControls().State(),
+            MediaControlsImpl::ControlsState::kPlaying);
+
+  // With the controls hidden, playback state change should not trigger style
+  // calculation.
+  EXPECT_FALSE(document.NeedsLayoutTreeUpdate());
+  UpdateAllLifecyclePhasesForTest();
+  int new_element_count = document.GetStyleEngine().StyleForElementCount();
+  EXPECT_EQ(old_element_count, new_element_count);
+
+  MediaControls().MediaElement().SetBooleanAttribute(html_names::kControlsAttr,
+                                                     true);
+  EXPECT_TRUE(IsElementVisible(*panel));
+
+  // Showing the controls should trigger the deferred style calculation.
+  EXPECT_TRUE(document.NeedsLayoutTreeUpdate());
+  UpdateAllLifecyclePhasesForTest();
+  new_element_count = document.GetStyleEngine().StyleForElementCount();
+  EXPECT_LT(old_element_count, new_element_count);
+}
+
+TEST_F(MediaControlsImplTest, HideControlsDefersStyleCalculationOnWaiting) {
+  MediaControls().MediaElement().SetBooleanAttribute(html_names::kControlsAttr,
+                                                     false);
+  MediaControls().MediaElement().SetSrc("https://example.com/foo.mp4");
+  MediaControls().MediaElement().Play();
+  test::RunPendingTasks();
+
+  Element* panel = GetElementByShadowPseudoId(MediaControls(),
+                                              "-webkit-media-controls-panel");
+  ASSERT_NE(nullptr, panel);
+  EXPECT_FALSE(IsElementVisible(*panel));
+  UpdateAllLifecyclePhasesForTest();
+  Document& document = this->GetDocument();
+  EXPECT_FALSE(document.NeedsLayoutTreeUpdate());
+
+  int old_element_count = document.GetStyleEngine().StyleForElementCount();
+
+  SimulateMediaControlBuffering();
+  SimulateOnWaiting();
+  EXPECT_EQ(MediaControls().State(),
+            MediaControlsImpl::ControlsState::kBuffering);
+
+  // With the controls hidden, playback state change should not trigger style
+  // calculation.
+  EXPECT_FALSE(document.NeedsLayoutTreeUpdate());
+  UpdateAllLifecyclePhasesForTest();
+  int new_element_count = document.GetStyleEngine().StyleForElementCount();
+  EXPECT_EQ(old_element_count, new_element_count);
+
+  MediaControls().MediaElement().SetBooleanAttribute(html_names::kControlsAttr,
+                                                     true);
+  EXPECT_TRUE(IsElementVisible(*panel));
+
+  // Showing the controls should trigger the deferred style calculation.
+  EXPECT_TRUE(document.NeedsLayoutTreeUpdate());
+  UpdateAllLifecyclePhasesForTest();
+  new_element_count = document.GetStyleEngine().StyleForElementCount();
+  EXPECT_LT(old_element_count, new_element_count);
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index eb69d7d..14e454de 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -388,6 +388,8 @@
       status: "experimental",
     },
     {
+      // blink::features::kComputePressure is a kill switch for the API. If the
+      // feature is disabled, origin trial tokens are ignored.
       name: "ComputePressure",
       origin_trial_feature_name: "ComputePressure",
       status: "experimental",
diff --git a/third_party/blink/tools/move_blink_source.py b/third_party/blink/tools/move_blink_source.py
deleted file mode 100755
index e3567ee..0000000
--- a/third_party/blink/tools/move_blink_source.py
+++ /dev/null
@@ -1,799 +0,0 @@
-#!/usr/bin/env vpython
-# Copyright 2017 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.
-"""Tool to move Blink source from third_party/WebKit to third_party/blink.
-
-See https://docs.google.com/document/d/1l3aPv1Wx__SpRkdOhvJz8ciEGigNT3wFKv78XiuW0Tw/edit?usp=sharing#heading=h.o225wrxp242h
-for the details.
-"""
-
-import argparse
-import logging
-import os
-import platform
-import re
-import sys
-from functools import partial
-
-sys.path.append(
-    os.path.join(
-        os.path.dirname(__file__), '..', 'renderer', 'build', 'scripts'))
-from blinkbuild.name_style_converter import NameStyleConverter
-from blinkpy.common.checkout.git import Git
-from blinkpy.common.path_finder import get_chromium_src_dir
-from blinkpy.common.path_finder import get_blink_tools_dir
-from blinkpy.common.system.executive import Executive
-from blinkpy.common.system.executive import ScriptError
-from blinkpy.common.system.filesystem import FileSystem
-from blinkpy.common.system.platform_info import PlatformInfo
-from plan_blink_move import plan_blink_move
-from plan_blink_move import relative_dest
-
-_log = logging.getLogger('move_blink_source')
-
-
-class FileType(object):
-    NONE = 0
-    BUILD = 1
-    BLINK_BUILD = 2
-    OWNERS = 3
-    DEPS = 4
-    MOJOM = 5
-    TYPEMAP = 6
-    BLINK_BUILD_PY = 7
-    LAYOUT_TESTS_WITH_MOJOM = 8
-    BLINK_DEPS = 9
-
-    @staticmethod
-    def detect(path):
-        slash_dir, basename = os.path.split(path)
-        slash_dir = slash_dir.replace(os.path.sep, '/')
-        if basename == 'DEPS':
-            if 'third_party/WebKit' in path:
-                return FileType.BLINK_DEPS
-            return FileType.DEPS
-        if basename == 'OWNERS':
-            return FileType.OWNERS
-        if basename.endswith('.mojom'):
-            return FileType.MOJOM
-        if basename.endswith('.typemap'):
-            return FileType.TYPEMAP
-        if basename.endswith(
-                '.py') and 'third_party/WebKit/Source/build' in slash_dir:
-            return FileType.BLINK_BUILD_PY
-        if basename.endswith(('.gn', '.gni')):
-            if 'third_party/WebKit' in path or 'third_party/blink' in slash_dir:
-                return FileType.BLINK_BUILD
-            if 'third_party' in slash_dir:
-                return FileType.NONE
-            return FileType.BUILD
-        if basename.endswith('.html') and re.search(
-                r'third_party/WebKit/LayoutTests/('
-                r'fast/dom/shadow|'
-                r'fast/forms/color|'
-                r'geolocation-api|'
-                r'http/tests/budget|'
-                r'http/tests/credentialmanager|'
-                r'http/tests/security/powerfulFeatureRestrictions|'
-                r'installedapp|'
-                r'media/mediasession|'
-                r'payments|'
-                r'presentation|'
-                r'reporting-observer|'
-                r'webshare)', slash_dir):
-            return FileType.LAYOUT_TESTS_WITH_MOJOM
-        return FileType.NONE
-
-
-class MoveBlinkSource(object):
-    def __init__(self, fs, options, repo_root):
-        self._fs = fs
-        self._platform = PlatformInfo(sys, platform, fs, Executive())
-        self._options = options
-        _log.debug(options)
-        self._repo_root = repo_root
-
-        # The following fields are initialized in _create_basename_maps.
-        self._basename_map = None
-        self._basename_re_list = None
-        self._idl_generated_impl_headers = None
-        # _checked_in_header_re_list is used to distinguish checked-in
-        # header files and generated header files.
-        self._checked_in_header_re_list = None
-
-        self._updated_files = []
-
-    def update(self, apply_only=None):
-        """Updates contents of files affected by Blink source move.
-
-        Args:
-            apply_only: If it's None, updates all affected files. Otherwise,
-                it should be a set of file paths and this function updates
-                only the files in |apply_only|.
-        """
-        _log.info('Planning renaming ...')
-        file_pairs = plan_blink_move(self._fs, [])
-
-        self._create_basename_maps(file_pairs)
-        dirs = self._update_file_content(apply_only)
-
-        # Updates #includes in files in directories with updated DEPS +
-        # third_party/WebKit/{Source,common,public}.
-        self._append_unless_upper_dir_exists(
-            dirs,
-            self._fs.join(self._repo_root, 'third_party', 'WebKit', 'Source'))
-        self._append_unless_upper_dir_exists(
-            dirs,
-            self._fs.join(self._repo_root, 'third_party', 'WebKit', 'common'))
-        self._append_unless_upper_dir_exists(
-            dirs,
-            self._fs.join(self._repo_root, 'third_party', 'WebKit', 'public'))
-        self._append_unless_upper_dir_exists(
-            dirs,
-            self._fs.join(self._repo_root, 'mojo', 'public', 'tools',
-                          'bindings', 'generators', 'cpp_templates'))
-        self._update_cpp_includes_in_directories(dirs, apply_only)
-
-        # Content update for individual files.
-        # The following is a list of tuples.
-        #  Tuple: (<file path relative to repo root>, [replacement commands])
-        #  Command: a callable object, or
-        #           a tuple of (<original string>, <new string>).
-        file_replacement_list = [
-            ('DEPS', [('src/third_party/WebKit/Source/devtools',
-                       'src/third_party/blink/renderer/devtools')]),
-            ('WATCHLISTS',
-             [('third_party/WebKit/Source', 'third_party/blink/renderer'),
-              ('third_party/WebKit/public', 'third_party/blink/public')]),
-            ('build/check_gn_headers_whitelist.txt',
-             [('third_party/WebKit/Source', 'third_party/blink/renderer'),
-              ('third_party/WebKit/public', 'third_party/blink/public'),
-              self._update_basename]),
-            ('chrome/browser/resources/chromeos/accessibility/chromevox/tools/jsbundler.py',
-             [('third_party/WebKit/Source', 'third_party/blink/renderer')]),
-            ('testing/buildbot/gn_isolate_map.pyl',
-             [('third_party/WebKit/Source', 'third_party/blink/renderer')]),
-            ('third_party/WebKit/Source/BUILD.gn',
-             [('$root_gen_dir/third_party/WebKit',
-               '$root_gen_dir/third_party/blink')]),
-            ('third_party/WebKit/Source/config.gni',
-             [('snake_case_source_files = false',
-               'snake_case_source_files = true')]),
-            ('third_party/WebKit/Source/core/css/CSSProperties.json5',
-             [self._update_basename]),
-            ('third_party/WebKit/Source/core/css/ComputedStyleExtraFields.json5',
-             [self._update_basename]),
-            ('third_party/WebKit/Source/core/css/ComputedStyleFieldAliases.json5',
-             [self._update_basename]),
-            ('third_party/WebKit/Source/core/html/parser/create-html-entity-table',
-             [self._update_basename]),
-            ('third_party/WebKit/Source/core/inspector/inspector_protocol_config.json',
-             [self._update_basename]),
-            ('third_party/WebKit/Source/core/probe/CoreProbes.json5',
-             [self._update_basename]),
-            ('third_party/WebKit/Source/core/testing/InternalSettings.h',
-             [('InternalSettingsGenerated.h',
-               'internal_settings_generated.h')]),
-            ('third_party/WebKit/Source/core/testing/Internals.cpp',
-             [('InternalRuntimeFlags.h', 'internal_runtime_flags.h')]),
-            ('third_party/WebKit/Source/platform/probe/PlatformProbes.json5',
-             [self._update_basename]),
-            ('third_party/WebKit/Tools/Scripts/audit-non-blink-usage.py',
-             [('third_party/WebKit/Source', 'third_party/blink/renderer'),
-              ('ls-files third_party/WebKit', 'ls-files third_party/blink')]),
-            ('third_party/WebKit/Tools/Scripts/webkitpy/style/checker.py',
-             [('Source/', 'renderer/')]),
-            ('third_party/WebKit/public/BUILD.gn',
-             [('$root_gen_dir/third_party/WebKit',
-               '$root_gen_dir/third_party/blink')]),
-            ('third_party/WebKit/public/blink_resources.grd',
-             [('../Source/', '../renderer/')]),
-            ('third_party/blink/tools/compile_devtools_frontend.py',
-             [('\'WebKit\', \'Source\'', '\'blink\', \'renderer\'')]),
-            ('tools/android/eclipse/.classpath',
-             [('third_party/WebKit/public', 'third_party/blink/public')]),
-            ('tools/android/loading/cloud/backend/deploy.sh',
-             [('third_party/WebKit/Source', 'third_party/blink/renderer')]),
-            ('tools/android/loading/emulation_unittest.py',
-             [('third_party/WebKit/Source', 'third_party/blink/renderer')]),
-            ('tools/android/loading/options.py',
-             [('third_party/WebKit/Source', 'third_party/blink/renderer')]),
-            ('tools/android/loading/request_track.py',
-             [('third_party/WebKit/Source', 'third_party/blink/renderer')]),
-            ('tools/cfi/blacklist.txt', [('third_party/WebKit/Source',
-                                          'third_party/blink/renderer')]),
-            ('tools/gritsettings/resource_ids',
-             [('third_party/WebKit/public', 'third_party/blink/public'),
-              ('third_party/WebKit/Source', 'third_party/blink/renderer')]),
-            ('tools/include_tracer.py', [('third_party/WebKit/Source',
-                                          'third_party/blink/renderer')]),
-            ('tools/metrics/actions/extract_actions.py',
-             [('third_party/WebKit/Source', 'third_party/blink/renderer')]),
-            ('tools/metrics/histograms/update_editor_commands.py',
-             [('third_party/WebKit/Source/core/editing/EditorCommand.cpp',
-               'third_party/blink/renderer/core/editing/editor_command.cc')]),
-            ('tools/metrics/histograms/update_use_counter_css.py',
-             [('third_party/WebKit/Source/core/frame/UseCounter.cpp',
-               'third_party/blink/renderer/core/frame/use_counter.cc')]),
-            ('tools/metrics/histograms/update_use_counter_feature_enum.py',
-             [('third_party/WebKit/public', 'third_party/blink/public')]),
-        ]
-        for file_path, replacement_list in file_replacement_list:
-            if not apply_only or file_path in apply_only:
-                self._update_single_file_content(
-                    file_path,
-                    replacement_list,
-                    should_write=self._options.run)
-
-        if self._options.run:
-            _log.info('Formatting updated %d files ...',
-                      len(self._updated_files))
-            git = self._create_git()
-            # |git cl format| can't handle too many files at once.
-            while len(self._updated_files) > 0:
-                end_index = 100
-                if end_index > len(self._updated_files):
-                    end_index = len(self._updated_files)
-                git.run(['cl', 'format'] + self._updated_files[:end_index])
-                self._updated_files = self._updated_files[end_index:]
-
-            if not apply_only:
-                _log.info('Make a local commit ...')
-                git.commit_locally_with_message(
-                    """The Great Blink mv for source files, part 1.
-
-Update file contents without moving files.
-
-NOAUTOREVERT=true
-NOPRESUBMIT=true
-NOTREECHECKS=true
-Bug: 768828
-""")
-
-    def move(self, apply_only=None):
-        """Move Blink source files.
-
-        Args:
-            apply_only: If it's None, move all affected files. Otherwise,
-                it should be a set of file paths and this function moves
-                only the files in |apply_only|.
-        """
-        _log.info('Planning renaming ...')
-        file_pairs = plan_blink_move(self._fs, [])
-
-        if apply_only:
-            file_pairs = [
-                (src, dest) for (src, dest) in file_pairs
-                if 'third_party/WebKit/' + src.replace('\\', '/') in apply_only
-            ]
-        _log.info('Will move %d files', len(file_pairs))
-
-        git = self._create_git()
-        files_set = self._get_checked_in_files(git)
-        for i, (src, dest) in enumerate(file_pairs):
-            src_from_repo = self._fs.join('third_party', 'WebKit', src)
-            if src_from_repo.replace('\\', '/') not in files_set:
-                _log.info('%s is not in the repository', src)
-                continue
-            dest_from_repo = self._fs.join('third_party', 'blink', dest)
-            self._fs.maybe_make_directory(self._repo_root, 'third_party',
-                                          'blink', self._fs.dirname(dest))
-            if self._options.run_git:
-                git.move(src_from_repo, dest_from_repo)
-                _log.info('[%d/%d] Git moved %s', i + 1, len(file_pairs), src)
-            else:
-                self._fs.move(
-                    self._fs.join(self._repo_root, src_from_repo),
-                    self._fs.join(self._repo_root, dest_from_repo))
-                _log.info('[%d/%d] Moved %s', i + 1, len(file_pairs), src)
-        if apply_only:
-            return
-
-        self._update_single_file_content('build/get_landmines.py', [(
-            '\ndef main',
-            '  print \'The Great Blink mv for source files (crbug.com/768828)\'\n\ndef main'
-        )])
-
-        _log.info('Run run_bindings_tests.py ...')
-        Executive().run_command([
-            'python',
-            self._fs.join(get_blink_tools_dir(), 'run_bindings_tests.py'),
-            '--reset-results'
-        ],
-                                cwd=self._repo_root)
-
-        if self._options.run_git:
-            _log.info('Make a local commit ...')
-            git.commit_locally_with_message(
-                """The Great Blink mv for source files, part 2.
-
-Move and rename files.
-
-NOAUTOREVERT=true
-NOPRESUBMIT=true
-NOTREECHECKS=true
-Bug: 768828
-""")
-
-    def fix_branch(self):
-        git = self._create_git()
-        status = self._get_local_change_status(git)
-        if len(status) == 0:
-            _log.info('No local changes.')
-            return
-        modified_files = {f for (s, f) in status if s != 'D'}
-        deleted_files = {f for (s, f) in status if s == 'D'}
-
-        self.update(apply_only=modified_files)
-        self.move(apply_only=modified_files)
-        try:
-            git.commit_locally_with_message('This commit should be squashed.')
-        except ScriptError:
-            _log.info('move_blink_source.py modified nothing.')
-
-        # TODO(tkent): Show a message about deleted_files.
-
-    def _get_local_change_status(self, git):
-        """Returns a list of tuples representing local change summary.
-
-        Each tuple contains two strings. The first one is file change status
-        such as "M", "D".  See --diff-filter section of git-diff manual page.
-        The second one is file name relative to the repository top.
-        """
-
-        base_commit = git.run(
-            ['show-branch', '--merge-base', 'master', 'HEAD']).strip()
-        # Note that file names in the following command result are always
-        # slash-separated, even on Windows.
-        status_lines = git.run(
-            ['diff', '--name-status', '--no-renames', base_commit]).split('\n')
-        status_tuple_list = []
-        for l in status_lines:
-            items = l.split('\t')
-            if len(items) == 2:
-                status_tuple_list.append(tuple(items))
-            elif len(l) > 0:
-                _log.warning('Unrecognized diff output: "%s"', l)
-        return status_tuple_list
-
-    def _get_checked_in_files(self, git):
-        files_text = git.run([
-            'ls-files', 'third_party/WebKit/Source',
-            'third_party/WebKit/common', 'third_party/WebKit/public'
-        ])
-        return set(files_text.split('\n'))
-
-    def _create_basename_maps(self, file_pairs):
-        basename_map = {}
-        basenames = []
-        idl_headers = set()
-        headers = []
-        for source, dest in file_pairs:
-            _, source_base = self._fs.split(source)
-            _, dest_base = self._fs.split(dest)
-            # OriginTrialFeaturesForCore.h in bindings/tests/results/modules/
-            # confuses generated/checked-in detection in _replace_include_path().
-            if 'bindings/tests' in source.replace('\\', '/'):
-                continue
-            if source_base.endswith('.h'):
-                headers.append(re.escape(source_base))
-            if source_base == dest_base:
-                continue
-            basename_map[source_base] = dest_base
-            basenames.append(re.escape(source_base))
-            # IDL sometimes generates implementation files as well as
-            # binding files. We'd like to update #includes for such files.
-            if source_base.endswith('.idl'):
-                source_header = source_base.replace('.idl', '.h')
-                basename_map[source_header] = dest_base.replace('.idl', '.h')
-                basenames.append(re.escape(source_header))
-                idl_headers.add(source_header)
-            elif source_base.endswith('.proto'):
-                source_header = source_base.replace('.proto', '.pb.h')
-                basename_map[source_header] = dest_base.replace(
-                    '.proto', '.pb.h')
-                basenames.append(re.escape(source_header))
-        _log.debug('Rename %d files for snake_case', len(basename_map))
-        self._basename_map = basename_map
-        self._idl_generated_impl_headers = idl_headers
-
-        self._basename_re_list = []
-        self._checked_in_header_re_list = []
-        # Split file names into some chunks to avoid "Regular expression
-        # code size limit exceeded" on Windows
-        CHUNK_SIZE = 700
-        # Generated inspector/protocol/* contains a lot of names duplicated with
-        # checked-in core files. We don't want to rename them, and don't want to
-        # replace them in BUILD.gn and #include accidentally.
-        RE_PREFIX = r'(?<!inspector/protocol/)'
-
-        while len(basenames) > 0:
-            end_index = min(CHUNK_SIZE, len(basenames))
-            self._basename_re_list.append(
-                re.compile(RE_PREFIX + r'\b(' +
-                           '|'.join(basenames[0:end_index]) + ')(?=["\']|$)'))
-            basenames = basenames[end_index:]
-
-        while len(headers) > 0:
-            end_index = min(CHUNK_SIZE, len(headers))
-            self._checked_in_header_re_list.append(
-                re.compile(RE_PREFIX + r'\b(' +
-                           '|'.join(headers[0:end_index]) + ')$'))
-            headers = headers[end_index:]
-
-    def _shorten_path(self, path):
-        if path.startswith(self._repo_root):
-            return path[len(self._repo_root) + 1:]
-        return path
-
-    @staticmethod
-    def _filter_file(fs, dirname, basename):
-        return FileType.detect(fs.join(dirname, basename)) != FileType.NONE
-
-    def _update_build(self, content):
-        content = content.replace('//third_party/WebKit/Source',
-                                  '//third_party/blink/renderer')
-        content = content.replace('//third_party/WebKit/common',
-                                  '//third_party/blink/common')
-        content = content.replace('//third_party/WebKit/public',
-                                  '//third_party/blink/public')
-        # export_header_blink exists outside of Blink too.
-        content = content.replace(
-            'export_header_blink = "third_party/WebKit/public/platform/WebCommon.h"',
-            'export_header_blink = "third_party/blink/public/platform/web_common.h"'
-        )
-        content = content.replace('$root_gen_dir/blink/public',
-                                  '$root_gen_dir/third_party/blink/public')
-        content = content.replace('$root_gen_dir/blink',
-                                  '$root_gen_dir/third_party/blink/renderer')
-        return content
-
-    def _update_blink_build(self, content):
-        content = self._update_build(content)
-
-        # Update visibility=[...]
-        content = content.replace('//third_party/WebKit/*',
-                                  '//third_party/blink/*')
-        content = content.replace('//third_party/WebKit/Source/*',
-                                  '//third_party/blink/renderer/*')
-        content = content.replace('//third_party/WebKit/public/*',
-                                  '//third_party/blink/public/*')
-
-        # Update mojom variables
-        content = content.replace('export_header = "third_party/WebKit/common',
-                                  'export_header = "third_party/blink/common')
-        content = content.replace(
-            'export_header_blink = "third_party/WebKit/Source',
-            'export_header_blink = "third_party/blink/renderer')
-
-        # Update buildflag_header() rules
-        content = content.replace('header_dir = "blink/',
-                                  'header_dir = "third_party/blink/renderer/')
-
-        return self._update_basename(content)
-
-    def _update_owners(self, content):
-        content = content.replace('//third_party/WebKit/Source',
-                                  '//third_party/blink/renderer')
-        content = content.replace('//third_party/WebKit/common',
-                                  '//third_party/blink/common')
-        content = content.replace('//third_party/WebKit/public',
-                                  '//third_party/blink/public')
-        return content
-
-    def _update_deps(self, content):
-        original_content = content
-        content = content.replace('third_party/WebKit/Source',
-                                  'third_party/blink/renderer')
-        content = content.replace('third_party/WebKit/common',
-                                  'third_party/blink/common')
-        content = content.replace('third_party/WebKit/public',
-                                  'third_party/blink/public')
-        content = content.replace('third_party/WebKit', 'third_party/blink')
-        if original_content == content:
-            return content
-        return self._update_basename(content)
-
-    def _update_blink_deps(self, content):
-        original_content = content
-        content = re.sub('(?<=[-+!])public', 'third_party/blink/public',
-                         content)
-        content = re.sub(
-            '(?<=[-+!])(bindings|controller|core|modules|platform)',
-            'third_party/blink/renderer/\\1', content)
-        content = content.replace('third_party/WebKit', 'third_party/blink')
-        if original_content == content:
-            return content
-        return self._update_basename(content)
-
-    def _update_mojom(self, content):
-        content = content.replace('third_party/WebKit/public',
-                                  'third_party/blink/public')
-        content = content.replace('third_party/WebKit/common',
-                                  'third_party/blink/common')
-        return content
-
-    def _update_typemap(self, content):
-        content = content.replace('//third_party/WebKit/Source',
-                                  '//third_party/blink/renderer')
-        content = content.replace('//third_party/WebKit/common',
-                                  '//third_party/blink/common')
-        content = content.replace('//third_party/WebKit/public',
-                                  '//third_party/blink/public')
-        return self._update_basename(content)
-
-    def _update_blink_build_py(self, content):
-        # We don't prepend 'third_party/blink/renderer/' to matched basenames
-        # because it won't affect build and manual update after the great mv is
-        # enough.
-        return self._update_basename(content)
-
-    def _update_layout_tests(self, content):
-        return content.replace('file:///gen/third_party/WebKit/public/',
-                               'file:///gen/third_party/blink/public/')
-
-    def _update_basename(self, content):
-        for regex in self._basename_re_list:
-            content = regex.sub(
-                lambda match: self._basename_map[match.group(1)], content)
-        return content
-
-    @staticmethod
-    def _append_unless_upper_dir_exists(dirs, new_dir):
-        for i in range(0, len(dirs)):
-            if new_dir.startswith(dirs[i]):
-                return
-            if dirs[i].startswith(new_dir):
-                dirs[i] = new_dir
-                return
-        dirs.append(new_dir)
-
-    def _update_file_content(self, apply_only):
-        _log.info('Find *.gn, *.mojom, *.py, *.typemap, DEPS, and OWNERS ...')
-        files = self._fs.files_under(
-            self._repo_root,
-            dirs_to_skip=['.git', 'out'],
-            file_filter=self._filter_file)
-        _log.info('Scan contents of %d files ...', len(files))
-        updated_deps_dirs = []
-        for file_path in files:
-            file_type = FileType.detect(file_path)
-            original_content = self._fs.read_text_file(file_path)
-            content = original_content
-            if file_type == FileType.BUILD:
-                content = self._update_build(content)
-            elif file_type == FileType.BLINK_BUILD:
-                content = self._update_blink_build(content)
-            elif file_type == FileType.OWNERS:
-                content = self._update_owners(content)
-            elif file_type == FileType.DEPS:
-                if self._fs.dirname(file_path) == self._repo_root:
-                    _log.debug("Skip //DEPS")
-                    continue
-                content = self._update_deps(content)
-            elif file_type == FileType.BLINK_DEPS:
-                content = self._update_blink_deps(content)
-            elif file_type == FileType.MOJOM:
-                content = self._update_mojom(content)
-            elif file_type == FileType.TYPEMAP:
-                content = self._update_typemap(content)
-            elif file_type == FileType.BLINK_BUILD_PY:
-                content = self._update_blink_build_py(content)
-            elif file_type == FileType.LAYOUT_TESTS_WITH_MOJOM:
-                content = self._update_layout_tests(content)
-
-            if original_content == content:
-                continue
-            if self._options.run and (not apply_only or file_path.replace(
-                    '\\', '/') in apply_only):
-                self._fs.write_text_file(file_path, content)
-                self._updated_files.append(file_path)
-                _log.info('Updated %s', self._shorten_path(file_path))
-            if file_type == FileType.DEPS:
-                self._append_unless_upper_dir_exists(
-                    updated_deps_dirs, self._fs.dirname(file_path))
-        return updated_deps_dirs
-
-    def _update_cpp_includes_in_directories(self, dirs, apply_only):
-        for dirname in dirs:
-            _log.info('Processing #include in %s ...',
-                      self._shorten_path(dirname))
-            files = self._fs.files_under(
-                dirname,
-                file_filter=
-                lambda fs, _, basename: basename.endswith(('.h', '.cc', '.cpp', '.mm', '.cc.tmpl', '.cpp.tmpl', '.h.tmpl', 'xpath_grammar.y', '.gperf'))
-            )
-            for file_path in files:
-                posix_file_path = file_path.replace('\\', '/')
-                if '/third_party/WebKit/Source/bindings/tests/results/' in posix_file_path:
-                    continue
-                original_content = self._fs.read_text_file(file_path)
-
-                content = self._update_cpp_includes(original_content)
-                if file_path.endswith(
-                        '.h'
-                ) and '/third_party/WebKit/public/' in posix_file_path:
-                    content = self._update_basename_only_includes(
-                        content, file_path)
-                if file_path.endswith(
-                        '.h') and '/third_party/WebKit/' in posix_file_path:
-                    content = self._update_include_guard(content, file_path)
-
-                if original_content == content:
-                    continue
-                if self._options.run and (not apply_only
-                                          or posix_file_path in apply_only):
-                    self._fs.write_text_file(file_path, content)
-                    self._updated_files.append(file_path)
-                    _log.info('Updated %s', self._shorten_path(file_path))
-
-    def _replace_include_path(self, match):
-        include_or_import = match.group(1)
-        path = match.group(2)
-
-        # If |path| starts with 'blink/public/resources', we should prepend
-        # 'third_party/'.
-        #
-        # If |path| starts with 'third_party/WebKit', we should adjust the
-        # directory name for third_party/blink, and replace its basename by
-        # self._basename_map.
-        #
-        # If |path| starts with a Blink-internal directory such as bindings,
-        # core, modules, platform, public, it refers to a checked-in file, or a
-        # generated file. For the former, we should add 'third_party/blink/' and
-        # replace the basename.  For the latter, we should update the basename
-        # for a name mapped from an IDL renaming, and should add
-        # 'third_party/blink/'.
-
-        if path.startswith('blink/public/resources'):
-            path = path.replace('blink/public', 'third_party/blink/public')
-            return '#%s "%s"' % (include_or_import, path)
-
-        if path.startswith('third_party/WebKit'):
-            path = path.replace('third_party/WebKit/Source',
-                                'third_party/blink/renderer')
-            path = path.replace('third_party/WebKit/common',
-                                'third_party/blink/common')
-            path = path.replace('third_party/WebKit/public',
-                                'third_party/blink/public')
-            path = self._update_basename(path)
-            return '#%s "%s"' % (include_or_import, path)
-
-        match = None
-        for regex in self._checked_in_header_re_list:
-            match = regex.search(path)
-            if match:
-                break
-        if match:
-            if match.group(1) in self._basename_map:
-                path = path[:match.start(1)] + self._basename_map[match.group(
-                    1)]
-        elif 'core/inspector/protocol/' not in path:
-            basename_start = path.rfind('/') + 1
-            basename = path[basename_start:]
-            if basename in self._idl_generated_impl_headers:
-                path = path[:basename_start] + self._basename_map[basename]
-            elif basename.startswith('V8'):
-                path = path[:basename_start] + NameStyleConverter(
-                    basename[:len(basename) - 2]).to_snake_case() + '.h'
-        if path.startswith('public'):
-            path = 'third_party/blink/' + path
-        else:
-            path = 'third_party/blink/renderer/' + path
-        return '#%s "%s"' % (include_or_import, path)
-
-    def _update_cpp_includes(self, content):
-        pattern = re.compile(
-            r'#(include|import)\s+"((bindings|controller|core|modules|platform|public|'
-            +
-            r'third_party/WebKit/(Source|common|public)|blink/public/resources)/[-_\w/.]+)"'
-        )
-        return pattern.sub(self._replace_include_path, content)
-
-    def _replace_basename_only_include(self, subdir, source_path, match):
-        source_basename = match.group(1)
-        if source_basename in self._basename_map:
-            return '#include "third_party/blink/public/%s/%s"' % (
-                subdir, self._basename_map[source_basename])
-        _log.warning('Basename-only %s in %s', match.group(0),
-                     self._shorten_path(source_path))
-        return match.group(0)
-
-    def _update_basename_only_includes(self, content, source_path):
-        if not source_path.endswith(
-                '.h'
-        ) or '/third_party/WebKit/public/' not in source_path.replace(
-                '\\', '/'):
-            return
-        # In public/ header files, we should replace |#include "WebFoo.h"|
-        # with |#include "third_party/blink/public/platform-or-web/web_foo.h"|
-        subdir = self._fs.basename(self._fs.dirname(source_path))
-        # subdir is 'web' or 'platform'.
-        return re.sub(
-            r'#include\s+"(\w+\.h)"',
-            partial(self._replace_basename_only_include, subdir, source_path),
-            content)
-
-    def _update_include_guard(self, content, source_path):
-        current_guard = re.sub(r'[-.]', '_', self._fs.basename(source_path))
-        new_path = relative_dest(
-            self._fs,
-            self._fs.relpath(
-                source_path,
-                start=self._fs.join(self._repo_root, 'third_party', 'WebKit')))
-        new_guard = 'THIRD_PARTY_BLINK_' + re.sub(r'[-\\/.]', '_',
-                                                  new_path.upper()) + '_'
-        content = re.sub(r'#ifndef\s+(WTF_)?' + current_guard,
-                         '#ifndef ' + new_guard, content)
-        content = re.sub(r'#define\s+(WTF_)?' + current_guard,
-                         '#define ' + new_guard, content)
-        content = re.sub(r'#endif\s+//\s+(WTF_)?' + current_guard,
-                         '#endif  // ' + new_guard, content)
-        return content
-
-    def _update_single_file_content(self,
-                                    file_path,
-                                    replace_list,
-                                    should_write=True):
-        full_path = self._fs.join(self._repo_root, file_path)
-        original_content = self._fs.read_text_file(full_path)
-        content = original_content
-        for command in replace_list:
-            if isinstance(command, tuple):
-                src, dest = command
-                content = content.replace(src, dest)
-            elif callable(command):
-                content = command(content)
-            else:
-                raise TypeError('A tuple or a function is expected.')
-        if content != original_content:
-            if should_write:
-                self._fs.write_text_file(full_path, content)
-                self._updated_files.append(full_path)
-                _log.info('Updated %s', file_path)
-        else:
-            _log.warning('%s does not contain specified source strings.',
-                         file_path)
-
-    def _create_git(self):
-        return Git(
-            cwd=self._repo_root, filesystem=self._fs, platform=self._platform)
-
-
-def main():
-    logging.basicConfig(
-        level=logging.INFO,
-        format='[%(asctime)s %(levelname)s %(name)s] %(message)s',
-        datefmt='%H:%M:%S')
-    parser = argparse.ArgumentParser(description='Blink source mover')
-    sub_parsers = parser.add_subparsers()
-
-    update_parser = sub_parsers.add_parser('update')
-    update_parser.set_defaults(command='update')
-    update_parser.add_argument(
-        '--run', dest='run', action='store_true', help='Update file contents')
-
-    move_parser = sub_parsers.add_parser('move')
-    move_parser.set_defaults(command='move')
-    move_parser.add_argument(
-        '--git',
-        dest='run_git',
-        action='store_true',
-        help='Run |git mv| command instead of |mv|.')
-
-    fixbranch_parser = sub_parsers.add_parser('fixbranch')
-    fixbranch_parser.set_defaults(command='fixbranch', run=True, run_git=True)
-
-    options = parser.parse_args()
-    mover = MoveBlinkSource(FileSystem(), options, get_chromium_src_dir())
-    if options.command == 'update':
-        mover.update()
-    elif options.command == 'move':
-        mover.move()
-    elif options.command == 'fixbranch':
-        mover.fix_branch()
-
-
-if __name__ == '__main__':
-    main()
diff --git a/third_party/blink/tools/plan_blink_move.py b/third_party/blink/tools/plan_blink_move.py
deleted file mode 100755
index 3de4469..0000000
--- a/third_party/blink/tools/plan_blink_move.py
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/usr/bin/env vpython
-# Copyright 2017 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 os
-import re
-import sys
-
-sys.path.append(
-    os.path.join(
-        os.path.dirname(__file__), '..', 'renderer', 'build', 'scripts'))
-from blinkbuild.name_style_converter import NameStyleConverter
-from blinkpy.common.system.filesystem import FileSystem
-
-
-def relative_dest(fs, filename):
-    """Returns a destination path string for given filename.
-
-    |filename| is a path relative to third_party/WebKit, and the resultant path
-    is relative to third_party/blink.
-    """
-    dest = None
-    if filename.startswith('Source'):
-        dest = re.sub(r'^Source', 'renderer', filename)
-    elif filename.startswith('common') or filename.startswith('public'):
-        dest = filename
-    else:
-        raise ValueError(
-            '|filename| must start with "common", "public", or "Source": %s' %
-            filename)
-    if filename.endswith(('.h', '.cpp', '.mm', '.idl', '.typemap', '.proto',
-                          'Settings.json5')):
-        dirname, basename = fs.split(dest)
-        basename, ext = fs.splitext(basename)
-        # Skip some inspector-related files. #includes for these files are
-        # generated by a script outside of Blink.
-        if (re.match(r'Inspector.*Agent', basename)
-                or basename.startswith('AdTracker')
-                or basename == 'InspectorTraceEvents'
-                or basename == 'PerformanceMonitor'
-                or basename == 'PlatformTraceEventsAgent'):
-            return dest
-        if filename.endswith('.cpp'):
-            ext = '.cc'
-        # WebKit.h should be renamed to blink.h.
-        if basename == 'WebKit' and ext == '.h':
-            basename = 'blink'
-        if basename.lower() != basename:
-            basename = NameStyleConverter(basename).to_snake_case()
-        return fs.join(dirname, basename + ext)
-    return dest
-
-
-def start_with_list(name, prefixes):
-    if len(prefixes) == 0:
-        return True
-    for prefix in prefixes:
-        if name.startswith(prefix):
-            return True
-    return False
-
-
-def plan_blink_move(fs, prefixes):
-    """Returns (source, dest) path pairs.
-
-    The source paths are relative to third_party/WebKit,
-    and the dest paths are relative to third_party/blink.
-    The paths use os.sep as the path part separator.
-    """
-    blink_dir = fs.join(fs.dirname(__file__), '..')
-    webkit_dir = fs.join(blink_dir, '..', '..', 'third_party', 'WebKit')
-    source_files = fs.files_under(fs.join(webkit_dir, 'Source'))
-    source_files += fs.files_under(fs.join(webkit_dir, 'common'))
-    source_files += fs.files_under(fs.join(webkit_dir, 'public'))
-
-    # It's possible to check git.exists() here, but we don't do it due to slow
-    # performance. We should check it just before executing git command.
-
-    source_files = [f[len(webkit_dir) + 1:] for f in source_files]
-    return [(f, relative_dest(fs, f)) for f in source_files
-            if f.find('node_modules') == -1 and start_with_list(f, prefixes)]
-
-
-def main():
-    fs = FileSystem()
-    file_pairs = plan_blink_move(fs, sys.argv[1:])
-    print 'Show renaming plan. It contains files not in the repository.'
-    print '<Source path relative to third_party/WebKit> => <Destination path relative to third_party/blink>'
-    for pair in file_pairs:
-        print '%s\t=>\t%s' % pair
-
-
-if __name__ == '__main__':
-    main()
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 18d9070..2cae442 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -4008,6 +4008,7 @@
 crbug.com/1045599 external/wpt/css/css-grid/alignment/grid-alignment-implies-size-change-031.html [ Failure ]
 crbug.com/1045599 external/wpt/css/css-grid/alignment/grid-alignment-implies-size-change-035.html [ Failure ]
 crbug.com/1045599 external/wpt/css/css-grid/alignment/grid-alignment-implies-size-change-036.html [ Failure ]
+crbug.com/1045599 external/wpt/css/css-grid/alignment/grid-baseline-004.html [ Failure ]
 crbug.com/941987 external/wpt/css/css-grid/alignment/grid-baseline-align-cycles-001.html [ Failure ]
 crbug.com/1045599 external/wpt/css/css-grid/alignment/grid-self-alignment-positioned-items-with-margin-border-padding-016.html [ Failure ]
 crbug.com/1045599 external/wpt/css/css-grid/alignment/replaced-alignment-with-aspect-ratio-001.html [ Failure ]
@@ -4115,6 +4116,7 @@
 virtual/layout-ng-grid/external/wpt/css/css-grid/alignment/grid-alignment-implies-size-change-031.html [ Pass ]
 virtual/layout-ng-grid/external/wpt/css/css-grid/alignment/grid-alignment-implies-size-change-035.html [ Pass ]
 virtual/layout-ng-grid/external/wpt/css/css-grid/alignment/grid-alignment-implies-size-change-036.html [ Pass ]
+virtual/layout-ng-grid/external/wpt/css/css-grid/alignment/grid-baseline-004.html [ Pass ]
 virtual/layout-ng-grid/external/wpt/css/css-grid/alignment/grid-baseline-align-cycles-001.html [ Pass ]
 virtual/layout-ng-grid/external/wpt/css/css-grid/alignment/replaced-alignment-with-aspect-ratio-001.html [ Pass ]
 virtual/layout-ng-grid/external/wpt/css/css-grid/alignment/replaced-alignment-with-aspect-ratio-002.html [ Pass ]
@@ -4148,7 +4150,6 @@
 
 ### Tests failing with LayoutNGGrid enabled:
 crbug.com/1045599 virtual/layout-ng-grid/external/wpt/css/css-grid/abspos/grid-abspos-staticpos-align-self-safe-001.html [ Failure ]
-crbug.com/1045599 virtual/layout-ng-grid/external/wpt/css/css-grid/alignment/grid-baseline-004.html [ Failure ]
 crbug.com/1045599 virtual/layout-ng-grid/external/wpt/css/css-grid/alignment/grid-baseline-align-001.html [ Failure ]
 crbug.com/1045599 virtual/layout-ng-grid/external/wpt/css/css-grid/alignment/grid-baseline-justify-001.html [ Failure ]
 crbug.com/1045599 virtual/layout-ng-grid/external/wpt/css/css-grid/alignment/grid-column-axis-self-baseline-synthesized-001.html [ Failure ]
@@ -6942,7 +6943,6 @@
 # Failing css-transforms-2 web platform tests.
 crbug.com/753080 external/wpt/css/css-transforms/transform3d-sorting-002.html [ Failure ]
 crbug.com/753080 external/wpt/css/css-transforms/transform3d-sorting-006.html [ Failure ]
-crbug.com/753080 external/wpt/css/css-transforms/ttwf-css-3d-polygon-cycle.html [ Failure ]
 
 external/wpt/css/css-transforms/2d-rotate-001.html [ Failure ]
 external/wpt/css/css-transforms/css-skew-002.html [ Failure ]
@@ -7095,3 +7095,8 @@
 external/wpt/css/css-transforms/ttwf-transform-skewy-001.html [ Failure ]
 external/wpt/css/css-transforms/ttwf-transform-translatex-001.html [ Failure ]
 
+# Sheriff 2021-05-18
+crbug.com/1210658 [ Mac10.14 ] virtual/jxl-enabled/images/jxl/jxl-images.html [ Failure ]
+crbug.com/1210658 [ Mac10.15 ] virtual/jxl-enabled/images/jxl/jxl-images.html [ Failure ]
+crbug.com/1210658 [ Win ] virtual/jxl-enabled/images/jxl/jxl-images.html [ Failure ]
+
diff --git a/third_party/blink/web_tests/external/wpt/css/css-grid/alignment/grid-baseline-004-expected.txt b/third_party/blink/web_tests/external/wpt/css/css-grid/alignment/grid-baseline-004-expected.txt
deleted file mode 100644
index 7d26c42..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-grid/alignment/grid-baseline-004-expected.txt
+++ /dev/null
@@ -1,75 +0,0 @@
-This is a testharness.js-based test.
-PASS .grid, container 1
-PASS .grid, container 2
-PASS .grid, container 3
-PASS .grid, container 4
-PASS .grid, container 5
-PASS .grid, container 6
-PASS .grid, container 7
-PASS .grid, container 8
-PASS .grid, container 9
-PASS .grid, container 10
-PASS .grid, container 11
-PASS .grid, container 12
-PASS .grid, container 13
-PASS .grid, container 14
-PASS .grid, container 15
-FAIL .grid, container 16 assert_equals: 
-<div class="container" data-expected-width="480" data-expected-height="250">
-    <div id="first" class="grid twoRows" data-offset-x="0" data-offset-y="80">
-        <div class="firstRowFirstColumn target" data-offset-x="0" data-offset-y="0"></div>
-        <div class="firstRowBothColumn" data-offset-x="0" data-offset-y="15"></div>
-        <div class="bothRowFirstColumn" data-offset-x="0" data-offset-y="10"></div>
-    </div>
-    <div id="second" class="grid threeRows" data-offset-x="160" data-offset-y="55">
-        <div class="thirdRowFirstColumn" data-offset-x="0" data-offset-y="100"></div>
-        <div class="secondRowBothColumn" data-offset-x="0" data-offset-y="65"></div>
-        <div class="secondRowSecondColumn style3 alignSelfBaseline target" id="first" data-offset-x="0" data-offset-y="80"></div>
-    </div>
-    <div class="grid empty threeRows" data-offset-x="320" data-offset-y="15">
-        <div class="thirdRowSecondColumn" data-offset-x="50" data-offset-y="110"></div>
-        <div class="secondRowFirstColumn target" data-offset-x="0" data-offset-y="60"></div>
-        <div class="secondRowBothColumn" data-offset-x="0" data-offset-y="75"></div>
-    </div>
-</div>
-height expected 250 but got 300
-FAIL .grid, container 17 assert_equals: 
-<div class="container" data-expected-width="480" data-expected-height="250">
-    <div id="first" class="grid twoRows" data-offset-x="0" data-offset-y="80">
-        <div class="firstRowFirstColumn target" data-offset-x="0" data-offset-y="0"></div>
-        <div class="firstRowBothColumn" data-offset-x="0" data-offset-y="15"></div>
-        <div class="bothRowFirstColumn" data-offset-x="0" data-offset-y="10"></div>
-    </div>
-    <div id="second" class="grid threeRows" data-offset-x="160" data-offset-y="55">
-        <div class="thirdRowFirstColumn" data-offset-x="0" data-offset-y="100"></div>
-        <div class="secondRowBothColumn" data-offset-x="0" data-offset-y="65"></div>
-        <div class="secondRowSecondColumn style3 alignSelfBaseline target" id="first" data-offset-x="0" data-offset-y="80"></div>
-    </div>
-    <div class="grid empty threeRows" data-offset-x="320" data-offset-y="15">
-        <div class="thirdRowSecondColumn" data-offset-x="50" data-offset-y="110"></div>
-        <div class="secondRowFirstColumn target" data-offset-x="0" data-offset-y="60"></div>
-        <div class="secondRowBothColumn" data-offset-x="0" data-offset-y="75"></div>
-    </div>
-</div>
-height expected 250 but got 300
-FAIL .grid, container 18 assert_equals: 
-<div class="container" data-expected-width="480" data-expected-height="250">
-    <div id="first" class="grid twoRows" data-offset-x="0" data-offset-y="80">
-        <div class="firstRowFirstColumn target" data-offset-x="0" data-offset-y="0"></div>
-        <div class="firstRowBothColumn" data-offset-x="0" data-offset-y="15"></div>
-        <div class="bothRowFirstColumn" data-offset-x="0" data-offset-y="10"></div>
-    </div>
-    <div id="second" class="grid threeRows" data-offset-x="160" data-offset-y="55">
-        <div class="thirdRowFirstColumn" data-offset-x="0" data-offset-y="100"></div>
-        <div class="secondRowBothColumn" data-offset-x="0" data-offset-y="65"></div>
-        <div class="secondRowSecondColumn style3 alignSelfBaseline target" id="first" data-offset-x="0" data-offset-y="80"></div>
-    </div>
-    <div class="grid empty threeRows" data-offset-x="320" data-offset-y="15">
-        <div class="thirdRowSecondColumn" data-offset-x="50" data-offset-y="110"></div>
-        <div class="secondRowFirstColumn target" data-offset-x="0" data-offset-y="60"></div>
-        <div class="secondRowBothColumn" data-offset-x="0" data-offset-y="75"></div>
-    </div>
-</div>
-height expected 250 but got 300
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/external/wpt/css/css-grid/alignment/grid-baseline-004.html b/third_party/blink/web_tests/external/wpt/css/css-grid/alignment/grid-baseline-004.html
index 7c3e46c..cd565a10 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-grid/alignment/grid-baseline-004.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-grid/alignment/grid-baseline-004.html
@@ -210,7 +210,7 @@
     <div id=second class="grid threeRows" data-offset-x="160" data-offset-y="55">
         <div class="thirdRowFirstColumn" data-offset-x="0" data-offset-y="100"></div>
         <div class="secondRowBothColumn" data-offset-x="0" data-offset-y="65"></div>
-        <div class="secondRowSecondColumn style3 alignSelfBaseline target" id="first"  data-offset-x="0"   data-offset-y="80"></div>
+        <div class="secondRowSecondColumn style3 alignSelfBaseline target" id="first"  data-offset-x="50"   data-offset-y="60"></div>
     </div>
     <div class="grid empty threeRows" data-offset-x="320" data-offset-y="15">
         <div class="thirdRowSecondColumn"  data-offset-x="50"   data-offset-y="110"></div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-tables/table-cell-child-overflow-measure-ref.html b/third_party/blink/web_tests/external/wpt/css/css-tables/table-cell-child-overflow-measure-ref.html
new file mode 100644
index 0000000..d4e3ec6
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-tables/table-cell-child-overflow-measure-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<div style="width: 100px; height: 100px; background: green;">
+  <div style="overflow: auto; height: 100%;">
+     <div style="height: 200px;"></div>
+  </div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-tables/table-cell-child-overflow-measure.html b/third_party/blink/web_tests/external/wpt/css/css-tables/table-cell-child-overflow-measure.html
new file mode 100644
index 0000000..c092d0c1
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-tables/table-cell-child-overflow-measure.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1210436">
+<link rel="match" href="table-cell-child-overflow-measure-ref.html">
+<div style="width: 100px; height: 100px; display: table-cell; background: green;">
+  <div style="overflow: auto; height: 100%;">
+     <div style="height: 200px;"></div>
+  </div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-transforms/transform3d-sorting-006.html b/third_party/blink/web_tests/external/wpt/css/css-transforms/transform3d-sorting-006.html
index d40edf0..966549a 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-transforms/transform3d-sorting-006.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-transforms/transform3d-sorting-006.html
@@ -12,9 +12,9 @@
   <body>
     <div style="transform-style: preserve-3d">
       <div style="height: 100px; width: 100px; background: red;
-        transform: rotateX(45deg) rotateY(45deg)"></div>
+        transform: rotateX(-45deg) rotateY(-45deg)"></div>
       <div style="height: 100px; width: 100px; background: green;
-        transform: translateY(-100px) rotateX(45deg) rotateY(-45deg)"></div>
+        transform: translateY(-100px) rotateX(-45deg) rotateY(45deg)"></div>
     </div>
   </body>
 </html>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-transforms/ttwf-css-3d-polygon-cycle.html b/third_party/blink/web_tests/external/wpt/css/css-transforms/ttwf-css-3d-polygon-cycle.html
index ec33e7d..bd18d20f 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-transforms/ttwf-css-3d-polygon-cycle.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-transforms/ttwf-css-3d-polygon-cycle.html
@@ -2,48 +2,51 @@
 <html>
 <!-- Submitted from TestTWF Paris -->
 <head>
-    <title>CSS Transforms Test: 3d transform polygon cycle</title>
-    <link rel="author" title="Leo Ziegler" href="mailto:leo.ziegler@gmail.com">
-    <link rel="help" href="http://www.w3.org/TR/css-transforms-2/#3d-transform-rendering">
-    <!-- See also: http://http://en.wikipedia.org/wiki/Newell's_algorithm -->
-    <link rel="match" href="reference/ttwf-css-3d-polygon-cycle-ref.html">
-    <meta name="flags" content="svg">
-    <meta name="assert" content="The red, green and blue rectangles are forming a cycle, which should be detected and rendered using Newell Algorithm's.">
-    <style type="text/css">
-        #container {
-        	position: absolute;
-        	top: 100px;
-        	left: 100px;
-        }
-        .rect {
-        	position: absolute;
-        }
-        #red {
-        	background-color: red;
-        	width: 200px;
-        	height: 50px;
-        	transform: rotateY(20deg);
-        }
-        #green {
-        	background-color: green;
-        	width: 50px;
-        	height: 200px;
-        	transform: rotateX(20deg);
-        }
-        #blue {
-        	background-color: blue;
-        	width: 50px;
-        	height: 200px;
-        	transform: rotate(45deg) translate(50px, -50px) rotateX(-20deg);
-        }
-    </style>
+  <title>CSS Transforms Test: 3d transform polygon cycle</title>
+  <link rel="author" title="Leo Ziegler" href="mailto:leo.ziegler@gmail.com">
+  <link rel="help" href="http://www.w3.org/TR/css-transforms-2/#3d-transform-rendering">
+  <!-- See also: http://http://en.wikipedia.org/wiki/Newell's_algorithm -->
+  <link rel="match" href="reference/ttwf-css-3d-polygon-cycle-ref.html">
+  <meta name=fuzzy content="0-104;0-610">
+  <meta name="flags" content="svg">
+  <meta name="assert"
+    content="The red, green and blue rectangles are forming a cycle, which should be detected and rendered using Newell Algorithm's.">
+  <style type="text/css">
+    #container {
+      position: absolute;
+      top: 100px;
+      left: 100px;
+      transform-style: preserve-3d;
+    }
+    .rect {
+      position: absolute;
+    }
+    #red {
+      background-color: red;
+      width: 200px;
+      height: 50px;
+      transform: rotateY(20deg);
+    }
+    #green {
+      background-color: green;
+      width: 50px;
+      height: 200px;
+      transform: rotateX(20deg);
+    }
+    #blue {
+      background-color: blue;
+      width: 50px;
+      height: 200px;
+      transform: rotate(45deg) translate(50px, -50px) rotateX(-20deg);
+    }
+  </style>
 </head>
 <body>
-    <p>The test passes if there red is over green, green is over blue and blue is over red.</p>
-    <div id="container">
-    	<div class="rect" id="red"></div>
-    	<div class="rect" id="green"></div>
-    	<div class="rect" id="blue"></div>
-    </div>
+  <p>The test passes if there red is over green, green is over blue and blue is over red.</p>
+  <div id="container">
+    <div class="rect" id="red"></div>
+    <div class="rect" id="green"></div>
+    <div class="rect" id="blue"></div>
+  </div>
 </body>
 </html>
diff --git a/third_party/blink/web_tests/external/wpt/dom/events/keypress-dispatch-crash.html b/third_party/blink/web_tests/external/wpt/dom/events/keypress-dispatch-crash.html
new file mode 100644
index 0000000..3207adbd
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/dom/events/keypress-dispatch-crash.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<link rel="author" title="Robert Flack" href="mailto:flackr@chromium.org">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1209098">
+
+<!-- No crash should occur if a keypress is dispatched to a constructed document. -->
+
+<script>
+var newDoc = document.implementation.createDocument( "", null);
+var testNode = newDoc.createElement('div');
+newDoc.append(testNode);
+
+var syntheticEvent = document.createEvent('KeyboardEvents');
+syntheticEvent.initKeyboardEvent("keypress");
+testNode.dispatchEvent(syntheticEvent)
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/media-source/mediasource-is-type-supported.html b/third_party/blink/web_tests/external/wpt/media-source/mediasource-is-type-supported.html
index f7c6bdc2..1981af7 100644
--- a/third_party/blink/web_tests/external/wpt/media-source/mediasource-is-type-supported.html
+++ b/third_party/blink/web_tests/external/wpt/media-source/mediasource-is-type-supported.html
@@ -53,6 +53,19 @@
               'video/webm;codecs="mp4a.40.2"',
           ], false, 'Test invalid mismatch between MIME type and codec ID');
 
+          // Note that, though the user agent might support some subset of
+          // these for progressive non-MSE playback, the MSE mpeg audio
+          // bytestream format specification requires there to be no codecs
+          // parameter.
+          test_type_support([
+              'audio/mpeg;codecs="mp3"',
+              'audio/mpeg;codecs="mp4a.69"',
+              'audio/mpeg;codecs="mp4a.6B"',
+              'audio/aac;codecs="aac"',
+              'audio/aac;codecs="adts"',
+              'audio/aac;codecs="mp4a.40"',
+          ], false, 'Test invalid inclusion of codecs parameter for mpeg audio types');
+
           test_type_support([
               'audio/mp4;codecs="mp4a"',
               'audio/mp4;codecs="mp4a.40"',
diff --git a/third_party/blink/web_tests/flag-specific/composite-after-paint/compositing/video/video-poster-expected.txt b/third_party/blink/web_tests/flag-specific/composite-after-paint/compositing/video/video-poster-expected.txt
index 357b992..541d030b 100644
--- a/third_party/blink/web_tests/flag-specific/composite-after-paint/compositing/video/video-poster-expected.txt
+++ b/third_party/blink/web_tests/flag-specific/composite-after-paint/compositing/video/video-poster-expected.txt
@@ -42,7 +42,7 @@
       "transform": 1
     },
     {
-      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-ready state-scrubbing'",
+      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-pre-ready state-no-source'",
       "bounds": [352, 288],
       "contentsOpaqueForText": true,
       "drawsContent": false,
diff --git a/third_party/blink/web_tests/flag-specific/composite-after-paint/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt b/third_party/blink/web_tests/flag-specific/composite-after-paint/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
index de0c785..196e805 100644
--- a/third_party/blink/web_tests/flag-specific/composite-after-paint/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
+++ b/third_party/blink/web_tests/flag-specific/composite-after-paint/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
@@ -58,7 +58,7 @@
       "transform": 1
     },
     {
-      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-ready state-stopped'",
+      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-pre-ready state-no-source'",
       "bounds": [150, 60],
       "contentsOpaqueForText": true,
       "drawsContent": false,
diff --git a/third_party/blink/web_tests/flag-specific/composite-after-paint/virtual/android/fullscreen/video-overlay-scroll-expected.txt b/third_party/blink/web_tests/flag-specific/composite-after-paint/virtual/android/fullscreen/video-overlay-scroll-expected.txt
index ac02836a..6c7e441 100644
--- a/third_party/blink/web_tests/flag-specific/composite-after-paint/virtual/android/fullscreen/video-overlay-scroll-expected.txt
+++ b/third_party/blink/web_tests/flag-specific/composite-after-paint/virtual/android/fullscreen/video-overlay-scroll-expected.txt
@@ -1,7 +1,7 @@
 {
   "layers": [
     {
-      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='phase-pre-ready state-no-source sizing-small use-default-poster'",
+      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-pre-ready state-no-source use-default-poster'",
       "bounds": [800, 600],
       "contentsOpaque": true,
       "backgroundColor": "#333333"
diff --git a/third_party/blink/web_tests/flag-specific/disable-layout-ng/compositing/video/video-poster-expected.txt b/third_party/blink/web_tests/flag-specific/disable-layout-ng/compositing/video/video-poster-expected.txt
index a5df5fd..e1f57d4 100644
--- a/third_party/blink/web_tests/flag-specific/disable-layout-ng/compositing/video/video-poster-expected.txt
+++ b/third_party/blink/web_tests/flag-specific/disable-layout-ng/compositing/video/video-poster-expected.txt
@@ -48,7 +48,7 @@
       "transform": 1
     },
     {
-      "name": "LayoutFlexibleBox (relative positioned) DIV class='sizing-small phase-ready state-scrubbing'",
+      "name": "LayoutFlexibleBox (relative positioned) DIV class='sizing-small phase-pre-ready state-no-source'",
       "bounds": [352, 288],
       "transform": 1
     },
diff --git a/third_party/blink/web_tests/flag-specific/disable-layout-ng/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt b/third_party/blink/web_tests/flag-specific/disable-layout-ng/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
index 5d5413de..2071ac90 100644
--- a/third_party/blink/web_tests/flag-specific/disable-layout-ng/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
+++ b/third_party/blink/web_tests/flag-specific/disable-layout-ng/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
@@ -64,7 +64,7 @@
       "transform": 1
     },
     {
-      "name": "LayoutFlexibleBox (relative positioned) DIV class='sizing-small phase-ready state-stopped'",
+      "name": "LayoutFlexibleBox (relative positioned) DIV class='sizing-small phase-pre-ready state-no-source'",
       "bounds": [150, 60],
       "transform": 1
     },
diff --git a/third_party/blink/web_tests/flag-specific/disable-layout-ng/virtual/android/fullscreen/video-overlay-scroll-expected.txt b/third_party/blink/web_tests/flag-specific/disable-layout-ng/virtual/android/fullscreen/video-overlay-scroll-expected.txt
index 2a21432..a3dbe67 100644
--- a/third_party/blink/web_tests/flag-specific/disable-layout-ng/virtual/android/fullscreen/video-overlay-scroll-expected.txt
+++ b/third_party/blink/web_tests/flag-specific/disable-layout-ng/virtual/android/fullscreen/video-overlay-scroll-expected.txt
@@ -6,7 +6,7 @@
       "drawsContent": false
     },
     {
-      "name": "LayoutFlexibleBox (relative positioned) DIV class='phase-pre-ready state-no-source sizing-small use-default-poster'",
+      "name": "LayoutFlexibleBox (relative positioned) DIV class='sizing-small phase-pre-ready state-no-source use-default-poster'",
       "bounds": [800, 600],
       "contentsOpaque": true,
       "backgroundColor": "#333333"
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/resources/basic.html b/third_party/blink/web_tests/http/tests/inspector-protocol/resources/basic.html
new file mode 100644
index 0000000..6e3dc7b
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/resources/basic.html
@@ -0,0 +1,9 @@
+<html>
+  <head>
+    <link rel="stylesheet" href="inspector-protocol/resources/style.css"></script>
+  </head>
+  <body>
+    <script src="inspector-protocol/resources/empty.js"></script>
+    <img src="inspector-protocol/resources/square.png">
+  </body>
+</html>
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/resources/tracing-test.js b/third_party/blink/web_tests/http/tests/inspector-protocol/resources/tracing-test.js
new file mode 100644
index 0000000..2642270
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/resources/tracing-test.js
@@ -0,0 +1,121 @@
+// Copyright 2014 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.
+
+(class TracingHelper {
+  constructor(testRunner, session) {
+    this._testRunner = testRunner;
+    this._session = session;
+  }
+
+  startTracing(categories="-*,disabled-by-default-devtools.timeline,devtools.timeline") {
+    return this.startTracingWithArguments({ "categories": categories, "type": "", "options": "" });
+  }
+
+  startTracingAndSaveAsStream() {
+    var args = {
+      "categories": "-*,disabled-by-default-devtools.timeline,devtools.timeline",
+      "type": "",
+      "options": "",
+      "transferMode": "ReturnAsStream"
+    };
+    return this.startTracingWithArguments(args);
+  }
+
+  async startTracingWithArguments(args) {
+    await this._session.protocol.Tracing.start(args);
+    this._testRunner.log("Recording started");
+  }
+
+  async stopTracing(filter_re=/devtools.timeline/) {
+    var devtoolsEvents = [];
+
+    function dataCollected(reply) {
+      var allEvents = reply.params.value;
+      var filteredEvents = allEvents.filter(e => filter_re.test(e.cat));
+      devtoolsEvents = devtoolsEvents.concat(filteredEvents);
+    };
+
+    this._session.protocol.Tracing.onDataCollected(dataCollected);
+    this._session.protocol.Tracing.end();
+    await this._session.protocol.Tracing.onceTracingComplete();
+    this._testRunner.log("Tracing complete");
+    this._session.protocol.Tracing.offDataCollected(dataCollected);
+    this._devtoolsEvents = devtoolsEvents;
+    return devtoolsEvents;
+  }
+
+  async stopTracingAndReturnStream() {
+    function dataCollected() {
+      this._testRunner.log(
+        "FAIL: dataCollected event should not be fired when returning trace as stream.");
+    }
+
+    this._session.protocol.Tracing.onDataCollected(dataCollected);
+    this._session.protocol.Tracing.end();
+    var event = await this._session.protocol.Tracing.onceTracingComplete();
+    this._testRunner.log("Tracing complete");
+    this._session.protocol.Tracing.offDataCollected(dataCollected);
+    return event.params.stream;
+  }
+
+  retrieveStream(streamHandle, offset, chunkSize) {
+    var callback;
+    var promise = new Promise(f => callback = f);
+    var result = "";
+    var had_eof = false;
+
+    var readArguments = { handle: streamHandle };
+    if (typeof chunkSize === "number")
+      readArguments.size = chunkSize;
+    var firstReadArguments = JSON.parse(JSON.stringify(readArguments));
+    if (typeof offset === "number")
+      firstReadArguments.offset = offset;
+    this._session.protocol.IO.read(firstReadArguments).then(message => onChunkRead.call(this, message.result));
+    // Assure multiple in-flight reads are fine (also, save on latencies).
+    this._session.protocol.IO.read(readArguments).then(message => onChunkRead.call(this, message.result));
+    return promise;
+
+    function onChunkRead(response) {
+      if (had_eof)
+        return;
+      result += response.data;
+      if (response.eof) {
+        // Ignore stray callbacks from proactive read requests.
+        had_eof = true;
+        if (response.base64Encoded)
+          result = atob(result);
+        callback(result);
+        return;
+      }
+      this._session.protocol.IO.read(readArguments).then(message => onChunkRead.call(this, message.result));
+    }
+  }
+
+  findEvents(name, ph, condition) {
+    return this._devtoolsEvents.filter(e => e.name === name && e.ph === ph && (!condition || condition(e)));
+  }
+
+  findEvent(name, ph, condition) {
+    var events = this.findEvents(name, ph, condition);
+    if (events.length)
+      return events[0];
+    throw new Error("Couldn't find event " + name + " / " + ph + "\n\n in " + JSON.stringify(this._devtoolsEvents, null, 2));
+  }
+
+  filterEvents(callback) {
+    return this._devtoolsEvents.filter(callback);
+  }
+
+  async invokeAsyncWithTracing(performActions) {
+    await this.startTracing();
+    var data = await this._session.evaluateAsync(`(${performActions.toString()})()`);
+    await this.stopTracing();
+    return data;
+  }
+
+  formattedEvents() {
+    var formattedEvents = this._devtoolsEvents.map(e => e.name + (e.args.data ? '(' + e.args.data.type + ')' : ''));
+    return JSON.stringify(formattedEvents, null, 2);
+  }
+})
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/timeline/request-queueing-time-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/timeline/request-queueing-time-expected.txt
new file mode 100644
index 0000000..8cb902a
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/timeline/request-queueing-time-expected.txt
@@ -0,0 +1,8 @@
+Tests requests queueing time consistency beetween the Network and Tracing domains.
+Recording started
+Tracing complete
+Queueing times for URL http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol/resources/empty.js match: true
+Queueing times for URL http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol/resources/square.png match: true
+Queueing times for URL http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol/resources/style.css match: true
+Queueing times for URL http://127.0.0.1:8000/inspector-protocol/resources/basic.html match: true
+
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/timeline/request-queueing-time.js b/third_party/blink/web_tests/http/tests/inspector-protocol/timeline/request-queueing-time.js
new file mode 100644
index 0000000..b70b321
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/timeline/request-queueing-time.js
@@ -0,0 +1,72 @@
+(async function(testRunner) {
+
+  const {session, dp} = await testRunner.startBlank('Tests requests queueing time consistency beetween the Network and Tracing domains.');
+  const resourceSendRequest = 'ResourceSendRequest';
+  const resourceWillSendRequest = 'ResourceWillSendRequest';
+  // Request count = basic.html + 3 resources.
+  const requestCount = 4;
+
+  const TracingHelper = await testRunner.loadScript('../resources/tracing-test.js');
+  const tracingHelper = new TracingHelper(testRunner, session);
+
+  await tracingHelper.startTracing();
+  dp.Network.enable();
+
+  const requestsFromNetorkDomain = new Map();
+
+  dp.Network.onRequestWillBeSent(event => {
+    requestsFromNetorkDomain.set(
+      event.params.requestId,
+      {
+        url: event.params.request.url,
+        // Timestamp from the network domain arrives in seconds.
+        // We convert it to microseconds to compare it against
+        // data coming from the tracing domain.
+        timestamp: Math.round(event.params.timestamp * 1000 * 1000)
+      });
+  });
+
+  dp.Page.navigate({url: 'http://127.0.0.1:8000/inspector-protocol/resources/basic.html'});
+
+  // Wait for traces to show up.
+  for (let i = 0; i < requestCount; ++i) {
+    await dp.Network.onceRequestWillBeSent();
+  }
+
+  const devtoolsEvents = await tracingHelper.stopTracing();
+
+  const requestsFromTracingDomain = new Map();
+  for (const event of devtoolsEvents) {
+    if (event.name !==  resourceSendRequest && event.name !== resourceWillSendRequest) {
+      continue;
+    }
+    const requestId = event.args.data.requestId;
+    const cachedEventInfo = requestsFromTracingDomain.get(requestId);
+
+    // If a request is initiated by the browser process the start time is marked by a
+    // "ResourceWillSendRequest" record. If, instead, the request is initiated by the
+    // renderer, the time is marked by "ResourceSendRequest" one. In the first case,
+    // it is possible that a "ResourceSendRequest" is issued as  well for the request
+    // and thus it must be discarded in favor of the time marked by the
+    // "ResourceWillSendRequest" record. In the latter case, no
+    // "ResourceWillSendRequest" record is issued for the request.
+    const mustOverwrite = cachedEventInfo && cachedEventInfo.name === resourceSendRequest && event.name === resourceWillSendRequest;
+    if (!cachedEventInfo || mustOverwrite) {
+      // Timestamp from tracing domain arrives in microseconds.
+      requestsFromTracingDomain.set(requestId, {name: event.name, timestamp: event.ts, url: event.args.data.url} );
+    }
+  }
+
+  const sortedEvents = [...requestsFromTracingDomain.entries()].sort((entry1, entry2) => {
+    const url1 = entry1[1].url;
+    const url2 = entry2[1].url;
+    return url1.localeCompare(url2);
+  });
+
+  for (const [requestId, request] of sortedEvents) {
+    const networkEvent = requestsFromNetorkDomain.get(requestId);
+    testRunner.log(`Queueing times for URL ${networkEvent.url} match: ${networkEvent.timestamp === request.timestamp}`);
+  }
+
+  testRunner.completeTest();
+})
diff --git a/third_party/blink/web_tests/platform/linux/external/wpt/url/failure-expected.txt b/third_party/blink/web_tests/platform/linux/external/wpt/url/failure-expected.txt
index 4fcfc8a..e1d541eb 100644
--- a/third_party/blink/web_tests/platform/linux/external/wpt/url/failure-expected.txt
+++ b/third_party/blink/web_tests/platform/linux/external/wpt/url/failure-expected.txt
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 433 tests; 169 PASS, 264 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 393 tests; 166 PASS, 227 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS URL's constructor's base argument: file://example:1/ should throw
 FAIL URL's href: file://example:1/ should throw assert_throws_js: function "() => url.href = test.input" did not throw
@@ -39,10 +39,6 @@
 PASS window.open(): http://foo:-80/ should throw
 PASS URL's constructor's base argument: http:/:@/www.example.com should throw
 FAIL URL's href: http:/:@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:/:@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:/:@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:/:@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:/:@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http://user@/www.example.com should throw
 FAIL URL's href: http://user@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
 PASS XHR: http://user@/www.example.com should throw
@@ -51,16 +47,8 @@
 PASS window.open(): http://user@/www.example.com should throw
 PASS URL's constructor's base argument: http:@/www.example.com should throw
 FAIL URL's href: http:@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http:/@/www.example.com should throw
 FAIL URL's href: http:/@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:/@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:/@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:/@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:/@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http://@/www.example.com should throw
 FAIL URL's href: http://@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
 PASS XHR: http://@/www.example.com should throw
@@ -69,22 +57,10 @@
 PASS window.open(): http://@/www.example.com should throw
 PASS URL's constructor's base argument: https:@/www.example.com should throw
 FAIL URL's href: https:@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-PASS XHR: https:@/www.example.com should throw
-PASS sendBeacon(): https:@/www.example.com should throw
-FAIL Location's href: https:@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" threw object "SyntaxError: Failed to set the 'href' property on 'Location': 'https:@/www.example.com' is not a valid URL." ("SyntaxError") expected instance of function "function TypeError() { [native code] }" ("TypeError")
-PASS window.open(): https:@/www.example.com should throw
 PASS URL's constructor's base argument: http:a:b@/www.example.com should throw
 FAIL URL's href: http:a:b@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:a:b@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:a:b@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:a:b@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:a:b@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http:/a:b@/www.example.com should throw
 FAIL URL's href: http:/a:b@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:/a:b@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:/a:b@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:/a:b@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:/a:b@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http://a:b@/www.example.com should throw
 FAIL URL's href: http://a:b@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
 PASS XHR: http://a:b@/www.example.com should throw
@@ -93,10 +69,6 @@
 PASS window.open(): http://a:b@/www.example.com should throw
 PASS URL's constructor's base argument: http::@/www.example.com should throw
 FAIL URL's href: http::@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http::@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http::@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http::@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http::@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http:@:www.example.com should throw
 FAIL URL's href: http:@:www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
 FAIL XHR: http:@:www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
@@ -399,22 +371,10 @@
 PASS window.open(): http://[::127.0.0.0.1] should throw
 PASS URL's constructor's base argument: a should throw
 FAIL URL's href: a should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: a should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): a should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: a should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): a should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: a/ should throw
 FAIL URL's href: a/ should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: a/ should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): a/ should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: a/ should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): a/ should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: a// should throw
 FAIL URL's href: a// should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: a// should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): a// should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: a// should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): a// should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: file://­/p should throw
 FAIL URL's href: file://­/p should throw assert_throws_js: function "() => url.href = test.input" did not throw
 PASS XHR: file://­/p should throw
diff --git a/third_party/blink/web_tests/platform/linux/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt b/third_party/blink/web_tests/platform/linux/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
index 6bb81bc..8d43831 100644
--- a/third_party/blink/web_tests/platform/linux/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
+++ b/third_party/blink/web_tests/platform/linux/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
@@ -64,7 +64,7 @@
       "transform": 1
     },
     {
-      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-ready state-stopped'",
+      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-pre-ready state-no-source'",
       "bounds": [150, 60],
       "transform": 1
     },
diff --git a/third_party/blink/web_tests/platform/mac/compositing/video/video-poster-expected.txt b/third_party/blink/web_tests/platform/mac/compositing/video/video-poster-expected.txt
index a3780a1..80930a2 100644
--- a/third_party/blink/web_tests/platform/mac/compositing/video/video-poster-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/compositing/video/video-poster-expected.txt
@@ -48,7 +48,7 @@
       "transform": 1
     },
     {
-      "name": "LayoutFlexibleBox (relative positioned) DIV class='sizing-small phase-ready state-scrubbing'",
+      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-pre-ready state-no-source'",
       "bounds": [352, 288],
       "transform": 1
     },
diff --git a/third_party/blink/web_tests/platform/mac/external/wpt/url/failure-expected.txt b/third_party/blink/web_tests/platform/mac/external/wpt/url/failure-expected.txt
index 4fcfc8a..e1d541eb 100644
--- a/third_party/blink/web_tests/platform/mac/external/wpt/url/failure-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/external/wpt/url/failure-expected.txt
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 433 tests; 169 PASS, 264 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 393 tests; 166 PASS, 227 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS URL's constructor's base argument: file://example:1/ should throw
 FAIL URL's href: file://example:1/ should throw assert_throws_js: function "() => url.href = test.input" did not throw
@@ -39,10 +39,6 @@
 PASS window.open(): http://foo:-80/ should throw
 PASS URL's constructor's base argument: http:/:@/www.example.com should throw
 FAIL URL's href: http:/:@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:/:@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:/:@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:/:@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:/:@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http://user@/www.example.com should throw
 FAIL URL's href: http://user@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
 PASS XHR: http://user@/www.example.com should throw
@@ -51,16 +47,8 @@
 PASS window.open(): http://user@/www.example.com should throw
 PASS URL's constructor's base argument: http:@/www.example.com should throw
 FAIL URL's href: http:@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http:/@/www.example.com should throw
 FAIL URL's href: http:/@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:/@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:/@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:/@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:/@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http://@/www.example.com should throw
 FAIL URL's href: http://@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
 PASS XHR: http://@/www.example.com should throw
@@ -69,22 +57,10 @@
 PASS window.open(): http://@/www.example.com should throw
 PASS URL's constructor's base argument: https:@/www.example.com should throw
 FAIL URL's href: https:@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-PASS XHR: https:@/www.example.com should throw
-PASS sendBeacon(): https:@/www.example.com should throw
-FAIL Location's href: https:@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" threw object "SyntaxError: Failed to set the 'href' property on 'Location': 'https:@/www.example.com' is not a valid URL." ("SyntaxError") expected instance of function "function TypeError() { [native code] }" ("TypeError")
-PASS window.open(): https:@/www.example.com should throw
 PASS URL's constructor's base argument: http:a:b@/www.example.com should throw
 FAIL URL's href: http:a:b@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:a:b@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:a:b@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:a:b@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:a:b@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http:/a:b@/www.example.com should throw
 FAIL URL's href: http:/a:b@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:/a:b@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:/a:b@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:/a:b@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:/a:b@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http://a:b@/www.example.com should throw
 FAIL URL's href: http://a:b@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
 PASS XHR: http://a:b@/www.example.com should throw
@@ -93,10 +69,6 @@
 PASS window.open(): http://a:b@/www.example.com should throw
 PASS URL's constructor's base argument: http::@/www.example.com should throw
 FAIL URL's href: http::@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http::@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http::@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http::@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http::@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http:@:www.example.com should throw
 FAIL URL's href: http:@:www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
 FAIL XHR: http:@:www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
@@ -399,22 +371,10 @@
 PASS window.open(): http://[::127.0.0.0.1] should throw
 PASS URL's constructor's base argument: a should throw
 FAIL URL's href: a should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: a should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): a should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: a should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): a should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: a/ should throw
 FAIL URL's href: a/ should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: a/ should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): a/ should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: a/ should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): a/ should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: a// should throw
 FAIL URL's href: a// should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: a// should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): a// should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: a// should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): a// should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: file://­/p should throw
 FAIL URL's href: file://­/p should throw assert_throws_js: function "() => url.href = test.input" did not throw
 PASS XHR: file://­/p should throw
diff --git a/third_party/blink/web_tests/platform/mac/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt b/third_party/blink/web_tests/platform/mac/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
index 1cb82e7..e03d9f2 100644
--- a/third_party/blink/web_tests/platform/mac/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
@@ -64,7 +64,7 @@
       "transform": 1
     },
     {
-      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-ready state-stopped'",
+      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-pre-ready state-no-source'",
       "bounds": [150, 60],
       "transform": 1
     },
diff --git a/third_party/blink/web_tests/platform/win/compositing/video/video-poster-expected.txt b/third_party/blink/web_tests/platform/win/compositing/video/video-poster-expected.txt
index d5ebd23..756deb3a 100644
--- a/third_party/blink/web_tests/platform/win/compositing/video/video-poster-expected.txt
+++ b/third_party/blink/web_tests/platform/win/compositing/video/video-poster-expected.txt
@@ -48,7 +48,7 @@
       "transform": 1
     },
     {
-      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-ready state-scrubbing'",
+      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-pre-ready state-no-source'",
       "bounds": [352, 288],
       "transform": 1
     },
diff --git a/third_party/blink/web_tests/platform/win/external/wpt/url/failure-expected.txt b/third_party/blink/web_tests/platform/win/external/wpt/url/failure-expected.txt
index 3dac586..2541c0b 100644
--- a/third_party/blink/web_tests/platform/win/external/wpt/url/failure-expected.txt
+++ b/third_party/blink/web_tests/platform/win/external/wpt/url/failure-expected.txt
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 433 tests; 170 PASS, 263 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 393 tests; 166 PASS, 227 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS URL's constructor's base argument: file://example:1/ should throw
 FAIL URL's href: file://example:1/ should throw assert_throws_js: function "() => url.href = test.input" did not throw
@@ -39,10 +39,6 @@
 PASS window.open(): http://foo:-80/ should throw
 PASS URL's constructor's base argument: http:/:@/www.example.com should throw
 FAIL URL's href: http:/:@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:/:@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:/:@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:/:@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:/:@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http://user@/www.example.com should throw
 FAIL URL's href: http://user@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
 PASS XHR: http://user@/www.example.com should throw
@@ -51,16 +47,8 @@
 PASS window.open(): http://user@/www.example.com should throw
 PASS URL's constructor's base argument: http:@/www.example.com should throw
 FAIL URL's href: http:@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http:/@/www.example.com should throw
 FAIL URL's href: http:/@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:/@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:/@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:/@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:/@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http://@/www.example.com should throw
 FAIL URL's href: http://@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
 PASS XHR: http://@/www.example.com should throw
@@ -69,22 +57,10 @@
 PASS window.open(): http://@/www.example.com should throw
 PASS URL's constructor's base argument: https:@/www.example.com should throw
 FAIL URL's href: https:@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-PASS XHR: https:@/www.example.com should throw
-PASS sendBeacon(): https:@/www.example.com should throw
-FAIL Location's href: https:@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" threw object "SyntaxError: Failed to set the 'href' property on 'Location': 'https:@/www.example.com' is not a valid URL." ("SyntaxError") expected instance of function "function TypeError() { [native code] }" ("TypeError")
-PASS window.open(): https:@/www.example.com should throw
 PASS URL's constructor's base argument: http:a:b@/www.example.com should throw
 FAIL URL's href: http:a:b@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:a:b@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-PASS sendBeacon(): http:a:b@/www.example.com should throw
-FAIL Location's href: http:a:b@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:a:b@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" threw object "TypeError: Cannot read property 'close' of null" that is not a DOMException SyntaxError: property "code" is equal to undefined, expected 12
 PASS URL's constructor's base argument: http:/a:b@/www.example.com should throw
 FAIL URL's href: http:/a:b@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http:/a:b@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http:/a:b@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http:/a:b@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http:/a:b@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http://a:b@/www.example.com should throw
 FAIL URL's href: http://a:b@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
 PASS XHR: http://a:b@/www.example.com should throw
@@ -93,10 +69,6 @@
 PASS window.open(): http://a:b@/www.example.com should throw
 PASS URL's constructor's base argument: http::@/www.example.com should throw
 FAIL URL's href: http::@/www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: http::@/www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): http::@/www.example.com should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: http::@/www.example.com should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): http::@/www.example.com should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: http:@:www.example.com should throw
 FAIL URL's href: http:@:www.example.com should throw assert_throws_js: function "() => url.href = test.input" did not throw
 FAIL XHR: http:@:www.example.com should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
@@ -399,22 +371,10 @@
 PASS window.open(): http://[::127.0.0.0.1] should throw
 PASS URL's constructor's base argument: a should throw
 FAIL URL's href: a should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: a should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): a should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: a should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): a should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: a/ should throw
 FAIL URL's href: a/ should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: a/ should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): a/ should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: a/ should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): a/ should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: a// should throw
 FAIL URL's href: a// should throw assert_throws_js: function "() => url.href = test.input" did not throw
-FAIL XHR: a// should throw assert_throws_dom: function "() => client.open("GET", test.input)" did not throw
-FAIL sendBeacon(): a// should throw assert_throws_js: function "() => self.navigator.sendBeacon(test.input)" did not throw
-FAIL Location's href: a// should throw assert_throws_js: function "() => self[0].location = test.input" did not throw
-FAIL window.open(): a// should throw assert_throws_dom: function "() => self.open(test.input).close()" did not throw
 PASS URL's constructor's base argument: file://­/p should throw
 FAIL URL's href: file://­/p should throw assert_throws_js: function "() => url.href = test.input" did not throw
 PASS XHR: file://­/p should throw
diff --git a/third_party/blink/web_tests/platform/win/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt b/third_party/blink/web_tests/platform/win/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
index fa66bc7..d18100b 100644
--- a/third_party/blink/web_tests/platform/win/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
+++ b/third_party/blink/web_tests/platform/win/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
@@ -64,7 +64,7 @@
       "transform": 1
     },
     {
-      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-ready state-stopped'",
+      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-pre-ready state-no-source'",
       "bounds": [150, 60],
       "transform": 1
     },
diff --git a/third_party/blink/web_tests/virtual/android/fullscreen/video-overlay-scroll-expected.txt b/third_party/blink/web_tests/virtual/android/fullscreen/video-overlay-scroll-expected.txt
index 10bc14ae..a6b22c0 100644
--- a/third_party/blink/web_tests/virtual/android/fullscreen/video-overlay-scroll-expected.txt
+++ b/third_party/blink/web_tests/virtual/android/fullscreen/video-overlay-scroll-expected.txt
@@ -6,7 +6,7 @@
       "drawsContent": false
     },
     {
-      "name": "LayoutFlexibleBox (relative positioned) DIV class='phase-pre-ready state-no-source sizing-small use-default-poster'",
+      "name": "LayoutNGFlexibleBox (relative positioned) DIV class='sizing-small phase-pre-ready state-no-source use-default-poster'",
       "bounds": [800, 600],
       "contentsOpaque": true,
       "backgroundColor": "#333333"
@@ -17,7 +17,7 @@
       "drawsContent": false
     },
     {
-      "name": "LayoutFlexibleBox DIV",
+      "name": "LayoutNGFlexibleBox DIV",
       "bounds": [800, 600]
     }
   ]
diff --git a/third_party/blink/web_tests/webexposed/feature-policy-features-expected.txt b/third_party/blink/web_tests/webexposed/feature-policy-features-expected.txt
index 59ecdc54..2abc7f4b 100644
--- a/third_party/blink/web_tests/webexposed/feature-policy-features-expected.txt
+++ b/third_party/blink/web_tests/webexposed/feature-policy-features-expected.txt
@@ -54,5 +54,6 @@
 trust-token-redemption
 usb
 vertical-scroll
+window-placement
 xr-spatial-tracking
 
diff --git a/tools/clang/rewrite_raw_ptr_fields/manual-fields-to-ignore.txt b/tools/clang/rewrite_raw_ptr_fields/manual-fields-to-ignore.txt
index 80d03c5..98c859e8 100644
--- a/tools/clang/rewrite_raw_ptr_fields/manual-fields-to-ignore.txt
+++ b/tools/clang/rewrite_raw_ptr_fields/manual-fields-to-ignore.txt
@@ -45,7 +45,7 @@
 # Populated manually - the rewriter has trouble appending |.get()| inside macros
 # that work with |XDisplay*|.
 extensions::GlobalShortcutListenerX11::x_display_
-gl::GLVisualPickerGLX::display_
+ui::VisualPickerGlx::display_
 media::(anonymous namespace)::UserInputMonitorLinuxCore::x_record_display_
 media::cast::test::LinuxOutputWindow::display_
 remoting::(anonymous namespace)::InputInjectorX11::Core::display_
diff --git a/tools/mb/mb.py b/tools/mb/mb.py
index d302cbc..9467283 100755
--- a/tools/mb/mb.py
+++ b/tools/mb/mb.py
@@ -99,9 +99,28 @@
         'win') else 'isolate'
     self.use_luci_auth = False
     self.rts_out_dir = self.PathJoin('gen', 'rts')
+    self.banned_from_rts = set()
+
+  def PostArgsInit(self):
+    self.use_luci_auth = getattr(self.args, 'luci_auth', False)
+
+    if 'config_file' in self.args and self.args.config_file is None:
+      self.args.config_file = self.default_config
+
+    if 'expectations_dir' in self.args and self.args.expectations_dir is None:
+      self.args.expectations_dir = os.path.join(
+          os.path.dirname(self.args.config_file), 'mb_config_expectations')
+
+    if getattr(self.args, 'builder', None):
+      banned_from_rts_map = json.loads(
+          self.ReadFile(
+              self.PathJoin(self.chromium_src_dir, 'tools', 'mb',
+                            'rts_banned_suites.json')))
+      self.banned_from_rts = banned_from_rts_map.get(self.args.builder, set())
 
   def Main(self, args):
     self.ParseArgs(args)
+    self.PostArgsInit()
     try:
       ret = self.args.func()
       if ret != 0:
@@ -400,15 +419,6 @@
 
     self.args = parser.parse_args(argv)
 
-    self.use_luci_auth = getattr(self.args, 'luci_auth', False)
-
-    if 'config_file' in self.args and self.args.config_file is None:
-      self.args.config_file = self.default_config
-
-    if 'expectations_dir' in self.args and self.args.expectations_dir is None:
-      self.args.expectations_dir = os.path.join(
-          os.path.dirname(self.args.config_file), 'mb_config_expectations')
-
   def DumpInputFiles(self):
 
     def DumpContentsOfFilePassedTo(arg_name, path):
@@ -1216,11 +1226,14 @@
       # For more info about RTS, please see
       # //docs/testing/regression-test-selection.md
       if self.args.use_rts:
-        filter_file = target + '.filter'
-        filter_file_path = self.PathJoin(self.rts_out_dir, filter_file)
-        if self.Exists(self.ToAbsPath(build_dir, filter_file_path)):
-          command.append('--test-launcher-filter-file=%s' % filter_file_path)
-          self.Print('added rts filter file to isolate: %s' % filter_file)
+        if target in self.banned_from_rts:
+          self.Print('%s is banned for RTS on this builder' % target)
+        else:
+          filter_file = target + '.filter'
+          filter_file_path = self.PathJoin(self.rts_out_dir, filter_file)
+          if self.Exists(self.ToAbsPath(build_dir, filter_file_path)):
+            command.append('--test-launcher-filter-file=%s' % filter_file_path)
+            self.Print('added RTS filter file to isolate: %s' % filter_file)
 
       canonical_target = target.replace(':','_').replace('/','_')
       ret = self.WriteIsolateFiles(build_dir, command, canonical_target,
diff --git a/tools/mb/mb_config.pyl b/tools/mb/mb_config.pyl
index e5f42ae..759d413 100644
--- a/tools/mb/mb_config.pyl
+++ b/tools/mb/mb_config.pyl
@@ -23,6 +23,7 @@
       'chromeos-kevin-chrome': 'chromeos_kevin_include_unwind_tables_official',
       'chromeos-kevin-chrome-lts': 'chromeos_kevin_include_unwind_tables_official',
       'lacros-amd64-generic-chrome': 'chromeos_amd64-generic_lacros_official',
+      'lacros-arm-generic-chrome': 'chromeos_arm-generic_lacros_official',
       # Don't include unwind tables for linux-/mac-/win-/win64-chrome builders.
       # They monitor binary size growth, which may be affected by the tables.
       'linux-chrome': 'official_goma',
@@ -889,6 +890,7 @@
       'chromeos-kevin-chrome': 'chromeos_kevin_include_unwind_tables_official',
       'chromeos-kevin-compile-chrome': 'chromeos_kevin_include_unwind_tables_official',
       'lacros-amd64-generic-chrome': 'chromeos_amd64-generic_lacros_official',
+      'lacros-arm-generic-chrome': 'chromeos_arm-generic_lacros_official',
       'linux-chrome': 'official_goma',
       'linux-chrome-beta': 'official_goma',
       'linux-chrome-stable': 'official_goma',
@@ -1764,6 +1766,10 @@
       'chromeos_device', 'arm-generic', 'debug',
     ],
 
+    'chromeos_arm-generic_lacros_official': [
+      'chromeos_arm-generic', 'arm-lacros', 'official', 'minimal_symbols', 'cfi', 'thin_lto',
+    ],
+
     'chromeos_arm-generic_lacros_rel': [
       'chromeos_arm-generic', 'arm-lacros',
     ],
@@ -2713,11 +2719,11 @@
     ],
 
     'release_trybot_backuprefptr_arm': [
-      'release_trybot', 'backuprefptr', 'android_without_codecs', 'arm',
+      'release_trybot', 'backuprefptr', 'android', 'arm',
     ],
 
     'release_trybot_backuprefptr_arm64': [
-      'release_trybot', 'backuprefptr', 'android_without_codecs', 'arm64',
+      'release_trybot', 'backuprefptr', 'android', 'arm64',
     ],
 
     'release_trybot_paeverywhere_x64': [
diff --git a/tools/mb/mb_config_expectations/chrome.json b/tools/mb/mb_config_expectations/chrome.json
index 96061701..fd22328 100644
--- a/tools/mb/mb_config_expectations/chrome.json
+++ b/tools/mb/mb_config_expectations/chrome.json
@@ -131,6 +131,42 @@
       "use_vaapi": false
     }
   },
+  "lacros-arm-generic-chrome": {
+    "args_file": "//build/args/chromeos/arm-generic.gni",
+    "gn_args": {
+      "chromeos_is_browser_only": true,
+      "cros_host_sysroot": "//build/linux/debian_sid_amd64-sysroot",
+      "cros_v8_snapshot_sysroot": "//build/linux/debian_sid_i386-sysroot",
+      "enable_linux_installer": false,
+      "is_cfi": true,
+      "is_chrome_branded": true,
+      "is_chromeos_device": true,
+      "is_official_build": true,
+      "ozone_platform": "wayland",
+      "ozone_platform_drm": false,
+      "ozone_platform_gbm": false,
+      "ozone_platform_headless": true,
+      "ozone_platform_wayland": true,
+      "ozone_platform_x11": false,
+      "rtc_use_pipewire": false,
+      "symbol_level": 1,
+      "target_os": "chromeos",
+      "use_custom_libcxx": false,
+      "use_custom_libcxx_for_host": true,
+      "use_evdev_gestures": false,
+      "use_gio": false,
+      "use_glib": false,
+      "use_goma": true,
+      "use_gtk": false,
+      "use_ozone": true,
+      "use_pangocairo": false,
+      "use_pulseaudio": false,
+      "use_system_libsync": false,
+      "use_thin_lto": true,
+      "use_v8_context_snapshot": false,
+      "use_vaapi": false
+    }
+  },
   "linux-chrome": {
     "gn_args": {
       "is_chrome_branded": true,
diff --git a/tools/mb/mb_config_expectations/chromium.fyi.json b/tools/mb/mb_config_expectations/chromium.fyi.json
index 897ae19..284c82f4 100644
--- a/tools/mb/mb_config_expectations/chromium.fyi.json
+++ b/tools/mb/mb_config_expectations/chromium.fyi.json
@@ -383,8 +383,10 @@
       "dcheck_always_on": true,
       "enable_backup_ref_ptr_slow_checks": true,
       "enable_runtime_backup_ref_ptr_control": true,
+      "ffmpeg_branding": "Chrome",
       "is_component_build": false,
       "is_debug": false,
+      "proprietary_codecs": true,
       "put_ref_count_in_previous_slot": true,
       "symbol_level": 1,
       "target_cpu": "arm",
@@ -399,8 +401,10 @@
       "dcheck_always_on": true,
       "enable_backup_ref_ptr_slow_checks": true,
       "enable_runtime_backup_ref_ptr_control": true,
+      "ffmpeg_branding": "Chrome",
       "is_component_build": false,
       "is_debug": false,
+      "proprietary_codecs": true,
       "put_ref_count_in_previous_slot": true,
       "symbol_level": 1,
       "target_cpu": "arm64",
diff --git a/tools/mb/mb_config_expectations/tryserver.chrome.json b/tools/mb/mb_config_expectations/tryserver.chrome.json
index 912195e..b1066d8 100644
--- a/tools/mb/mb_config_expectations/tryserver.chrome.json
+++ b/tools/mb/mb_config_expectations/tryserver.chrome.json
@@ -155,6 +155,42 @@
       "use_vaapi": false
     }
   },
+  "lacros-arm-generic-chrome": {
+    "args_file": "//build/args/chromeos/arm-generic.gni",
+    "gn_args": {
+      "chromeos_is_browser_only": true,
+      "cros_host_sysroot": "//build/linux/debian_sid_amd64-sysroot",
+      "cros_v8_snapshot_sysroot": "//build/linux/debian_sid_i386-sysroot",
+      "enable_linux_installer": false,
+      "is_cfi": true,
+      "is_chrome_branded": true,
+      "is_chromeos_device": true,
+      "is_official_build": true,
+      "ozone_platform": "wayland",
+      "ozone_platform_drm": false,
+      "ozone_platform_gbm": false,
+      "ozone_platform_headless": true,
+      "ozone_platform_wayland": true,
+      "ozone_platform_x11": false,
+      "rtc_use_pipewire": false,
+      "symbol_level": 1,
+      "target_os": "chromeos",
+      "use_custom_libcxx": false,
+      "use_custom_libcxx_for_host": true,
+      "use_evdev_gestures": false,
+      "use_gio": false,
+      "use_glib": false,
+      "use_goma": true,
+      "use_gtk": false,
+      "use_ozone": true,
+      "use_pangocairo": false,
+      "use_pulseaudio": false,
+      "use_system_libsync": false,
+      "use_thin_lto": true,
+      "use_v8_context_snapshot": false,
+      "use_vaapi": false
+    }
+  },
   "linux-chrome": {
     "gn_args": {
       "is_chrome_branded": true,
diff --git a/tools/mb/mb_unittest.py b/tools/mb/mb_unittest.py
index 8449108..3d48163 100755
--- a/tools/mb/mb_unittest.py
+++ b/tools/mb/mb_unittest.py
@@ -303,6 +303,8 @@
     mbw.files.setdefault(
         mbw.ToAbsPath('//build/args/bots/fake_builder_group/fake_args_bot.gn'),
         'is_debug = false\n')
+    mbw.files.setdefault(mbw.ToAbsPath('//tools/mb/rts_banned_suites.json'),
+                         '{}')
     if files:
       for path, contents in files.items():
         mbw.files[path] = contents
diff --git a/tools/mb/rts_banned_suites.json b/tools/mb/rts_banned_suites.json
new file mode 100644
index 0000000..e8eb8a63
--- /dev/null
+++ b/tools/mb/rts_banned_suites.json
@@ -0,0 +1,66 @@
+{
+  "linux-rel-rts": [
+    "xr_browser_tests",
+    "hardware_accelerated_feature_tests",
+    "telemetry_perf_unittests",
+    "pixel_skia_gold_passthrough_test",
+    "gl_renderer_screenshot_sync_tests",
+    "mojo_python_unittests",
+    "blink_python_tests",
+    "info_collection_tests",
+    "telemetry_gpu_unittests",
+    "depth_capture_tests",
+    "metrics_python_tests",
+    "grit_python_unittests",
+    "webgl_conformance_tests",
+    "gpu_process_launch_tests",
+    "maps_pixel_passthrough_test",
+    "trace_test",
+    "context_lost_passthrough_tests",
+    "screenshot_sync_passthrough_tests"
+  ],
+  "win10_chromium_x64_rel_ng_rts": [
+    "trace_test",
+    "webgl_conformance_d3d11_passthrough_tests",
+    "xr_browser_tests",
+    "info_collection_tests",
+    "maps_pixel_passthrough_test",
+    "pixel_skia_gold_passthrough_test",
+    "metrics_python_tests",
+    "screenshot_sync_passthrough_tests",
+    "blink_python_tests",
+    "gpu_process_launch_tests",
+    "mojo_python_unittests",
+    "depth_capture_tests",
+    "grit_python_unittests",
+    "hardware_accelerated_feature_tests",
+    "telemetry_gpu_unittests",
+    "telemetry_desktop_minidump_unittests",
+    "context_lost_passthrough_tests"
+  ],
+  "fuchsia_x64_rts": [
+    "maps_tests",
+    "hardware_accelerated_feature_tests",
+    "webgl_conformance_tests",
+    "trace_test",
+    "blink_web_tests",
+    "gpu_process_launch_tests",
+    "context_lost_validating_tests"
+  ],
+  "mac-rel-rts": [
+    "trace_test",
+    "maps_pixel_validating_test",
+    "screenshot_sync_validating_tests",
+    "hardware_accelerated_feature_tests",
+    "webgl_conformance_tests",
+    "pixel_skia_gold_validating_test",
+    "info_collection_tests",
+    "context_lost_validating_tests",
+    "depth_capture_tests",
+    "gpu_process_launch_tests"
+  ],
+  "chromeos-amd64-generic-rel-rts": [
+    "telemetry_perf_unittests",
+    "webgl_conformance_tests"
+  ]
+}
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 4419e7b..aa0ab88 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -32642,6 +32642,7 @@
   <int value="83" label="SharedAutofill"/>
   <int value="84" label="DirectSockets"/>
   <int value="85" label="ClientHintPrefersColorScheme"/>
+  <int value="86" label="WindowPlacement"/>
 </enum>
 
 <enum name="FeaturePolicyImageCompressionFormat">
diff --git a/tools/metrics/histograms/histograms_xml/accessibility/histograms.xml b/tools/metrics/histograms/histograms_xml/accessibility/histograms.xml
index ee3ebb1..8be66ac4 100644
--- a/tools/metrics/histograms/histograms_xml/accessibility/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/accessibility/histograms.xml
@@ -694,7 +694,7 @@
 </histogram>
 
 <histogram name="Accessibility.LanguageDetection.CountDetectionAttempted"
-    units="count" expires_after="2021-08-09">
+    units="count" expires_after="2021-11-19">
   <owner>chrishall@chromium.org</owner>
   <owner>aboxhall@chromium.org</owner>
   <owner>dmazzoni@chromium.org</owner>
@@ -704,7 +704,7 @@
 </histogram>
 
 <histogram name="Accessibility.LanguageDetection.CountLabelled" units="count"
-    expires_after="2021-08-09">
+    expires_after="2021-11-19">
   <owner>chrishall@chromium.org</owner>
   <owner>aboxhall@chromium.org</owner>
   <owner>dmazzoni@chromium.org</owner>
@@ -715,7 +715,7 @@
 </histogram>
 
 <histogram name="Accessibility.LanguageDetection.LangsPerPage" units="count"
-    expires_after="2021-10-10">
+    expires_after="2021-11-19">
   <owner>chrishall@chromium.org</owner>
   <owner>aboxhall@chromium.org</owner>
   <owner>dmazzoni@chromium.org</owner>
@@ -727,7 +727,7 @@
 </histogram>
 
 <histogram name="Accessibility.LanguageDetection.PercentageLabelledWithTop"
-    units="%" expires_after="2021-08-09">
+    units="%" expires_after="2021-11-19">
   <owner>chrishall@chromium.org</owner>
   <owner>aboxhall@chromium.org</owner>
   <owner>dmazzoni@chromium.org</owner>
@@ -739,7 +739,7 @@
 </histogram>
 
 <histogram name="Accessibility.LanguageDetection.PercentageLanguageDetected"
-    units="%" expires_after="2021-05-23">
+    units="%" expires_after="2021-11-19">
   <owner>chrishall@chromium.org</owner>
   <owner>aboxhall@chromium.org</owner>
   <owner>dmazzoni@chromium.org</owner>
@@ -750,7 +750,7 @@
 </histogram>
 
 <histogram name="Accessibility.LanguageDetection.PercentageOverridden"
-    units="%" expires_after="2021-08-09">
+    units="%" expires_after="2021-11-19">
   <owner>chrishall@chromium.org</owner>
   <owner>aboxhall@chromium.org</owner>
   <owner>dmazzoni@chromium.org</owner>
diff --git a/tools/metrics/histograms/histograms_xml/android/histograms.xml b/tools/metrics/histograms/histograms_xml/android/histograms.xml
index 57b4e2d6..efa4894 100644
--- a/tools/metrics/histograms/histograms_xml/android/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/android/histograms.xml
@@ -3260,9 +3260,13 @@
 <histogram
     name="Android.WebView.ShouldInterceptRequest.NullInputStream.ResponseStatusCode"
     enum="HttpResponseCode" expires_after="2020-06-14">
+  <obsolete>
+    Removed 2021-05
+  </obsolete>
   <owner>timvolodine@chromium.org</owner>
   <owner>tobiasjs@chromium.org</owner>
   <owner>ntfschr@chromium.org</owner>
+  <owner>src/android_webview/OWNERS</owner>
   <summary>
     Records the custom response status code for the intercepted requests where
     input stream is null. In case status code is invalid (or has not been
diff --git a/tools/metrics/histograms/histograms_xml/autofill/histograms.xml b/tools/metrics/histograms/histograms_xml/autofill/histograms.xml
index ec46d6d..fcd6a66 100644
--- a/tools/metrics/histograms/histograms_xml/autofill/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/autofill/histograms.xml
@@ -2139,6 +2139,7 @@
 <histogram name="Autofill.UsedCachedServerCard" units="uses"
     expires_after="2021-12-20">
   <owner>jsaul@google.com</owner>
+  <owner>siyua@chromium.org</owner>
   <owner>payments-autofill-team@google.com</owner>
   <summary>
     Records the number of times that the cache for unmasked server cards has
@@ -2148,6 +2149,19 @@
   </summary>
 </histogram>
 
+<histogram name="Autofill.UsedCachedVirtualCard" units="uses"
+    expires_after="2021-12-20">
+  <owner>jsaul@google.com</owner>
+  <owner>siyua@chromium.org</owner>
+  <owner>payments-autofill-team@google.com</owner>
+  <summary>
+    Records the number of times that the cache for virtual cards has been
+    accessed for a given card. For example, if the cache is being accessed and
+    it has already been accessed for the card twice before, then &quot;3&quot;
+    is recorded.
+  </summary>
+</histogram>
+
 <histogram name="Autofill.UserHappiness" enum="AutofillUserHappiness"
     expires_after="M95">
   <owner>battre@chromium.org</owner>
diff --git a/tools/metrics/histograms/histograms_xml/browser/histograms.xml b/tools/metrics/histograms/histograms_xml/browser/histograms.xml
index 55a63b5..1e72261 100644
--- a/tools/metrics/histograms/histograms_xml/browser/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/browser/histograms.xml
@@ -28,8 +28,9 @@
 </variants>
 
 <histogram name="Browser.AnyWindowHasName" enum="Boolean"
-    expires_after="2021-06-30">
+    expires_after="2021-09-30">
   <owner>ellyjones@chromium.org</owner>
+  <owner>lgrey@chromium.org</owner>
   <summary>
     Whether any browser window in the current session has a user-set name.
     Logged once every histogram recording.
diff --git a/tools/metrics/histograms/histograms_xml/download/histograms.xml b/tools/metrics/histograms/histograms_xml/download/histograms.xml
index 9f5fa7a0..343b2cb 100644
--- a/tools/metrics/histograms/histograms_xml/download/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/download/histograms.xml
@@ -228,7 +228,7 @@
 </histogram>
 
 <histogram base="true" name="Download.InsecureBlocking.Extensions"
-    enum="InsecureDownloadExtensions" expires_after="M90">
+    enum="InsecureDownloadExtensions" expires_after="M94">
   <owner>jdeblasio@chromium.org</owner>
   <owner>estark@chromium.org</owner>
   <owner>cthomp@chromium.org</owner>
@@ -239,7 +239,7 @@
 </histogram>
 
 <histogram name="Download.InsecureBlocking.Totals"
-    enum="InsecureDownloadSecurityStatus" expires_after="M90">
+    enum="InsecureDownloadSecurityStatus" expires_after="M94">
   <owner>jdeblasio@chromium.org</owner>
   <owner>estark@chromium.org</owner>
   <owner>cthomp@chromium.org</owner>
@@ -250,7 +250,7 @@
 </histogram>
 
 <histogram base="true" name="Download.InsecureBlocking.Verification"
-    enum="DownloadContentType" expires_after="M90">
+    enum="DownloadContentType" expires_after="M92">
   <owner>jdeblasio@chromium.org</owner>
   <owner>estark@chromium.org</owner>
   <owner>cthomp@chromium.org</owner>
diff --git a/tools/metrics/histograms/histograms_xml/histogram_suffixes_list.xml b/tools/metrics/histograms/histograms_xml/histogram_suffixes_list.xml
index 68355a5..d76b648 100644
--- a/tools/metrics/histograms/histograms_xml/histogram_suffixes_list.xml
+++ b/tools/metrics/histograms/histograms_xml/histogram_suffixes_list.xml
@@ -9047,6 +9047,8 @@
   <suffix name="TextClassifier.FindLanguages" label=""/>
   <suffix name="TextClassifier.LoadModelResult" label=""/>
   <suffix name="TextClassifier.SuggestSelection" label=""/>
+  <suffix name="TextSuggester.LoadModelResult" label=""/>
+  <suffix name="TextSuggester.Suggest" label=""/>
   <affected-histogram name="MachineLearningService.CpuTimeMicrosec"/>
   <affected-histogram name="MachineLearningService.ElapsedTimeMicrosec">
     <obsolete>
diff --git a/tools/metrics/histograms/histograms_xml/history/histograms.xml b/tools/metrics/histograms/histograms_xml/history/histograms.xml
index b5fe2486..dc6dfb1 100644
--- a/tools/metrics/histograms/histograms_xml/history/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/history/histograms.xml
@@ -170,7 +170,7 @@
 </histogram>
 
 <histogram name="History.ClearBrowsingData.FailedTasksChrome"
-    enum="ChromeBrowsingDataRemoverTasks" expires_after="2021-07-01">
+    enum="ChromeBrowsingDataRemoverTasks" expires_after="2021-09-01">
   <owner>dullweber@chromium.org</owner>
   <owner>msramek@chromium.org</owner>
   <summary>
diff --git a/tools/metrics/histograms/histograms_xml/mobile/OWNERS b/tools/metrics/histograms/histograms_xml/mobile/OWNERS
new file mode 100644
index 0000000..bc7608b6c
--- /dev/null
+++ b/tools/metrics/histograms/histograms_xml/mobile/OWNERS
@@ -0,0 +1,5 @@
+per-file OWNERS=file://tools/metrics/histograms/histograms_xml/METRIC_REVIEWER_OWNERS
+
+# Prefer sending CLs to the owners listed below.
+# Use chromium-metrics-reviews@google.com as a backup.
+ender@chromium.org
diff --git a/tools/metrics/histograms/histograms_xml/nearby/histograms.xml b/tools/metrics/histograms/histograms_xml/nearby/histograms.xml
index 7753cc8e..e93cb17 100644
--- a/tools/metrics/histograms/histograms_xml/nearby/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/nearby/histograms.xml
@@ -610,6 +610,18 @@
   </summary>
 </histogram>
 
+<histogram name="Nearby.Share.EnabledStateChanged" enum="BooleanEnabled"
+    expires_after="2021-10-25">
+  <owner>nohle@chromium.org</owner>
+  <owner>nearby-share-chromeos-eng@google.com</owner>
+  <summary>
+    Records the enabled/disabled state of Nearby Share. Emitted when the feature
+    state changes from disabled to enabled or vice versa. This can happen after
+    Nearby Share onboarding or when the user enables/disables the Nearby Share
+    feature toggle in Settings.
+  </summary>
+</histogram>
+
 <histogram name="Nearby.Share.IsKnownContact" enum="BooleanKnown"
     expires_after="2021-10-25">
   <owner>nohle@chromium.org</owner>
diff --git a/tools/metrics/histograms/histograms_xml/others/histograms.xml b/tools/metrics/histograms/histograms_xml/others/histograms.xml
index 8b391a20..3d07e77 100644
--- a/tools/metrics/histograms/histograms_xml/others/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/others/histograms.xml
@@ -3545,6 +3545,17 @@
   </summary>
 </histogram>
 
+<histogram name="Conversions.Report.HttpResponseOrNetErrorCode"
+    enum="CombinedHttpResponseAndNetErrorCode" expires_after="2021-10-17">
+  <owner>johnidel@chromium.org</owner>
+  <owner>measurement-api-dev+metrics@google.com</owner>
+  <summary>
+    Error info for sending a conversion report, recorded for each sent report.
+    The HTTP response code is recorded if there is no net error code for the
+    request, or the net error code indicates there was a response code failusre.
+  </summary>
+</histogram>
+
 <histogram name="Conversions.ReportRetrySucceed" enum="BooleanSuccess"
     expires_after="M95">
   <owner>johnidel@chromium.org</owner>
@@ -9315,6 +9326,15 @@
   </summary>
 </histogram>
 
+<histogram name="MachineLearningService.TextSuggester.Suggest.Event"
+    enum="BooleanError" expires_after="2022-05-31">
+  <owner>curtismcmullan@chromium.org</owner>
+  <owner>amoylan@chromium.org</owner>
+  <summary>
+    The result of a text suggester request, which can be OK or ERROR.
+  </summary>
+</histogram>
+
 <histogram base="true" name="MachineLearningService.TotalMemoryDeltaKb"
     units="KB" expires_after="2021-07-01">
   <owner>amoylan@chromium.org</owner>
@@ -14983,18 +15003,31 @@
 </histogram>
 
 <histogram name="SiteIsolation.SavedUserTriggeredIsolatedOrigins.Size"
-    units="origins" expires_after="2021-09-30">
+    units="origins" expires_after="2021-11-30">
   <owner>alexmos@chromium.org</owner>
   <owner>creis@chromium.org</owner>
   <owner>lukasza@chromium.org</owner>
   <summary>
     The number of currently saved user-triggered isolated sites. This includes
     sites where the user has entered a password while using Site Isolation for
-    password sites (which is the target Site Isolation mode for Android).
+    password sites (which is a currently active site isolation mode on Android).
     Recorded once on browser startup.
   </summary>
 </histogram>
 
+<histogram name="SiteIsolation.SavedWebTriggeredIsolatedOrigins.Size"
+    units="origins" expires_after="2021-11-30">
+  <owner>alexmos@chromium.org</owner>
+  <owner>creis@chromium.org</owner>
+  <owner>lukasza@chromium.org</owner>
+  <summary>
+    The number of currently saved web-triggered isolated sites. This includes
+    sites that were isolated due to Cross-Origin-Opener-Policy headers, which is
+    a heuristic used for site isolation on Android. Recorded once on browser
+    startup.
+  </summary>
+</histogram>
+
 <histogram name="SiteIsolation.SiteInstancesPerBrowsingInstance" units="units"
     expires_after="2021-09-30">
   <owner>alexmos@chromium.org</owner>
@@ -18427,7 +18460,7 @@
 </histogram>
 
 <histogram name="Webapp.Update.ManifestUpdateResult.DefaultApp"
-    enum="WebAppManifestUpdateResult" expires_after="M92">
+    enum="WebAppManifestUpdateResult" expires_after="M96">
   <owner>alancutter@chromium.org</owner>
   <owner>tsergeant@chromium.org</owner>
   <owner>desktop-pwas-team@google.com</owner>
diff --git a/tools/metrics/histograms/histograms_xml/sb_client/histograms.xml b/tools/metrics/histograms/histograms_xml/sb_client/histograms.xml
index 59f1c6f7..a0f0c47a 100644
--- a/tools/metrics/histograms/histograms_xml/sb_client/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/sb_client/histograms.xml
@@ -140,6 +140,9 @@
 
 <histogram name="SBClientDownload.DownloadRequestPayloadSize" units="bytes"
     expires_after="M90">
+  <obsolete>
+    Remove 05-2021 due to lack of use.
+  </obsolete>
   <owner>drubery@chromium.org</owner>
   <owner>chrome-safebrowsing-alerts@google.com</owner>
   <owner>mattm@chromium.org</owner>
diff --git a/tools/metrics/histograms/histograms_xml/service/histograms.xml b/tools/metrics/histograms/histograms_xml/service/histograms.xml
index 08d0744..29fac3fd 100644
--- a/tools/metrics/histograms/histograms_xml/service/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/service/histograms.xml
@@ -1123,7 +1123,7 @@
 </histogram>
 
 <histogram name="ServiceWorker.Storage.RetryCountForRecovery" units="retries"
-    expires_after="2021-06-30">
+    expires_after="2021-11-17">
   <owner>bashi@chromium.org</owner>
   <owner>chrome-worker@google.com</owner>
   <summary>
diff --git a/tools/perf/core/perfetto_binary_roller/binary_deps.json b/tools/perf/core/perfetto_binary_roller/binary_deps.json
index 7cab00a..1235a74 100644
--- a/tools/perf/core/perfetto_binary_roller/binary_deps.json
+++ b/tools/perf/core/perfetto_binary_roller/binary_deps.json
@@ -1,8 +1,8 @@
 {
     "trace_processor_shell": {
         "win": {
-            "hash": "6c9d1659449dd8f1930cfcf263ab1005aa602320",
-            "remote_path": "perfetto_binaries/trace_processor_shell/win/5e860d49ed384a4785abec0eb68f4ce2f652dca6/trace_processor_shell.exe"
+            "hash": "d66c9ed761628a115d9b50ca0816aed0ff3155fa",
+            "remote_path": "perfetto_binaries/trace_processor_shell/win/eb6479ae08749f235840d15128127f39bc434ac1/trace_processor_shell.exe"
         },
         "mac": {
             "hash": "b71e08180101b61191679d51b089504977d8c19c",
@@ -10,7 +10,7 @@
         },
         "linux": {
             "hash": "9076c8b15696207c77b63956250bee3969d86d35",
-            "remote_path": "perfetto_binaries/trace_processor_shell/linux/9109f50a742ab7b4cb70717308281137a8734b2b/trace_processor_shell"
+            "remote_path": "perfetto_binaries/trace_processor_shell/linux/426cf86299ed21bd62ea2ba09af8ac27b69e02f1/trace_processor_shell"
         }
     },
     "power_profile.sql": {
diff --git a/ui/accessibility/accessibility_features.cc b/ui/accessibility/accessibility_features.cc
index 722db1ee..57551faa 100644
--- a/ui/accessibility/accessibility_features.cc
+++ b/ui/accessibility/accessibility_features.cc
@@ -161,4 +161,13 @@
 }
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
+#if defined(OS_ANDROID)
+const base::Feature kComputeAXMode{"ComputeAXMode",
+                                   base::FEATURE_DISABLED_BY_DEFAULT};
+
+bool IsComputeAXModeEnabled() {
+  return base::FeatureList::IsEnabled(::features::kComputeAXMode);
+}
+#endif  // defined(OS_ANDROID)
+
 }  // namespace features
diff --git a/ui/accessibility/accessibility_features.h b/ui/accessibility/accessibility_features.h
index a303ecf..09e87f2 100644
--- a/ui/accessibility/accessibility_features.h
+++ b/ui/accessibility/accessibility_features.h
@@ -147,6 +147,15 @@
 AX_BASE_EXPORT bool IsSelectToSpeakNavigationControlEnabled();
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
+#if defined(OS_ANDROID)
+// Compute the AXMode based on AccessibilityServiceInfo. If disabled,
+// the AXMode is either entirely on or entirely off.
+AX_BASE_EXPORT extern const base::Feature kComputeAXMode;
+
+// Returns true if the IChromeAccessible COM API is enabled.
+AX_BASE_EXPORT bool IsComputeAXModeEnabled();
+#endif  // defined(OS_ANDROID)
+
 }  // namespace features
 
 #endif  // UI_ACCESSIBILITY_ACCESSIBILITY_FEATURES_H_
diff --git a/ui/base/x/BUILD.gn b/ui/base/x/BUILD.gn
index 5f90ba68..5dc75c9 100644
--- a/ui/base/x/BUILD.gn
+++ b/ui/base/x/BUILD.gn
@@ -16,6 +16,8 @@
     "selection_requestor.h",
     "selection_utils.cc",
     "selection_utils.h",
+    "visual_picker_glx.cc",
+    "visual_picker_glx.h",
     "x11_clipboard_helper.cc",
     "x11_clipboard_helper.h",
     "x11_cursor.cc",
diff --git a/ui/gl/gl_visual_picker_glx.cc b/ui/base/x/visual_picker_glx.cc
similarity index 74%
rename from ui/gl/gl_visual_picker_glx.cc
rename to ui/base/x/visual_picker_glx.cc
index abe58bd..349598fd 100644
--- a/ui/gl/gl_visual_picker_glx.cc
+++ b/ui/base/x/visual_picker_glx.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ui/gl/gl_visual_picker_glx.h"
+#include "ui/base/x/visual_picker_glx.h"
 
 #include <algorithm>
 #include <bitset>
@@ -13,11 +13,47 @@
 #include "base/memory/singleton.h"
 #include "base/stl_util.h"
 #include "ui/gfx/x/future.h"
-#include "ui/gl/gl_bindings.h"
-#include "ui/gl/gl_context.h"
-#include "ui/gl/gl_surface_glx.h"
 
-namespace gl {
+// These constants are obtained from GL/glx.h and GL/glxext.h.
+constexpr uint32_t GLX_LEVEL = 3;
+constexpr uint32_t GLX_DOUBLEBUFFER = 5;
+constexpr uint32_t GLX_STEREO = 6;
+constexpr uint32_t GLX_BUFFER_SIZE = 2;
+constexpr uint32_t GLX_AUX_BUFFERS = 7;
+constexpr uint32_t GLX_RED_SIZE = 8;
+constexpr uint32_t GLX_GREEN_SIZE = 9;
+constexpr uint32_t GLX_BLUE_SIZE = 10;
+constexpr uint32_t GLX_ALPHA_SIZE = 11;
+constexpr uint32_t GLX_DEPTH_SIZE = 12;
+constexpr uint32_t GLX_STENCIL_SIZE = 13;
+constexpr uint32_t GLX_ACCUM_RED_SIZE = 14;
+constexpr uint32_t GLX_ACCUM_GREEN_SIZE = 15;
+constexpr uint32_t GLX_ACCUM_BLUE_SIZE = 16;
+constexpr uint32_t GLX_ACCUM_ALPHA_SIZE = 17;
+
+constexpr uint32_t GLX_CONFIG_CAVEAT = 0x20;
+constexpr uint32_t GLX_VISUAL_CAVEAT_EXT = 0x20;
+constexpr uint32_t GLX_X_VISUAL_TYPE = 0x22;
+
+constexpr uint32_t GLX_BIND_TO_TEXTURE_TARGETS_EXT = 0x20D3;
+
+constexpr uint32_t GLX_NONE = 0x8000;
+constexpr uint32_t GLX_NONE_EXT = 0x8000;
+constexpr uint32_t GLX_TRUE_COLOR = 0x8002;
+constexpr uint32_t GLX_VISUAL_ID = 0x800B;
+constexpr uint32_t GLX_DRAWABLE_TYPE = 0x8010;
+constexpr uint32_t GLX_RENDER_TYPE = 0x8011;
+constexpr uint32_t GLX_FBCONFIG_ID = 0x8013;
+
+constexpr uint32_t GLX_PIXMAP_BIT = 0x00000002;
+constexpr uint32_t GLX_TEXTURE_2D_BIT_EXT = 0x00000002;
+
+constexpr uint32_t GLX_SAMPLE_BUFFERS_ARB = 100000;
+constexpr uint32_t GLX_SAMPLES = 100001;
+
+constexpr uint32_t GL_FALSE = 0;
+
+namespace ui {
 
 namespace {
 
@@ -47,17 +83,19 @@
 }  // anonymous namespace
 
 // static
-GLVisualPickerGLX* GLVisualPickerGLX::GetInstance() {
-  return base::Singleton<GLVisualPickerGLX>::get();
+VisualPickerGlx* VisualPickerGlx::GetInstance() {
+  return base::Singleton<VisualPickerGlx>::get();
 }
 
-x11::Glx::FbConfig GLVisualPickerGLX::GetFbConfigForFormat(
-    gfx::BufferFormat format) const {
-  auto it = config_map_.find(format);
-  return it == config_map_.end() ? x11::Glx::FbConfig{} : it->second;
+x11::Glx::FbConfig VisualPickerGlx::GetFbConfigForFormat(
+    gfx::BufferFormat format) {
+  if (!config_map_)
+    FillConfigMap();
+  auto it = config_map_->find(format);
+  return it == config_map_->end() ? x11::Glx::FbConfig{} : it->second;
 }
 
-x11::VisualId GLVisualPickerGLX::PickBestGlVisual(
+x11::VisualId VisualPickerGlx::PickBestGlVisual(
     const x11::Glx::GetVisualConfigsReply& configs,
     base::RepeatingCallback<bool(const x11::Connection::VisualInfo&)> pred,
     bool want_alpha) const {
@@ -129,7 +167,7 @@
   return best_visual;
 }
 
-x11::VisualId GLVisualPickerGLX::PickBestSystemVisual(
+x11::VisualId VisualPickerGlx::PickBestSystemVisual(
     const x11::Glx::GetVisualConfigsReply& configs) const {
   x11::Connection::VisualInfo default_visual_info =
       *connection_->GetVisualInfoFromId(
@@ -154,7 +192,7 @@
       IsArgbVisual(default_visual_info));
 }
 
-x11::VisualId GLVisualPickerGLX::PickBestRgbaVisual(
+x11::VisualId VisualPickerGlx::PickBestRgbaVisual(
     const x11::Glx::GetVisualConfigsReply& configs) const {
   int best_class_score = -1;
   for (const auto& depth : connection_->default_screen().allowed_depths) {
@@ -171,9 +209,10 @@
                           true);
 }
 
-void GLVisualPickerGLX::FillConfigMap() {
-  if (!GLSurfaceGLX::HasGLXExtension("GLX_EXT_texture_from_pixmap"))
-    return;
+void VisualPickerGlx::FillConfigMap() {
+  DCHECK(!config_map_);
+  config_map_ =
+      std::make_unique<base::flat_map<gfx::BufferFormat, x11::Glx::FbConfig>>();
 
   if (auto configs = connection_->glx()
                          .GetFBConfigs({connection_->DefaultScreenId()})
@@ -235,18 +274,18 @@
       auto b = get(GLX_BLUE_SIZE);
       auto a = get(GLX_ALPHA_SIZE);
       if (r == 5 && g == 6 && b == 5 && a == 0)
-        config_map_[gfx::BufferFormat::BGR_565] = fbconfig;
+        (*config_map_)[gfx::BufferFormat::BGR_565] = fbconfig;
       else if (r == 8 && g == 8 && b == 8 && a == 0)
-        config_map_[gfx::BufferFormat::BGRX_8888] = fbconfig;
+        (*config_map_)[gfx::BufferFormat::BGRX_8888] = fbconfig;
       else if (r == 10 && g == 10 && b == 10 && a == 0)
-        config_map_[gfx::BufferFormat::BGRA_1010102] = fbconfig;
+        (*config_map_)[gfx::BufferFormat::BGRA_1010102] = fbconfig;
       else if (r == 8 && g == 8 && b == 8 && a == 8)
-        config_map_[gfx::BufferFormat::BGRA_8888] = fbconfig;
+        (*config_map_)[gfx::BufferFormat::BGRA_8888] = fbconfig;
     }
   }
 }
 
-GLVisualPickerGLX::GLVisualPickerGLX() : connection_(x11::Connection::Get()) {
+VisualPickerGlx::VisualPickerGlx() : connection_(x11::Connection::Get()) {
   auto configs = connection_->glx()
                      .GetVisualConfigs({connection_->DefaultScreenId()})
                      .Sync();
@@ -255,10 +294,8 @@
     system_visual_ = PickBestSystemVisual(*configs.reply);
     rgba_visual_ = PickBestRgbaVisual(*configs.reply);
   }
-
-  FillConfigMap();
 }
 
-GLVisualPickerGLX::~GLVisualPickerGLX() = default;
+VisualPickerGlx::~VisualPickerGlx() = default;
 
-}  // namespace gl
+}  // namespace ui
diff --git a/ui/gl/gl_visual_picker_glx.h b/ui/base/x/visual_picker_glx.h
similarity index 71%
rename from ui/gl/gl_visual_picker_glx.h
rename to ui/base/x/visual_picker_glx.h
index 5a48f9b..cd4772c 100644
--- a/ui/gl/gl_visual_picker_glx.h
+++ b/ui/base/x/visual_picker_glx.h
@@ -2,43 +2,42 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef UI_GL_GL_VISUAL_PICKER_GLX_H_
-#define UI_GL_GL_VISUAL_PICKER_GLX_H_
+#ifndef UI_BASE_X_VISUAL_PICKER_GLX_H_
+#define UI_BASE_X_VISUAL_PICKER_GLX_H_
 
-
+#include "base/component_export.h"
 #include "base/containers/flat_map.h"
 #include "base/macros.h"
 #include "ui/gfx/buffer_types.h"
 #include "ui/gfx/x/connection.h"
 #include "ui/gfx/x/glx.h"
-#include "ui/gl/gl_export.h"
 
 namespace base {
 template <typename T>
 struct DefaultSingletonTraits;
 }
 
-namespace gl {
+namespace ui {
 
 // Picks the best X11 visuals to use for GL.  This class is adapted from GTK's
 // pick_better_visual_for_gl.  Tries to find visuals that
 // 1. Support GL
 // 2. Support double buffer
 // 3. Have an alpha channel only if we want one
-class GL_EXPORT GLVisualPickerGLX {
+class COMPONENT_EXPORT(UI_BASE_X) VisualPickerGlx {
  public:
-  static GLVisualPickerGLX* GetInstance();
+  static VisualPickerGlx* GetInstance();
 
-  ~GLVisualPickerGLX();
+  ~VisualPickerGlx();
 
   x11::VisualId system_visual() const { return system_visual_; }
 
   x11::VisualId rgba_visual() const { return rgba_visual_; }
 
-  x11::Glx::FbConfig GetFbConfigForFormat(gfx::BufferFormat format) const;
+  x11::Glx::FbConfig GetFbConfigForFormat(gfx::BufferFormat format);
 
  private:
-  friend struct base::DefaultSingletonTraits<GLVisualPickerGLX>;
+  friend struct base::DefaultSingletonTraits<VisualPickerGlx>;
 
   x11::VisualId PickBestGlVisual(
       const x11::Glx::GetVisualConfigsReply& configs,
@@ -58,13 +57,14 @@
   x11::VisualId system_visual_;
   x11::VisualId rgba_visual_;
 
-  base::flat_map<gfx::BufferFormat, x11::Glx::FbConfig> config_map_;
+  std::unique_ptr<base::flat_map<gfx::BufferFormat, x11::Glx::FbConfig>>
+      config_map_;
 
-  GLVisualPickerGLX();
+  VisualPickerGlx();
 
-  DISALLOW_COPY_AND_ASSIGN(GLVisualPickerGLX);
+  DISALLOW_COPY_AND_ASSIGN(VisualPickerGlx);
 };
 
-}  // namespace gl
+}  // namespace ui
 
-#endif  // UI_GL_GL_VISUAL_PICKER_GLX_H_
+#endif  // UI_BASE_X_VISUAL_PICKER_GLX_H_
diff --git a/ui/base/x/x11_gl_egl_utility.cc b/ui/base/x/x11_gl_egl_utility.cc
index ae54288c..624c6f9 100644
--- a/ui/base/x/x11_gl_egl_utility.cc
+++ b/ui/base/x/x11_gl_egl_utility.cc
@@ -39,20 +39,8 @@
 
 void GetPlatformExtraDisplayAttribs(EGLenum platform_type,
                                     std::vector<EGLAttrib>* attributes) {
-  // ANGLE_NULL and SwiftShader backends don't use the visual,
-  // and may run without X11 where we can't get it anyway.
-  if ((platform_type != EGL_PLATFORM_ANGLE_TYPE_NULL_ANGLE) &&
-      (std::find(attributes->begin(), attributes->end(),
-                 EGL_PLATFORM_ANGLE_DEVICE_TYPE_SWIFTSHADER_ANGLE) ==
-       attributes->end())) {
-    x11::VisualId visual_id;
-    XVisualManager::GetInstance()->ChooseVisualForWindow(
-        true, &visual_id, nullptr, nullptr, nullptr);
-    attributes->push_back(EGL_X11_VISUAL_ID_ANGLE);
-    attributes->push_back(static_cast<EGLAttrib>(visual_id));
-    attributes->push_back(EGL_PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE);
-    attributes->push_back(EGL_PLATFORM_X11_EXT);
-  }
+  attributes->push_back(EGL_PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE);
+  attributes->push_back(EGL_PLATFORM_X11_EXT);
 }
 
 void ChoosePlatformCustomAlphaAndBufferSize(EGLint* alpha_size,
@@ -72,11 +60,4 @@
   return ui::XVisualManager::GetInstance()->ArgbVisualAvailable();
 }
 
-bool UpdateVisualsOnGpuInfoChanged(bool software_rendering,
-                                   x11::VisualId default_visual_id,
-                                   x11::VisualId transparent_visual_id) {
-  return XVisualManager::GetInstance()->UpdateVisualsOnGpuInfoChanged(
-      software_rendering, default_visual_id, transparent_visual_id);
-}
-
 }  // namespace ui
diff --git a/ui/base/x/x11_gl_egl_utility.h b/ui/base/x/x11_gl_egl_utility.h
index fd33bbb..8f559f2 100644
--- a/ui/base/x/x11_gl_egl_utility.h
+++ b/ui/base/x/x11_gl_egl_utility.h
@@ -8,7 +8,6 @@
 #include <vector>
 
 #include "third_party/khronos/EGL/egl.h"
-#include "ui/gfx/x/xproto.h"
 
 namespace ui {
 
@@ -23,12 +22,6 @@
 // Returns whether transparent background is suppored.
 bool IsTransparentBackgroundSupported();
 
-// Wraps XVisualManager::UpdateVisualsOnGpuInfoChanged(), passes parameters to
-// it directly. Returns whether provided visuals are valid.
-bool UpdateVisualsOnGpuInfoChanged(bool software_rendering,
-                                   x11::VisualId default_visual_id,
-                                   x11::VisualId transparent_visual_id);
-
 }  // namespace ui
 
 #endif  // UI_BASE_X_X11_GL_EGL_UTILITY_H_
diff --git a/ui/base/x/x11_util.cc b/ui/base/x/x11_util.cc
index ff558ea..e96116b 100644
--- a/ui/base/x/x11_util.cc
+++ b/ui/base/x/x11_util.cc
@@ -49,6 +49,7 @@
 #include "third_party/skia/include/core/SkImageInfo.h"
 #include "third_party/skia/include/core/SkTypes.h"
 #include "ui/base/cursor/mojom/cursor_type.mojom-shared.h"
+#include "ui/base/x/visual_picker_glx.h"
 #include "ui/base/x/x11_cursor.h"
 #include "ui/base/x/x11_cursor_loader.h"
 #include "ui/base/x/x11_menu_list.h"
@@ -1006,14 +1007,10 @@
 }
 
 bool DoesVisualHaveAlphaForTest() {
-  // testing/xvfb.py runs xvfb and xcompmgr.
-  std::unique_ptr<base::Environment> env(base::Environment::Create());
-
   uint8_t depth = 0;
   bool visual_has_alpha = false;
   ui::XVisualManager::GetInstance()->ChooseVisualForWindow(
-      env->HasVar("_CHROMIUM_INSIDE_XVFB"), nullptr, &depth, nullptr,
-      &visual_has_alpha);
+      true, nullptr, &depth, nullptr, &visual_has_alpha);
 
   if (visual_has_alpha)
     DCHECK_EQ(32, depth);
@@ -1069,36 +1066,48 @@
   return base::Singleton<XVisualManager>::get();
 }
 
-XVisualManager::XVisualManager() : connection_(x11::Connection::Get()) {
-  base::AutoLock lock(lock_);
-
-  for (const auto& depth : connection_->default_screen().allowed_depths) {
+XVisualManager::XVisualManager() {
+  auto* connection = x11::Connection::Get();
+  for (const auto& depth : connection->default_screen().allowed_depths) {
     for (const auto& visual : depth.visuals) {
       visuals_[visual.visual_id] =
-          std::make_unique<XVisualData>(connection_, depth.depth, &visual);
+          std::make_unique<XVisualData>(connection, depth.depth, &visual);
     }
   }
 
+  auto* visual_picker = VisualPickerGlx::GetInstance();
+  x11::ColorMap colormap;
+
   // Choose the opaque visual.
-  default_visual_id_ = connection_->default_screen().root_visual;
-  system_visual_id_ = default_visual_id_;
-  DCHECK_NE(system_visual_id_, x11::VisualId{});
-  DCHECK(visuals_.find(system_visual_id_) != visuals_.end());
+  opaque_visual_id_ = visual_picker->system_visual();
+  if (opaque_visual_id_ == x11::VisualId{})
+    opaque_visual_id_ = connection->default_screen().root_visual;
+  // opaque_visual_id_ may be unset in headless environments
+  if (opaque_visual_id_ != x11::VisualId{}) {
+    DCHECK(visuals_.find(opaque_visual_id_) != visuals_.end());
+    ChooseVisualForWindow(false, nullptr, nullptr, &colormap, nullptr);
+  }
 
   // Choose the transparent visual.
-  for (const auto& pair : visuals_) {
-    // Why support only 8888 ARGB? Because it's all that GTK+ supports. In
-    // gdkvisual-x11.cc, they look for this specific visual and use it for
-    // all their alpha channel using needs.
-    const auto& data = *pair.second;
-    if (data.depth == 32 && data.info->red_mask == 0xff0000 &&
-        data.info->green_mask == 0x00ff00 && data.info->blue_mask == 0x0000ff) {
-      transparent_visual_id_ = pair.first;
-      break;
+  transparent_visual_id_ = visual_picker->rgba_visual();
+  if (transparent_visual_id_ == x11::VisualId{}) {
+    for (const auto& pair : visuals_) {
+      // Why support only 8888 ARGB? Because it's all that GTK+ supports. In
+      // gdkvisual-x11.cc, they look for this specific visual and use it for
+      // all their alpha channel using needs.
+      const auto& data = *pair.second;
+      if (data.depth == 32 && data.info->red_mask == 0xff0000 &&
+          data.info->green_mask == 0x00ff00 &&
+          data.info->blue_mask == 0x0000ff) {
+        transparent_visual_id_ = pair.first;
+        break;
+      }
     }
   }
-  if (transparent_visual_id_ != x11::VisualId{})
+  if (transparent_visual_id_ != x11::VisualId{}) {
     DCHECK(visuals_.find(transparent_visual_id_) != visuals_.end());
+    ChooseVisualForWindow(true, nullptr, nullptr, &colormap, nullptr);
+  }
 }
 
 XVisualManager::~XVisualManager() = default;
@@ -1108,16 +1117,12 @@
                                            uint8_t* depth,
                                            x11::ColorMap* colormap,
                                            bool* visual_has_alpha) {
-  base::AutoLock lock(lock_);
-  bool use_argb = want_argb_visual && IsCompositingManagerPresent() &&
-                  (using_software_rendering_ || have_gpu_argb_visual_);
-  x11::VisualId visual = use_argb && transparent_visual_id_ != x11::VisualId{}
-                             ? transparent_visual_id_
-                             : system_visual_id_;
+  bool use_argb = want_argb_visual && ArgbVisualAvailable();
+  x11::VisualId visual = use_argb ? transparent_visual_id_ : opaque_visual_id_;
 
   if (visual_id)
     *visual_id = visual;
-  bool success = GetVisualInfoImpl(visual, depth, colormap, visual_has_alpha);
+  bool success = GetVisualInfo(visual, depth, colormap, visual_has_alpha);
   DCHECK(success);
 }
 
@@ -1125,54 +1130,20 @@
                                    uint8_t* depth,
                                    x11::ColorMap* colormap,
                                    bool* visual_has_alpha) {
-  base::AutoLock lock(lock_);
-  return GetVisualInfoImpl(visual_id, depth, colormap, visual_has_alpha);
-}
-
-bool XVisualManager::UpdateVisualsOnGpuInfoChanged(
-    bool software_rendering,
-    x11::VisualId system_visual_id,
-    x11::VisualId transparent_visual_id) {
-  base::AutoLock lock(lock_);
-  // TODO(thomasanderson): Cache these visual IDs as a property of the root
-  // window so that newly created browser processes can get them immediately.
-  if ((system_visual_id != x11::VisualId{} &&
-       !visuals_.count(system_visual_id)) ||
-      (transparent_visual_id != x11::VisualId{} &&
-       !visuals_.count(transparent_visual_id)))
-    return false;
-  using_software_rendering_ = software_rendering;
-  have_gpu_argb_visual_ =
-      have_gpu_argb_visual_ || transparent_visual_id != x11::VisualId{};
-  if (system_visual_id != x11::VisualId{})
-    system_visual_id_ = system_visual_id;
-  if (transparent_visual_id != x11::VisualId{})
-    transparent_visual_id_ = transparent_visual_id;
-  return true;
-}
-
-bool XVisualManager::ArgbVisualAvailable() const {
-  base::AutoLock lock(lock_);
-  return IsCompositingManagerPresent() &&
-         (using_software_rendering_ || have_gpu_argb_visual_);
-}
-
-bool XVisualManager::GetVisualInfoImpl(x11::VisualId visual_id,
-                                       uint8_t* depth,
-                                       x11::ColorMap* colormap,
-                                       bool* visual_has_alpha) {
+  DCHECK_NE(visual_id, x11::VisualId{});
   auto it = visuals_.find(visual_id);
   if (it == visuals_.end())
     return false;
   XVisualData& data = *it->second;
   const x11::VisualType& info = *data.info;
 
-  bool is_default_visual = visual_id == default_visual_id_;
-
   if (depth)
     *depth = data.depth;
-  if (colormap)
+  if (colormap) {
+    bool is_default_visual =
+        visual_id == x11::Connection::Get()->default_root_visual().visual_id;
     *colormap = is_default_visual ? x11::ColorMap{} : data.GetColormap();
+  }
   if (visual_has_alpha) {
     auto popcount = [](auto x) {
       return std::bitset<8 * sizeof(decltype(x))>(x).count();
@@ -1184,10 +1155,15 @@
   return true;
 }
 
+bool XVisualManager::ArgbVisualAvailable() const {
+  return IsCompositingManagerPresent() &&
+         transparent_visual_id_ != x11::VisualId{};
+}
+
 XVisualManager::XVisualData::XVisualData(x11::Connection* connection,
                                          uint8_t depth,
                                          const x11::VisualType* info)
-    : depth(depth), info(info), connection_(connection) {}
+    : depth(depth), info(info) {}
 
 // Do not free the colormap as this would uninstall the colormap even for
 // non-Chromium clients.
@@ -1195,9 +1171,10 @@
 
 x11::ColorMap XVisualManager::XVisualData::GetColormap() {
   if (colormap_ == x11::ColorMap{}) {
-    colormap_ = connection_->GenerateId<x11::ColorMap>();
-    connection_->CreateColormap({x11::ColormapAlloc::None, colormap_,
-                                 connection_->default_root(), info->visual_id});
+    auto* connection = x11::Connection::Get();
+    colormap_ = connection->GenerateId<x11::ColorMap>();
+    connection->CreateColormap({x11::ColormapAlloc::None, colormap_,
+                                connection->default_root(), info->visual_id});
   }
   return colormap_;
 }
diff --git a/ui/base/x/x11_util.h b/ui/base/x/x11_util.h
index 14be4ae8..bf38c1fb 100644
--- a/ui/base/x/x11_util.h
+++ b/ui/base/x/x11_util.h
@@ -40,7 +40,6 @@
 namespace base {
 template <typename T>
 struct DefaultSingletonTraits;
-class Value;
 }
 
 namespace gfx {
@@ -378,13 +377,6 @@
 // Suspends or resumes the X screen saver.  Must be called on the UI thread.
 COMPONENT_EXPORT(UI_BASE_X) void SuspendX11ScreenSaver(bool suspend);
 
-// Returns human readable description of the window manager, desktop, and
-// other system properties related to the compositing.
-COMPONENT_EXPORT(UI_BASE_X)
-void StoreGpuExtraInfoIntoListValue(x11::VisualId system_visual,
-                                    x11::VisualId rgba_visual,
-                                    base::Value& list_value);
-
 // Returns true if the window manager supports the given hint.
 COMPONENT_EXPORT(UI_BASE_X) bool WmSupportsHint(x11::Atom atom);
 
@@ -412,8 +404,7 @@
 // Return true if VulkanSurface is supported.
 COMPONENT_EXPORT(UI_BASE_X) bool IsVulkanSurfaceSupported();
 
-// Returns whether the visual supports alpha.
-// The function examines the _CHROMIUM_INSIDE_XVFB environment variable.
+// Returns whether ARGB visuals are supported.
 COMPONENT_EXPORT(UI_BASE_X) bool DoesVisualHaveAlphaForTest();
 
 // Returns an icon for a native window referred by |target_window_id|. Can be
@@ -440,14 +431,6 @@
                      x11::ColorMap* colormap,
                      bool* visual_has_alpha);
 
-  // Called by GpuDataManagerImplPrivate when GPUInfo becomes available.  It is
-  // necessary for the GPU process to find out which visuals are best for GL
-  // because we don't want to load GL in the browser process.  Returns false iff
-  // |default_visual_id| or |transparent_visual_id| are invalid.
-  bool UpdateVisualsOnGpuInfoChanged(bool software_rendering,
-                                     x11::VisualId default_visual_id,
-                                     x11::VisualId transparent_visual_id);
-
   // Are all of the system requirements met for using transparent visuals?
   bool ArgbVisualAvailable() const;
 
@@ -470,32 +453,15 @@
 
    private:
     x11::ColorMap colormap_{};
-    x11::Connection* const connection_;
   };
 
   XVisualManager();
 
-  bool GetVisualInfoImpl(x11::VisualId visual_id,
-                         uint8_t* depth,
-                         x11::ColorMap* colormap,
-                         bool* visual_has_alpha);
-
-  mutable base::Lock lock_;
-
   std::unordered_map<x11::VisualId, std::unique_ptr<XVisualData>> visuals_;
 
-  x11::Connection* const connection_;
-
-  x11::VisualId default_visual_id_{};
-
-  // The system visual is usually the same as the default visual, but
-  // may not be in general.
-  x11::VisualId system_visual_id_{};
+  x11::VisualId opaque_visual_id_{};
   x11::VisualId transparent_visual_id_{};
 
-  bool using_software_rendering_ = false;
-  bool have_gpu_argb_visual_ = false;
-
   DISALLOW_COPY_AND_ASSIGN(XVisualManager);
 };
 
diff --git a/ui/gfx/gpu_extra_info.h b/ui/gfx/gpu_extra_info.h
index 98234d2..c895145 100644
--- a/ui/gfx/gpu_extra_info.h
+++ b/ui/gfx/gpu_extra_info.h
@@ -66,9 +66,6 @@
   ANGLEFeatures angle_features;
 
 #if defined(USE_X11) || defined(USE_OZONE_PLATFORM_X11)
-  x11::VisualId system_visual{};
-  x11::VisualId rgba_visual{};
-
   std::vector<gfx::BufferUsageAndFormat> gpu_memory_buffer_support_x11;
 #endif
 };
diff --git a/ui/gfx/mojom/gpu_extra_info.mojom b/ui/gfx/mojom/gpu_extra_info.mojom
index 705cc683..1b7b023 100644
--- a/ui/gfx/mojom/gpu_extra_info.mojom
+++ b/ui/gfx/mojom/gpu_extra_info.mojom
@@ -23,9 +23,5 @@
   array<ANGLEFeature> angle_features;
 
   [EnableIf=enable_x11_params]
-  uint64 system_visual;
-  [EnableIf=enable_x11_params]
-  uint64 rgba_visual;
-  [EnableIf=enable_x11_params]
   array<gfx.mojom.BufferUsageAndFormat> gpu_memory_buffer_support_x11;
 };
diff --git a/ui/gfx/mojom/gpu_extra_info_mojom_traits.cc b/ui/gfx/mojom/gpu_extra_info_mojom_traits.cc
index 6b7ff134e..c40726b 100644
--- a/ui/gfx/mojom/gpu_extra_info_mojom_traits.cc
+++ b/ui/gfx/mojom/gpu_extra_info_mojom_traits.cc
@@ -25,14 +25,6 @@
   if (!data.ReadAngleFeatures(&out->angle_features))
     return false;
 #if defined(USE_OZONE_PLATFORM_X11) || defined(USE_X11)
-  // These visuals below are obtained via methods of gl::GLVisualPickerGLX class
-  // and consumed by ui::XVisualManager::UpdateVisualsOnGpuInfoChanged(); should
-  // bad visuals come there, the GPU process will be shut down.
-  //
-  // See content::GpuDataManagerVisualProxyOzoneLinux and the ShutdownGpuOnIO()
-  // function there.
-  out->system_visual = static_cast<x11::VisualId>(data.system_visual());
-  out->rgba_visual = static_cast<x11::VisualId>(data.rgba_visual());
   if (!data.ReadGpuMemoryBufferSupportX11(&out->gpu_memory_buffer_support_x11))
     return false;
 #endif
diff --git a/ui/gfx/mojom/gpu_extra_info_mojom_traits.h b/ui/gfx/mojom/gpu_extra_info_mojom_traits.h
index 40a1114c..dd6b118 100644
--- a/ui/gfx/mojom/gpu_extra_info_mojom_traits.h
+++ b/ui/gfx/mojom/gpu_extra_info_mojom_traits.h
@@ -62,14 +62,6 @@
   }
 
 #if defined(USE_OZONE_PLATFORM_X11) || defined(USE_X11)
-  static uint64_t system_visual(const gfx::GpuExtraInfo& input) {
-    return static_cast<uint64_t>(input.system_visual);
-  }
-
-  static uint64_t rgba_visual(const gfx::GpuExtraInfo& input) {
-    return static_cast<uint64_t>(input.rgba_visual);
-  }
-
   static const std::vector<gfx::BufferUsageAndFormat>&
   gpu_memory_buffer_support_x11(const gfx::GpuExtraInfo& input) {
     return input.gpu_memory_buffer_support_x11;
diff --git a/ui/gl/BUILD.gn b/ui/gl/BUILD.gn
index 2e5fff3..20e98943 100644
--- a/ui/gl/BUILD.gn
+++ b/ui/gl/BUILD.gn
@@ -290,8 +290,6 @@
       "gl_image_glx_native_pixmap.h",
       "gl_surface_glx.cc",
       "gl_surface_glx.h",
-      "gl_visual_picker_glx.cc",
-      "gl_visual_picker_glx.h",
       "glx_util.cc",
       "glx_util.h",
     ]
diff --git a/ui/gl/gl_image_egl_pixmap.cc b/ui/gl/gl_image_egl_pixmap.cc
index 6c5612d..ac11b6d 100644
--- a/ui/gl/gl_image_egl_pixmap.cc
+++ b/ui/gl/gl_image_egl_pixmap.cc
@@ -6,10 +6,10 @@
 
 #include <memory>
 
+#include "ui/gfx/x/connection.h"
 #include "ui/gl/buffer_format_utils.h"
 #include "ui/gl/gl_bindings.h"
 #include "ui/gl/gl_surface_glx.h"
-#include "ui/gl/gl_visual_picker_glx.h"
 
 namespace gl {
 
diff --git a/ui/gl/gl_image_glx.cc b/ui/gl/gl_image_glx.cc
index 01f95ed..29d182e 100644
--- a/ui/gl/gl_image_glx.cc
+++ b/ui/gl/gl_image_glx.cc
@@ -7,11 +7,11 @@
 #include <memory>
 
 #include "base/logging.h"
+#include "ui/base/x/visual_picker_glx.h"
 #include "ui/gl/buffer_format_utils.h"
 #include "ui/gl/gl_bindings.h"
 #include "ui/gl/gl_image_glx.h"
 #include "ui/gl/gl_surface_glx.h"
-#include "ui/gl/gl_visual_picker_glx.h"
 #include "ui/gl/glx_util.h"
 
 namespace gl {
@@ -46,7 +46,7 @@
 
 bool GLImageGLX::Initialize(x11::Pixmap pixmap) {
   auto fbconfig_id =
-      GLVisualPickerGLX::GetInstance()->GetFbConfigForFormat(format_);
+      ui::VisualPickerGlx::GetInstance()->GetFbConfigForFormat(format_);
 
   auto* connection = x11::Connection::Get();
   GLXFBConfig config = GetGlxFbConfigForXProtoFbConfig(connection, fbconfig_id);
diff --git a/ui/gl/gl_implementation.cc b/ui/gl/gl_implementation.cc
index 5acbd036..3a1baf0d 100644
--- a/ui/gl/gl_implementation.cc
+++ b/ui/gl/gl_implementation.cc
@@ -205,6 +205,19 @@
   }
 }
 
+void SetSoftwareWebGLCommandLineSwitches(base::CommandLine* command_line,
+                                         bool legacy_software_gl) {
+  if (legacy_software_gl) {
+    command_line->AppendSwitchASCII(switches::kUseGL,
+                                    kGLImplementationSwiftShaderForWebGLName);
+  } else {
+    command_line->AppendSwitchASCII(switches::kUseGL,
+                                    kGLImplementationANGLEName);
+    command_line->AppendSwitchASCII(
+        switches::kUseANGLE, kANGLEImplementationSwiftShaderForWebGLName);
+  }
+}
+
 const char* GetGLImplementationGLName(GLImplementationParts implementation) {
   for (auto name_pair : kGLImplementationNamePairs) {
     if (implementation.gl == name_pair.implementation.gl &&
diff --git a/ui/gl/gl_implementation.h b/ui/gl/gl_implementation.h
index 2c34023..054d2c29 100644
--- a/ui/gl/gl_implementation.h
+++ b/ui/gl/gl_implementation.h
@@ -140,6 +140,11 @@
 GL_EXPORT void SetSoftwareGLCommandLineSwitches(base::CommandLine* command_line,
                                                 bool legacy_software_gl);
 
+// Set the software WebGL implementation on the provided command line
+GL_EXPORT void SetSoftwareWebGLCommandLineSwitches(
+    base::CommandLine* command_line,
+    bool legacy_software_gl);
+
 // Whether the implementation is one of the software GL implementations
 GL_EXPORT bool IsSoftwareGLImplementation(GLImplementationParts implementation);
 
diff --git a/ui/gl/gl_surface_glx.cc b/ui/gl/gl_surface_glx.cc
index 6a8e253..1e40d49 100644
--- a/ui/gl/gl_surface_glx.cc
+++ b/ui/gl/gl_surface_glx.cc
@@ -22,6 +22,7 @@
 #include "base/time/time.h"
 #include "base/trace_event/trace_event.h"
 #include "build/build_config.h"
+#include "ui/base/x/visual_picker_glx.h"
 #include "ui/base/x/x11_display_util.h"
 #include "ui/base/x/x11_util.h"
 #include "ui/base/x/x11_xrandr_interval_only_vsync_provider.h"
@@ -32,7 +33,6 @@
 #include "ui/gl/gl_context.h"
 #include "ui/gl/gl_implementation.h"
 #include "ui/gl/gl_surface_presentation_helper.h"
-#include "ui/gl/gl_visual_picker_glx.h"
 #include "ui/gl/glx_util.h"
 #include "ui/gl/sync_control_vsync_provider.h"
 
@@ -437,7 +437,7 @@
     return false;
   }
 
-  auto* visual_picker = gl::GLVisualPickerGLX::GetInstance();
+  auto* visual_picker = ui::VisualPickerGlx::GetInstance();
   auto visual_id = visual_picker->rgba_visual();
   if (visual_id == x11::VisualId{})
     visual_id = visual_picker->system_visual();
diff --git a/ui/gl/gl_utils.cc b/ui/gl/gl_utils.cc
index 77d3d1a..70fa53a 100644
--- a/ui/gl/gl_utils.cc
+++ b/ui/gl/gl_utils.cc
@@ -27,9 +27,10 @@
 #endif
 
 #if defined(USE_X11) || defined(USE_OZONE_PLATFORM_X11)
+#include "ui/base/x/visual_picker_glx.h"                 // nogncheck
 #include "ui/gfx/linux/gpu_memory_buffer_support_x11.h"  // nogncheck
+#include "ui/gfx/x/glx.h"                                // nogncheck
 #include "ui/gl/gl_implementation.h"                     // nogncheck
-#include "ui/gl/gl_visual_picker_glx.h"                  // nogncheck
 #endif
 
 namespace gl {
@@ -163,12 +164,9 @@
   }
 
   if (GetGLImplementation() == kGLImplementationDesktopGL) {
-    // Create the GLVisualPickerGLX singleton now while the GbmSupportX11
+    // Create the VisualPickerGlx singleton now while the GbmSupportX11
     // singleton is busy being created on another thread.
-    GLVisualPickerGLX* visual_picker = GLVisualPickerGLX::GetInstance();
-
-    info.system_visual = visual_picker->system_visual();
-    info.rgba_visual = visual_picker->rgba_visual();
+    auto* visual_picker = ui::VisualPickerGlx::GetInstance();
 
     // With GLX, only BGR(A) buffer formats are supported.  EGL does not have
     // this restriction.
diff --git a/ui/gtk/BUILD.gn b/ui/gtk/BUILD.gn
index d83a802..7bdb5a0 100644
--- a/ui/gtk/BUILD.gn
+++ b/ui/gtk/BUILD.gn
@@ -90,7 +90,6 @@
   deps = [
     ":gtk_stubs",
     "//base",
-    "//printing",
     "//skia",
 
     # GTK pulls pangoft2, which requires HarfBuzz symbols. When linking
@@ -118,6 +117,10 @@
     "//ui/views",
   ]
 
+  if (enable_basic_printing) {
+    deps += [ "//printing" ]
+  }
+
   if (use_cups) {
     deps += [ "//printing/mojom" ]
   }
diff --git a/ui/gtk/gtk_ui.cc b/ui/gtk/gtk_ui.cc
index dcfee840..38f049c 100644
--- a/ui/gtk/gtk_ui.cc
+++ b/ui/gtk/gtk_ui.cc
@@ -22,7 +22,7 @@
 #include "base/stl_util.h"
 #include "base/strings/string_split.h"
 #include "chrome/browser/themes/theme_properties.h"  // nogncheck
-#include "printing/buildflags/buildflags.h"
+#include "printing/buildflags/buildflags.h"          // nogncheck
 #include "third_party/skia/include/core/SkBitmap.h"
 #include "third_party/skia/include/core/SkCanvas.h"
 #include "third_party/skia/include/core/SkColor.h"
diff --git a/ui/gtk/printing/print_dialog_gtk.cc b/ui/gtk/printing/print_dialog_gtk.cc
index 5e7dd30..62108f0 100644
--- a/ui/gtk/printing/print_dialog_gtk.cc
+++ b/ui/gtk/printing/print_dialog_gtk.cc
@@ -33,7 +33,7 @@
 #include "ui/gtk/printing/printing_gtk_util.h"
 
 #if defined(USE_CUPS)
-#include "printing/mojom/print.mojom.h"
+#include "printing/mojom/print.mojom.h"  // nogncheck
 #endif
 
 using printing::PageRanges;
diff --git a/ui/ozone/platform/x11/gl_egl_utility_x11.cc b/ui/ozone/platform/x11/gl_egl_utility_x11.cc
index d5612033..387e275 100644
--- a/ui/ozone/platform/x11/gl_egl_utility_x11.cc
+++ b/ui/ozone/platform/x11/gl_egl_utility_x11.cc
@@ -42,13 +42,4 @@
   return true;
 }
 
-bool GLEGLUtilityX11::UpdateVisualsOnGpuInfoChanged(
-    bool software_rendering,
-    uint32_t default_visual_id,
-    uint32_t transparent_visual_id) {
-  return ui::UpdateVisualsOnGpuInfoChanged(
-      software_rendering, static_cast<x11::VisualId>(default_visual_id),
-      static_cast<x11::VisualId>(transparent_visual_id));
-}
-
 }  // namespace ui
diff --git a/ui/ozone/platform/x11/gl_egl_utility_x11.h b/ui/ozone/platform/x11/gl_egl_utility_x11.h
index 1ca2ddf3..da5e2968 100644
--- a/ui/ozone/platform/x11/gl_egl_utility_x11.h
+++ b/ui/ozone/platform/x11/gl_egl_utility_x11.h
@@ -28,9 +28,6 @@
                            gfx::GpuExtraInfo& gpu_extra_info) const override;
   bool X11DoesVisualHaveAlphaForTest() const override;
   bool HasVisualManager() override;
-  bool UpdateVisualsOnGpuInfoChanged(bool software_rendering,
-                                     uint32_t default_visual_id,
-                                     uint32_t transparent_visual_id) override;
 };
 
 }  // namespace ui
diff --git a/ui/ozone/platform/x11/x11_screen_ozone.cc b/ui/ozone/platform/x11/x11_screen_ozone.cc
index b17a53ae..7b3ab857 100644
--- a/ui/ozone/platform/x11/x11_screen_ozone.cc
+++ b/ui/ozone/platform/x11/x11_screen_ozone.cc
@@ -134,8 +134,6 @@
 base::Value X11ScreenOzone::GetGpuExtraInfoAsListValue(
     const gfx::GpuExtraInfo& gpu_extra_info) {
   auto result = GetDesktopEnvironmentInfoAsListValue();
-  StoreGpuExtraInfoIntoListValue(gpu_extra_info.system_visual,
-                                 gpu_extra_info.rgba_visual, result);
   StorePlatformNameIntoListValue(result, "x11");
   return result;
 }
diff --git a/ui/ozone/public/platform_gl_egl_utility.cc b/ui/ozone/public/platform_gl_egl_utility.cc
index 38a135e..756334b 100644
--- a/ui/ozone/public/platform_gl_egl_utility.cc
+++ b/ui/ozone/public/platform_gl_egl_utility.cc
@@ -17,13 +17,4 @@
   return false;
 }
 
-bool PlatformGLEGLUtility::UpdateVisualsOnGpuInfoChanged(
-    bool software_rendering,
-    uint32_t default_visual_id,
-    uint32_t transparent_visual_id) {
-  NOTREACHED() << "This must not be called if the platform does not support "
-                  "X11 visuals.";
-  return false;
-}
-
 }  // namespace ui
diff --git a/ui/ozone/public/platform_gl_egl_utility.h b/ui/ozone/public/platform_gl_egl_utility.h
index 34d79ac..59728da 100644
--- a/ui/ozone/public/platform_gl_egl_utility.h
+++ b/ui/ozone/public/platform_gl_egl_utility.h
@@ -44,14 +44,6 @@
 
   // X11 specific; returns whether the platform supports visuals.
   virtual bool HasVisualManager();
-
-  // X11 specific; sets new visuals.
-  // Must be called only if the X11 visual manager is available.
-  // Should be called when the updated GPU info is available.
-  // Returns whether the visuals provided were valid.
-  virtual bool UpdateVisualsOnGpuInfoChanged(bool software_rendering,
-                                             uint32_t default_visual_id,
-                                             uint32_t transparent_visual_id);
 };
 
 }  // namespace ui
diff --git a/ui/platform_window/x11/x11_window.cc b/ui/platform_window/x11/x11_window.cc
index 4d8b7b7..ce480a8 100644
--- a/ui/platform_window/x11/x11_window.cc
+++ b/ui/platform_window/x11/x11_window.cc
@@ -207,8 +207,7 @@
 }
 
 void X11Window::Initialize(PlatformWindowInitProperties properties) {
-  PlatformWindowOpacity opacity = properties.opacity;
-  CreateXWindow(properties, opacity);
+  CreateXWindow(properties);
 
   // It can be a status icon window.  If it fails to initialize, don't provide
   // it with a native window handle, close ourselves and let the client destroy
@@ -400,10 +399,8 @@
   if (properties.icon)
     SetWindowIcons(gfx::ImageSkia(), *properties.icon);
 
-  if (properties.type == PlatformWindowType::kDrag &&
-      opacity == PlatformWindowOpacity::kTranslucentWindow) {
+  if (properties.type == PlatformWindowType::kDrag)
     SetOpacity(kDragWidgetOpacity);
-  }
 
   SetWmDragHandler(this, this);
 
@@ -961,10 +958,13 @@
 }
 
 bool X11Window::IsTranslucentWindowOpacitySupported() const {
-  // This function may be called before InitX11Window() (which
-  // initializes |visual_has_alpha_|), so we cannot simply return
-  // |visual_has_alpha_|.
-  return ui::XVisualManager::GetInstance()->ArgbVisualAvailable();
+  // If this function may be called before InitX11Window() (which
+  // initializes |visual_has_alpha_|), return whether it is possible
+  // to create windows with ARGB visuals.
+  if (xwindow_ == x11::Window::None)
+    ui::XVisualManager::GetInstance()->ArgbVisualAvailable();
+
+  return visual_has_alpha_;
 }
 
 void X11Window::SetOpacity(float opacity) {
@@ -1519,19 +1519,13 @@
                                              located_event);
 }
 
-void X11Window::CreateXWindow(const PlatformWindowInitProperties& properties,
-                              PlatformWindowOpacity& opacity) {
+void X11Window::CreateXWindow(const PlatformWindowInitProperties& properties) {
   auto bounds = properties.bounds;
   gfx::Size adjusted_size_in_pixels = AdjustSizeForDisplay(bounds.size());
   bounds.set_size(adjusted_size_in_pixels);
   const auto override_redirect =
       properties.x11_extension_delegate &&
       properties.x11_extension_delegate->IsOverrideRedirect(IsWmTiling());
-  if (properties.type == PlatformWindowType::kDrag) {
-    opacity = ui::IsCompositingManagerPresent()
-                  ? PlatformWindowOpacity::kTranslucentWindow
-                  : PlatformWindowOpacity::kOpaqueWindow;
-  }
 
   workspace_extension_delegate_ = properties.workspace_extension_delegate;
   x11_extension_delegate_ = properties.x11_extension_delegate;
@@ -1575,7 +1569,7 @@
   override_redirect_ = req.override_redirect.has_value();
 
   bool enable_transparent_visuals;
-  switch (opacity) {
+  switch (properties.opacity) {
     case PlatformWindowOpacity::kOpaqueWindow:
       enable_transparent_visuals = false;
       break;
diff --git a/ui/platform_window/x11/x11_window.h b/ui/platform_window/x11/x11_window.h
index dbaf7eba..b8b98f0b 100644
--- a/ui/platform_window/x11/x11_window.h
+++ b/ui/platform_window/x11/x11_window.h
@@ -220,8 +220,7 @@
   // Depending on presence of the compositing manager and window type, may
   // change the opacity, in which case returns the final opacity type through
   // |opacity|.
-  void CreateXWindow(const PlatformWindowInitProperties& properties,
-                     PlatformWindowOpacity& opacity);
+  void CreateXWindow(const PlatformWindowInitProperties& properties);
   void CloseXWindow();
   void Map(bool inactive = false);
   void SetFullscreen(bool fullscreen);
diff --git a/ui/views/widget/desktop_aura/desktop_screen_x11.cc b/ui/views/widget/desktop_aura/desktop_screen_x11.cc
index 4faed83e..956c0824 100644
--- a/ui/views/widget/desktop_aura/desktop_screen_x11.cc
+++ b/ui/views/widget/desktop_aura/desktop_screen_x11.cc
@@ -163,14 +163,6 @@
   return x11_display_manager_->GetCurrentWorkspace();
 }
 
-base::Value DesktopScreenX11::GetGpuExtraInfoAsListValue(
-    const gfx::GpuExtraInfo& gpu_extra_info) {
-  auto result = ui::GetDesktopEnvironmentInfoAsListValue();
-  ui::StoreGpuExtraInfoIntoListValue(gpu_extra_info.system_visual,
-                                     gpu_extra_info.rgba_visual, result);
-  return result;
-}
-
 void DesktopScreenX11::OnEvent(const x11::Event& event) {
   x11_display_manager_->OnEvent(event);
 }
diff --git a/ui/views/widget/desktop_aura/desktop_screen_x11.h b/ui/views/widget/desktop_aura/desktop_screen_x11.h
index dd7725c..efbb849 100644
--- a/ui/views/widget/desktop_aura/desktop_screen_x11.h
+++ b/ui/views/widget/desktop_aura/desktop_screen_x11.h
@@ -53,8 +53,6 @@
   void AddObserver(display::DisplayObserver* observer) override;
   void RemoveObserver(display::DisplayObserver* observer) override;
   std::string GetCurrentWorkspace() override;
-  base::Value GetGpuExtraInfoAsListValue(
-      const gfx::GpuExtraInfo& gpu_extra_info) override;
 
   // x11::EventObserver:
   void OnEvent(const x11::Event& event) override;
diff --git a/ui/views/widget/native_widget_mac_interactive_uitest.mm b/ui/views/widget/native_widget_mac_interactive_uitest.mm
index 77a8c22..e28d988 100644
--- a/ui/views/widget/native_widget_mac_interactive_uitest.mm
+++ b/ui/views/widget/native_widget_mac_interactive_uitest.mm
@@ -14,6 +14,7 @@
 #import "ui/events/test/cocoa_test_event_utils.h"
 #include "ui/views/bubble/bubble_dialog_delegate_view.h"
 #include "ui/views/controls/textfield/textfield.h"
+#include "ui/views/test/native_widget_factory.h"
 #include "ui/views/test/test_widget_observer.h"
 #include "ui/views/test/widget_test.h"
 
@@ -248,6 +249,41 @@
   parent_widget->CloseNow();
 }
 
+// Test activation of a window that has restoration data that was restored to
+// the dock. See crbug.com/1205683 .
+TEST_F(NativeWidgetMacInteractiveUITest,
+       DeminiaturizeWindowWithRestorationData) {
+  Widget* widget = new Widget;
+  Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
+  params.native_widget =
+      CreatePlatformNativeWidgetImpl(widget, kStubCapture, nullptr);
+  // Start the window off in the dock.
+  params.show_state = ui::SHOW_STATE_MINIMIZED;
+  // "{}" in base64encode, to create some dummy restoration data.
+  const std::string kDummyWindowRestorationData = "e30=";
+  params.workspace = kDummyWindowRestorationData;
+  widget->Init(std::move(params));
+
+  NSWindow* window = widget->GetNativeWindow().GetNativeNSWindow();
+  EXPECT_TRUE([window isMiniaturized]);
+
+  // As part of the window restoration process,
+  // SessionRestoreImpl::ShowBrowser() -> BrowserView::Show() ->
+  // views::Widget::Show() -> views::NativeWidgetMac::Show() which calls
+  // SetVisibilityState(), the code path we want to test. Even though the method
+  // name is Show(), it "shows" the saved_show_state_ which in this case is
+  // WindowVisibilityState::kHideWindow.
+  widget->Show();
+  EXPECT_TRUE([window isMiniaturized]);
+
+  // Activate the window from the dock (i.e.
+  // SetVisibilityState(WindowVisibilityState::kShowAndActivateWindow)).
+  widget->Activate();
+  EXPECT_FALSE([window isMiniaturized]);
+
+  widget->CloseNow();
+}
+
 // Test that bubble widgets are dismissed on right mouse down.
 TEST_F(NativeWidgetMacInteractiveUITest, BubbleDismiss) {
   Widget* parent_widget = CreateTopLevelPlatformWidget();
diff --git a/ui/views/widget/native_widget_mac_unittest.mm b/ui/views/widget/native_widget_mac_unittest.mm
index 6ad0a5fe..866fb8bb 100644
--- a/ui/views/widget/native_widget_mac_unittest.mm
+++ b/ui/views/widget/native_widget_mac_unittest.mm
@@ -46,6 +46,11 @@
 #include "ui/views/widget/native_widget_private.h"
 #include "ui/views/window/dialog_delegate.h"
 
+namespace {
+// "{}" in base64encode, to create some dummy restoration data.
+const std::string kDummyWindowRestorationData = "e30=";
+}  // namespace
+
 // Donates an implementation of -[NSAnimation stopAnimation] which calls the
 // original implementation, then quits a nested run loop.
 @interface TestStopAnimationWaiter : NSObject
@@ -110,6 +115,10 @@
         bridge_->show_animation_.get());
   }
 
+  bool HasWindowRestorationData() {
+    return bridge_->HasWindowRestorationData();
+  }
+
  private:
   remote_cocoa::NativeWidgetNSWindowBridge* bridge_;
 
@@ -1174,6 +1183,55 @@
   }
 }
 
+// Tests that the first call into SetVisibilityState() restores the window state
+// for windows that start off miniaturized in the dock.
+TEST_F(NativeWidgetMacTest, ConfirmMinimizedWindowRestoration) {
+  Widget* widget = new Widget;
+  Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
+  params.native_widget =
+      CreatePlatformNativeWidgetImpl(widget, kStubCapture, nullptr);
+  // Start the window off in the dock.
+  params.show_state = ui::SHOW_STATE_MINIMIZED;
+  params.workspace = kDummyWindowRestorationData;
+  widget->Init(std::move(params));
+
+  BridgedNativeWidgetTestApi test_api(
+      widget->GetNativeWindow().GetNativeNSWindow());
+
+  EXPECT_TRUE(test_api.HasWindowRestorationData());
+
+  // Show() ultimately invokes SetVisibilityState().
+  widget->Show();
+
+  EXPECT_FALSE(test_api.HasWindowRestorationData());
+
+  widget->CloseNow();
+}
+
+// Tests that the first call into SetVisibilityState() restores the window state
+// for windows that start off visible.
+TEST_F(NativeWidgetMacTest, ConfirmVisibleWindowRestoration) {
+  Widget* widget = new Widget;
+  Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
+  params.native_widget =
+      CreatePlatformNativeWidgetImpl(widget, kStubCapture, nullptr);
+  params.show_state = ui::SHOW_STATE_NORMAL;
+  params.workspace = kDummyWindowRestorationData;
+  widget->Init(std::move(params));
+
+  BridgedNativeWidgetTestApi test_api(
+      widget->GetNativeWindow().GetNativeNSWindow());
+
+  EXPECT_TRUE(test_api.HasWindowRestorationData());
+
+  // Show() ultimately invokes SetVisibilityState().
+  widget->Show();
+
+  EXPECT_FALSE(test_api.HasWindowRestorationData());
+
+  widget->CloseNow();
+}
+
 // Tests that calls to Hide() a Widget cancel any in-progress show animation,
 // and that clients can control the triggering of the animation.
 TEST_F(NativeWidgetMacTest, ShowAnimationControl) {
diff --git a/ui/views/widget/widget_unittest.cc b/ui/views/widget/widget_unittest.cc
index 01a65c8..429f72b0 100644
--- a/ui/views/widget/widget_unittest.cc
+++ b/ui/views/widget/widget_unittest.cc
@@ -4062,6 +4062,18 @@
 #endif
 }
 
+bool ExpectWidgetTransparency(Widget::InitParams::WindowOpacity opacity) {
+  switch (opacity) {
+    case Widget::InitParams::WindowOpacity::kOpaque:
+      return false;
+    case Widget::InitParams::WindowOpacity::kTranslucent:
+      return true;
+    case Widget::InitParams::WindowOpacity::kInferred:
+      ADD_FAILURE() << "WidgetOpacity must be explicitly set";
+      return false;
+  }
+}
+
 class CompositingWidgetTest : public DesktopWidgetTest {
  public:
   CompositingWidgetTest()
@@ -4118,13 +4130,10 @@
                 should_be_transparent);
 
       if (CanHaveCompositingManager()) {
-        if (HasCompositingManager() &&
-            (widget_type == Widget::InitParams::TYPE_DRAG ||
-             widget_type == Widget::InitParams::TYPE_WINDOW)) {
+        if (HasCompositingManager() && ExpectWidgetTransparency(opacity))
           EXPECT_TRUE(widget->IsTranslucentWindowOpacitySupported());
-        } else {
+        else
           EXPECT_FALSE(widget->IsTranslucentWindowOpacitySupported());
-        }
       }
     }
   }
@@ -4139,11 +4148,8 @@
 
 }  // namespace
 
-// Test opacity when compositing is enabled.
-TEST_F(CompositingWidgetTest, Transparency_DesktopWidgetInferOpacity) {
-  CheckAllWidgetsForOpacity(Widget::InitParams::WindowOpacity::kInferred);
-}
-
+// Only test manually set opacity via kOpaque or kTranslucent.  kInferred is
+// unpredictable and depends on the platform and window type.
 TEST_F(CompositingWidgetTest, Transparency_DesktopWidgetOpaque) {
   CheckAllWidgetsForOpacity(Widget::InitParams::WindowOpacity::kOpaque);
 }
diff --git a/ui/webui/resources/BUILD.gn b/ui/webui/resources/BUILD.gn
index 803d3247..620d18b 100644
--- a/ui/webui/resources/BUILD.gn
+++ b/ui/webui/resources/BUILD.gn
@@ -143,7 +143,10 @@
   "cr_elements/cr_drawer/cr_drawer.d.ts",
   "cr_elements/cr_icon_button/cr_icon_button.m.d.ts",
   "cr_elements/cr_input/cr_input.m.d.ts",
+  "cr_elements/cr_lazy_render/cr_lazy_render.m.d.ts",
+  "cr_elements/cr_search_field/cr_search_field_behavior.d.ts",
   "cr_elements/cr_toast/cr_toast.m.d.ts",
+  "cr_elements/cr_toolbar/cr_toolbar_search_field.d.ts",
   "cr_elements/cr_view_manager/cr_view_manager.d.ts",
   "cr_elements/find_shortcut_behavior.d.ts",
   "js/cr/ui/focus_row_behavior.m.d.ts",
@@ -180,6 +183,7 @@
     "$root_dir/cr_elements/cr_input/cr_input_style_css.m.d.ts",
     "$root_dir/cr_elements/cr_menu_selector/cr_menu_selector.d.ts",
     "$root_dir/cr_elements/cr_splitter/cr_splitter.d.ts",
+    "$root_dir/cr_elements/cr_toolbar/cr_toolbar.d.ts",
     "$root_dir/cr_elements/hidden_style_css.m.d.ts",
     "$root_dir/cr_elements/icons.m.d.ts",
     "$root_dir/cr_elements/md_select_css.m.d.ts",
@@ -191,6 +195,8 @@
     "$root_dir/js/cr/ui/focus_grid.m.d.ts",
     "$root_dir/js/cr/ui/focus_outline_manager.m.d.ts",
     "$root_dir/js/cr/ui/focus_row.m.d.ts",
+    "$root_dir/js/cr/ui/store.m.d.ts",
+    "$root_dir/js/cr/ui/store_client.m.d.ts",
     "$root_dir/js/event_tracker.m.d.ts",
     "$root_dir/js/load_time_data.m.d.ts",
     "$root_dir/js/parse_html_subset.m.d.ts",
@@ -219,6 +225,7 @@
     "cr_elements/cr_input/cr_input_style_css.m.js",
     "cr_elements/cr_menu_selector/cr_menu_selector.js",
     "cr_elements/cr_splitter/cr_splitter.js",
+    "cr_elements/cr_toolbar/cr_toolbar.js",
     "cr_elements/hidden_style_css.m.js",
     "cr_elements/icons.m.js",
     "cr_elements/md_select_css.m.js",
@@ -229,6 +236,8 @@
     "js/cr/event_target.m.js",
     "js/cr/ui/focus_grid.m.js",
     "js/cr/ui/focus_outline_manager.m.js",
+    "js/cr/ui/store.m.js",
+    "js/cr/ui/store_client.m.js",
     "js/cr/ui/focus_row.m.js",
     "js/event_tracker.m.js",
     "js/load_time_data.m.js",
diff --git a/ui/webui/resources/cr_components/BUILD.gn b/ui/webui/resources/cr_components/BUILD.gn
index e1ab44f6..5cb6265 100644
--- a/ui/webui/resources/cr_components/BUILD.gn
+++ b/ui/webui/resources/cr_components/BUILD.gn
@@ -148,6 +148,7 @@
     in_files += [
       "chromeos/bluetooth/bluetooth_dialog.m.js",
       "chromeos/cellular_setup/activation_code_page.m.js",
+      "chromeos/cellular_setup/activation_verification_page.m.js",
       "chromeos/cellular_setup/base_page.m.js",
       "chromeos/cellular_setup/button_bar.m.js",
       "chromeos/cellular_setup/cellular_setup_delegate.m.js",
@@ -275,6 +276,8 @@
     in_files += [
       "chromeos/cellular_setup/activation_code_page.html",
       "chromeos/cellular_setup/activation_code_page.js",
+      "chromeos/cellular_setup/activation_verification_page.html",
+      "chromeos/cellular_setup/activation_verification_page.js",
       "chromeos/cellular_setup/base_page.html",
       "chromeos/cellular_setup/base_page.js",
       "chromeos/cellular_setup/button_bar.html",
diff --git a/ui/webui/resources/cr_components/chromeos/cellular_setup/BUILD.gn b/ui/webui/resources/cr_components/chromeos/cellular_setup/BUILD.gn
index 31776a0..ea45ec53 100644
--- a/ui/webui/resources/cr_components/chromeos/cellular_setup/BUILD.gn
+++ b/ui/webui/resources/cr_components/chromeos/cellular_setup/BUILD.gn
@@ -13,6 +13,7 @@
   is_polymer3 = true
   deps = [
     ":activation_code_page.m",
+    ":activation_verification_page.m",
     ":base_page.m",
     ":button_bar.m",
     ":cellular_eid_dialog.m",
@@ -54,6 +55,17 @@
   extra_deps = [ ":final_page_module" ]
 }
 
+js_library("activation_verification_page.m") {
+  sources = [ "$root_gen_dir/ui/webui/resources/cr_components/chromeos/cellular_setup/activation_verification_page.m.js" ]
+  deps = [
+    ":base_page.m",
+    "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
+    "//ui/webui/resources/cr_elements/cr_lottie:cr_lottie.m",
+    "//ui/webui/resources/js:i18n_behavior.m",
+  ]
+  extra_deps = [ ":activation_verification_page_module" ]
+}
+
 js_library("activation_code_page.m") {
   sources = [ "$root_gen_dir/ui/webui/resources/cr_components/chromeos/cellular_setup/activation_code_page.m.js" ]
   deps = [
@@ -201,6 +213,7 @@
   sources = [ "$root_gen_dir/ui/webui/resources/cr_components/chromeos/cellular_setup/esim_flow_ui.m.js" ]
   deps = [
     ":activation_code_page.m",
+    ":activation_verification_page.m",
     ":cellular_setup_delegate.m",
     ":cellular_types.m",
     ":confirmation_code_page.m",
@@ -245,6 +258,7 @@
 group("polymer3_elements") {
   public_deps = [
     ":activation_code_page_module",
+    ":activation_verification_page_module",
     ":base_page_module",
     ":button_bar_module",
     ":cellular_eid_dialog_module",
@@ -278,6 +292,14 @@
   auto_imports = cr_components_chromeos_auto_imports
 }
 
+polymer_modulizer("activation_verification_page") {
+  js_file = "activation_verification_page.js"
+  html_file = "activation_verification_page.html"
+  html_type = "dom-module"
+  namespace_rewrites = cr_components_chromeos_namespace_rewrites
+  auto_imports = cr_components_chromeos_auto_imports
+}
+
 polymer_modulizer("activation_code_page") {
   js_file = "activation_code_page.js"
   html_file = "activation_code_page.html"
diff --git a/ui/webui/resources/cr_components/chromeos/cellular_setup/activation_verification_page.html b/ui/webui/resources/cr_components/chromeos/cellular_setup/activation_verification_page.html
new file mode 100644
index 0000000..9930ca1c
--- /dev/null
+++ b/ui/webui/resources/cr_components/chromeos/cellular_setup/activation_verification_page.html
@@ -0,0 +1,36 @@
+<link rel="import" href="../../../html/polymer.html">
+
+<link rel="import" href="base_page.html">
+<link rel="import" href="chrome://resources/polymer/v1_0/paper-spinner/paper-spinner-lite.html">
+<link rel="import" href="../../../html/i18n_behavior.html">
+<link rel="import" href="chrome://resources/cr_elements/cr_lottie/cr_lottie.html">
+
+<dom-module id="activation-verification-page">
+  <template>
+    <style>
+      #pageBody {
+        height: 282px;
+        margin-top: -20px;
+        overflow: hidden;
+      }
+
+      #animationContainer {
+        display: flex;
+        height: 216px;
+        margin-bottom: 30px;
+        margin-top: 24px;
+      }
+    </style>
+
+    <base-page>
+      <div slot="page-body" id="pageBody" class="layout vertical center-center">
+        <span>[[i18n('verifyingActivationCode')]]</span>
+        <div id="animationContainer">
+          <cr-lottie id="spinner" animation-url="spinner.json" autoplay>
+          </cr-lottie>
+        </div>
+      </div>
+    </base-page>
+  </template>
+  <script src="activation_verification_page.js"></script>
+</dom-module>
diff --git a/ui/webui/resources/cr_components/chromeos/cellular_setup/activation_verification_page.js b/ui/webui/resources/cr_components/chromeos/cellular_setup/activation_verification_page.js
new file mode 100644
index 0000000..5868da10
--- /dev/null
+++ b/ui/webui/resources/cr_components/chromeos/cellular_setup/activation_verification_page.js
@@ -0,0 +1,13 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * This page is displayed when the activation code is being verified, and
+ * an ESim profile is being installed.
+ */
+Polymer({
+  is: 'activation-verification-page',
+
+  behaviors: [I18nBehavior],
+});
diff --git a/ui/webui/resources/cr_components/chromeos/cellular_setup/esim_flow_ui.html b/ui/webui/resources/cr_components/chromeos/cellular_setup/esim_flow_ui.html
index edc972a..916383a9 100644
--- a/ui/webui/resources/cr_components/chromeos/cellular_setup/esim_flow_ui.html
+++ b/ui/webui/resources/cr_components/chromeos/cellular_setup/esim_flow_ui.html
@@ -12,6 +12,7 @@
 <link rel="import" href="cellular_setup_delegate.html">
 <link rel="import" href="setup_loading_page.html">
 <link rel="import" href="activation_code_page.html">
+<link rel="import" href="activation_verification_page.html">
 <link rel="import" href="final_page.html">
 <link rel="import" href="profile_discovery_list_page.html">
 <link rel="import" href="confirmation_code_page.html">
@@ -43,6 +44,8 @@
           show-error="{{showError_}}"
           show-busy="[[shouldShowSubpageBusy_(state_)]]">
       </activation-code-page>
+      <activation-verification-page id="activationVerificationPage">
+      </activation-verification-page>
       <confirmation-code-page id="confirmationCodePage"
           confirmation-code="{{confirmationCode_}}"
           profile="[[selectedProfile_]]"
diff --git a/ui/webui/resources/cr_components/chromeos/cellular_setup/esim_flow_ui.js b/ui/webui/resources/cr_components/chromeos/cellular_setup/esim_flow_ui.js
index 215120b..ee35f8f 100644
--- a/ui/webui/resources/cr_components/chromeos/cellular_setup/esim_flow_ui.js
+++ b/ui/webui/resources/cr_components/chromeos/cellular_setup/esim_flow_ui.js
@@ -8,6 +8,7 @@
     PROFILE_LOADING: 'profileLoadingPage',
     PROFILE_DISCOVERY: 'profileDiscoveryPage',
     ACTIVATION_CODE: 'activationCodePage',
+    ACTIVATION_VERIFCATION: 'activationVerificationPage',
     CONFIRMATION_CODE: 'confirmationCodePage',
     FINAL: 'finalPage',
   };
@@ -364,9 +365,11 @@
           break;
         case ESimUiState.ACTIVATION_CODE_ENTRY:
         case ESimUiState.ACTIVATION_CODE_ENTRY_READY:
-        case ESimUiState.ACTIVATION_CODE_ENTRY_INSTALLING:
           this.selectedESimPageName_ = ESimPageName.ACTIVATION_CODE;
           break;
+        case ESimUiState.ACTIVATION_CODE_ENTRY_INSTALLING:
+          this.selectedESimPageName_ = ESimPageName.ACTIVATION_VERIFCATION;
+          break;
         case ESimUiState.CONFIRMATION_CODE_ENTRY:
         case ESimUiState.CONFIRMATION_CODE_ENTRY_READY:
         case ESimUiState.CONFIRMATION_CODE_ENTRY_INSTALLING:
diff --git a/ui/webui/resources/cr_elements/cr_lazy_render/cr_lazy_render.m.d.ts b/ui/webui/resources/cr_elements/cr_lazy_render/cr_lazy_render.m.d.ts
new file mode 100644
index 0000000..7f9abac
--- /dev/null
+++ b/ui/webui/resources/cr_elements/cr_lazy_render/cr_lazy_render.m.d.ts
@@ -0,0 +1,18 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LegacyElementMixin} from 'chrome://resources/polymer/v3_0/polymer/lib/legacy/legacy-element-mixin.js';
+
+interface CrLazyRenderElement extends LegacyElementMixin, HTMLElement {
+  get(): Element;
+  getIfExists(): (Element|null);
+}
+
+export {CrLazyRenderElement};
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'cr-lazy-render': CrLazyRenderElement;
+  }
+}
diff --git a/ui/webui/resources/cr_elements/cr_search_field/cr_search_field_behavior.d.ts b/ui/webui/resources/cr_elements/cr_search_field/cr_search_field_behavior.d.ts
new file mode 100644
index 0000000..291713ad
--- /dev/null
+++ b/ui/webui/resources/cr_elements/cr_search_field/cr_search_field_behavior.d.ts
@@ -0,0 +1,20 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+export interface CrSearchFieldBehaviorInterface {
+  label: string;
+  clearLabel: string;
+  hasSearchText: boolean;
+  getSearchInput(): HTMLInputElement;
+  getValue(): string;
+  setValue(value: string, noEvent?: boolean): void;
+  onSearchTermSearch(): void;
+  onSearchTermInput(): void;
+}
+
+export {CrSearchFieldBehavior};
+
+interface CrSearchFieldBehavior extends CrSearchFieldBehaviorInterface {}
+
+declare const CrSearchFieldBehavior: object;
diff --git a/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar.js b/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar.js
index 98d4d1f..46f862f 100644
--- a/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar.js
+++ b/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar.js
@@ -4,82 +4,90 @@
 
 import '../cr_icon_button/cr_icon_button.m.js';
 import '../cr_icons_css.m.js';
-import './cr_toolbar_search_field.js';
 import '../hidden_style_css.m.js';
 import '../icons.m.js';
 import '../shared_vars_css.m.js';
 import '//resources/polymer/v3_0/iron-media-query/iron-media-query.js';
 
-import {html, Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {html, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
-Polymer({
-  is: 'cr-toolbar',
+import {CrToolbarSearchFieldElement} from './cr_toolbar_search_field.js';
 
-  _template: html`{__html_template__}`,
+export class CrToolbarElement extends PolymerElement {
+  static get is() {
+    return 'cr-toolbar';
+  }
 
-  properties: {
-    // Name to display in the toolbar, in titlecase.
-    pageName: String,
+  static get template() {
+    return html`{__html_template__}`;
+  }
 
-    // Prompt text to display in the search field.
-    searchPrompt: String,
+  static get properties() {
+    return {
+      // Name to display in the toolbar, in titlecase.
+      pageName: String,
 
-    // Tooltip to display on the clear search button.
-    clearLabel: String,
+      // Prompt text to display in the search field.
+      searchPrompt: String,
 
-    // Tooltip to display on the menu button.
-    menuLabel: String,
+      // Tooltip to display on the clear search button.
+      clearLabel: String,
 
-    // Value is proxied through to cr-toolbar-search-field. When true,
-    // the search field will show a processing spinner.
-    spinnerActive: Boolean,
+      // Tooltip to display on the menu button.
+      menuLabel: String,
 
-    // Controls whether the menu button is shown at the start of the menu.
-    showMenu: {type: Boolean, value: false},
+      // Value is proxied through to cr-toolbar-search-field. When true,
+      // the search field will show a processing spinner.
+      spinnerActive: Boolean,
 
-    // Controls whether the search field is shown.
-    showSearch: {type: Boolean, value: true},
+      // Controls whether the menu button is shown at the start of the menu.
+      showMenu: {type: Boolean, value: false},
 
-    // Controls whether the search field is autofocused.
-    autofocus: {
-      type: Boolean,
-      value: false,
-      reflectToAttribute: true,
-    },
+      // Controls whether the search field is shown.
+      showSearch: {type: Boolean, value: true},
 
-    // True when the toolbar is displaying in narrow mode.
-    narrow: {
-      type: Boolean,
-      reflectToAttribute: true,
-      readonly: true,
-      notify: true,
-    },
+      // Controls whether the search field is autofocused.
+      autofocus: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
 
-    /**
-     * The threshold at which the toolbar will change from normal to narrow
-     * mode, in px.
-     */
-    narrowThreshold: {
-      type: Number,
-      value: 900,
-    },
+      // True when the toolbar is displaying in narrow mode.
+      narrow: {
+        type: Boolean,
+        reflectToAttribute: true,
+        readonly: true,
+        notify: true,
+      },
 
-    /** @private */
-    showingSearch_: {
-      type: Boolean,
-      reflectToAttribute: true,
-    },
-  },
+      /**
+       * The threshold at which the toolbar will change from normal to narrow
+       * mode, in px.
+       */
+      narrowThreshold: {
+        type: Number,
+        value: 900,
+      },
+
+      /** @private */
+      showingSearch_: {
+        type: Boolean,
+        reflectToAttribute: true,
+      },
+    };
+  }
 
   /** @return {!CrToolbarSearchFieldElement} */
   getSearchField() {
     return /** @type {!CrToolbarSearchFieldElement} */ (this.$.search);
-  },
+  }
 
   /** @private */
   onMenuTap_() {
-    this.fire('cr-toolbar-menu-tap');
-  },
+    this.dispatchEvent(new CustomEvent(
+        'cr-toolbar-menu-tap', {bubbles: true, composed: true}));
+  }
 
   focusMenuButton() {
     requestAnimationFrame(() => {
@@ -90,11 +98,13 @@
         menuButton.focus();
       }
     });
-  },
+  }
 
   /** @return {boolean} */
   isMenuFocused() {
     return Boolean(this.shadowRoot.activeElement) &&
         this.shadowRoot.activeElement.id === 'menuButton';
   }
-});
+}
+
+customElements.define(CrToolbarElement.is, CrToolbarElement);
diff --git a/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar_search_field.d.ts b/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar_search_field.d.ts
new file mode 100644
index 0000000..e619a79
--- /dev/null
+++ b/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar_search_field.d.ts
@@ -0,0 +1,30 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LegacyElementMixin} from 'chrome://resources/polymer/v3_0/polymer/lib/legacy/legacy-element-mixin.js';
+
+import {CrSearchFieldBehaviorInterface} from '../cr_search_field/cr_search_field_behavior.js';
+
+interface CrToolbarSearchFieldElement extends CrSearchFieldBehaviorInterface,
+                                              LegacyElementMixin, HTMLElement {
+  narrow: boolean;
+  showingSearch: boolean;
+  autofocus: boolean;
+  label: string;
+  clearLabel: string;
+  spinnerActive: boolean;
+
+  getSearchInput(): HTMLInputElement;
+  isSearchFocused(): boolean;
+  showAndFocus(): void;
+  onSearchTermInput(): void;
+}
+
+export {CrToolbarSearchFieldElement};
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'cr-toolbar-search-field': CrToolbarSearchFieldElement;
+  }
+}
diff --git a/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js b/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js
index 47be44f..15dd22e2 100644
--- a/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js
+++ b/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js
@@ -9,91 +9,108 @@
 import '../shared_vars_css.m.js';
 import '//resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
 
-import {html, Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {html, mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
-import {CrSearchFieldBehavior} from '../cr_search_field/cr_search_field_behavior.js';
+import {CrSearchFieldBehavior, CrSearchFieldBehaviorInterface} from '../cr_search_field/cr_search_field_behavior.js';
 
-Polymer({
-  is: 'cr-toolbar-search-field',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {CrSearchFieldBehaviorInterface}
+ */
+const CrToolbarSearchFieldElementBase =
+    mixinBehaviors(CrSearchFieldBehavior, PolymerElement);
 
-  _template: html`{__html_template__}`,
+/** @polymer */
+export class CrToolbarSearchFieldElement extends
+    CrToolbarSearchFieldElementBase {
+  static get is() {
+    return 'cr-toolbar-search-field';
+  }
 
-  behaviors: [CrSearchFieldBehavior],
+  static get template() {
+    return html`{__html_template__}`;
+  }
 
-  properties: {
-    narrow: {
-      type: Boolean,
-      reflectToAttribute: true,
-    },
+  static get properties() {
+    return {
+      narrow: {
+        type: Boolean,
+        reflectToAttribute: true,
+      },
 
-    showingSearch: {
-      type: Boolean,
-      value: false,
-      notify: true,
-      observer: 'showingSearchChanged_',
-      reflectToAttribute: true
-    },
+      showingSearch: {
+        type: Boolean,
+        value: false,
+        notify: true,
+        observer: 'showingSearchChanged_',
+        reflectToAttribute: true,
+      },
 
-    autofocus: {
-      type: Boolean,
-      value: false,
-      reflectToAttribute: true,
-    },
+      autofocus: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
 
-    // Prompt text to display in the search field.
-    label: String,
+      // Prompt text to display in the search field.
+      label: String,
 
-    // Tooltip to display on the clear search button.
-    clearLabel: String,
+      // Tooltip to display on the clear search button.
+      clearLabel: String,
 
-    // When true, show a loading spinner to indicate that the backend is
-    // processing the search. Will only show if the search field is open.
-    spinnerActive: {type: Boolean, reflectToAttribute: true},
+      // When true, show a loading spinner to indicate that the backend is
+      // processing the search. Will only show if the search field is open.
+      spinnerActive: {type: Boolean, reflectToAttribute: true},
 
-    /** @private */
-    isSpinnerShown_: {
-      type: Boolean,
-      computed: 'computeIsSpinnerShown_(spinnerActive, showingSearch)'
-    },
+      /** @private */
+      isSpinnerShown_: {
+        type: Boolean,
+        computed: 'computeIsSpinnerShown_(spinnerActive, showingSearch)'
+      },
 
-    /** @private */
-    searchFocused_: {type: Boolean, value: false},
-  },
+      /** @private */
+      searchFocused_: {type: Boolean, value: false},
+    };
+  }
 
-  listeners: {
-    // Deliberately uses 'click' instead of 'tap' to fix crbug.com/624356.
-    'click': 'showSearch_',
-  },
+  /** @override */
+  ready() {
+    super.ready();
+
+    this.addEventListener('click', e => this.showSearch_(e));
+  }
 
   /** @return {!HTMLInputElement} */
   getSearchInput() {
     return /** @type {!HTMLInputElement} */ (this.$.searchInput);
-  },
+  }
 
   /** @return {boolean} */
   isSearchFocused() {
     return this.searchFocused_;
-  },
+  }
 
   showAndFocus() {
     this.showingSearch = true;
     this.focus_();
-  },
+  }
 
   onSearchTermInput() {
     CrSearchFieldBehavior.onSearchTermInput.call(this);
     this.showingSearch = this.hasSearchText || this.isSearchFocused();
-  },
+  }
 
   /** @private */
   onSearchIconClicked_() {
-    this.fire('search-icon-clicked');
-  },
+    this.dispatchEvent(new CustomEvent(
+        'search-icon-clicked', {bubbles: true, composed: true}));
+  }
 
   /** @private */
   focus_() {
     this.getSearchInput().focus();
-  },
+  }
 
   /**
    * @param {boolean} narrow
@@ -102,7 +119,7 @@
    */
   computeIconTabIndex_(narrow) {
     return narrow && !this.hasSearchText ? 0 : -1;
-  },
+  }
 
   /**
    * @param {boolean} narrow
@@ -111,7 +128,7 @@
    */
   computeIconAriaHidden_(narrow) {
     return Boolean(!narrow || this.hasSearchText).toString();
-  },
+  }
 
   /**
    * @return {boolean}
@@ -123,12 +140,12 @@
       this.$.spinnerTemplate.if = true;
     }
     return showSpinner;
-  },
+  }
 
   /** @private */
   onInputFocus_() {
     this.searchFocused_ = true;
-  },
+  }
 
   /** @private */
   onInputBlur_() {
@@ -136,14 +153,14 @@
     if (!this.hasSearchText) {
       this.showingSearch = false;
     }
-  },
+  }
 
   /** @private */
   onSearchTermKeydown_(e) {
     if (e.key === 'Escape') {
       this.showingSearch = false;
     }
-  },
+  }
 
   /**
    * @param {Event} e
@@ -153,7 +170,7 @@
     if (e.target !== this.$.clearSearch) {
       this.showingSearch = true;
     }
-  },
+  }
 
   /**
    * @param {Event} e
@@ -163,7 +180,7 @@
     this.setValue('');
     this.focus_();
     this.spinnerActive = false;
-  },
+  }
 
   /**
    * @param {boolean} current
@@ -183,5 +200,8 @@
 
     this.setValue('');
     this.getSearchInput().blur();
-  },
-});
+  }
+}
+
+customElements.define(
+    CrToolbarSearchFieldElement.is, CrToolbarSearchFieldElement);
diff --git a/weblayer/app/content_main_delegate_impl.cc b/weblayer/app/content_main_delegate_impl.cc
index 26f4e70..0c8d1de1b 100644
--- a/weblayer/app/content_main_delegate_impl.cc
+++ b/weblayer/app/content_main_delegate_impl.cc
@@ -178,6 +178,8 @@
     blink::features::kPortals,
     // TODO(crbug.com/1174566): Enable by default after experiment.
     content_capture::features::kContentCapture,
+    // TODO(crbug.com/1144912): Support BackForwardCache on WebLayer.
+    ::features::kBackForwardCache,
 
 #if defined(OS_ANDROID)
     // TODO(crbug.com/1131016): Support Picture in Picture API on WebLayer.
diff --git a/weblayer/browser/browser_context_impl.cc b/weblayer/browser/browser_context_impl.cc
index aad4f42..431ae2b 100644
--- a/weblayer/browser/browser_context_impl.cc
+++ b/weblayer/browser/browser_context_impl.cc
@@ -276,6 +276,8 @@
       embedder_support::kAlternateErrorPagesEnabled, true);
   pref_registry->RegisterListPref(
       site_isolation::prefs::kUserTriggeredIsolatedOrigins);
+  pref_registry->RegisterDictionaryPref(
+      site_isolation::prefs::kWebTriggeredIsolatedOrigins);
 
   StatefulSSLHostStateDelegate::RegisterProfilePrefs(pref_registry);
   HostContentSettingsMap::RegisterProfilePrefs(pref_registry);
diff --git a/weblayer/browser/persistence/browser_persister.cc b/weblayer/browser/persistence/browser_persister.cc
index 3b15de2..2298243 100644
--- a/weblayer/browser/persistence/browser_persister.cc
+++ b/weblayer/browser/persistence/browser_persister.cc
@@ -61,7 +61,7 @@
               sessions::CommandStorageManager::kOther,
               path,
               this,
-              /* use_marker */ false,
+              /* use_marker */ true,
               browser->profile()->GetBrowserContext()->IsOffTheRecord(),
               decryption_key)),
       rebuild_on_next_save_(false),
@@ -106,8 +106,8 @@
 }
 
 void BrowserPersister::OnErrorWritingSessionCommands() {
-  // TODO(https://crbug.com/648266): implement this.
-  NOTIMPLEMENTED();
+  rebuild_on_next_save_ = true;
+  command_storage_manager_->StartSaveTimer();
 }
 
 void BrowserPersister::OnTabAdded(Tab* tab) {
diff --git a/weblayer/browser/persistence/browser_persister_browsertest.cc b/weblayer/browser/persistence/browser_persister_browsertest.cc
index f1c5680..5ab10e2 100644
--- a/weblayer/browser/persistence/browser_persister_browsertest.cc
+++ b/weblayer/browser/persistence/browser_persister_browsertest.cc
@@ -418,4 +418,35 @@
   }
 }
 
+IN_PROC_BROWSER_TEST_F(BrowserPersisterTest, OnErrorWritingSessionCommands) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  std::unique_ptr<BrowserImpl> browser = CreateBrowser(GetProfile(), "x");
+  Tab* tab = browser->CreateTab();
+  EXPECT_TRUE(browser->IsRestoringPreviousState());
+  const GURL url = embedded_test_server()->GetURL("/simple_page.html");
+  NavigateAndWaitForCompletion(url, tab);
+  static_cast<sessions::CommandStorageManagerDelegate*>(
+      browser->browser_persister())
+      ->OnErrorWritingSessionCommands();
+  ShutdownBrowserPersisterAndWait(browser.get());
+  tab = nullptr;
+  browser.reset();
+
+  browser = CreateBrowser(GetProfile(), "x");
+  // Should be no tabs while waiting for restore.
+  EXPECT_TRUE(browser->GetTabs().empty());
+  EXPECT_TRUE(browser->IsRestoringPreviousState());
+  // Wait for the restore and navigation to complete.
+  BrowserNavigationObserverImpl::WaitForNewTabToCompleteNavigation(
+      browser.get(), url);
+
+  ASSERT_EQ(1u, browser->GetTabs().size());
+  EXPECT_EQ(browser->GetTabs()[0], browser->GetActiveTab());
+  EXPECT_EQ(1, browser->GetTabs()[0]
+                   ->GetNavigationController()
+                   ->GetNavigationListSize());
+  EXPECT_FALSE(browser->IsRestoringPreviousState());
+}
+
 }  // namespace weblayer